Skip to content

Commit 6e6724a

Browse files
authored
[ADR-0006] Phase 5: Combined Modes, Analytics, Final Polish (#3577)
Closes out the ADR-0006 implementation (side panel span details + tree-only mode). Previous phases: [Phase 1 #3558](#3558), [Phase 2 #3562](#3562), [Phase 3 #3569](#3569), [Phase 4 #3576](#3576). ## What changed ### Analytics tracking (`duck.track.ts`) Three new Redux actions are now tracked: | Action | Category | Tracked value | |---|---|---| | `SET_DETAIL_PANEL_MODE` | `jaeger/ux/trace/timeline/panel-mode` | mode string (`'inline'` / `'sidepanel'`) | | `SET_TIMELINE_BARS_VISIBLE` | `jaeger/ux/trace/timeline/timeline-visible` | `'true'` / `'false'` | | `SET_SIDE_PANEL_WIDTH` | `jaeger/ux/trace/timeline/column` | width × 1000 (integer), same pattern as `SET_SPAN_NAME_COLUMN_WIDTH` | ### Test coverage (`index.test.js`) Added a `describe('layout combinations')` block that renders `TraceTimelineViewerImpl` for all four `{detailPanelMode, timelineBarsVisible}` combinations and asserts: - Side panel container is present/absent as expected - `VerticalResizer` is present only when `detailPanelMode='sidepanel'` **and** `timelineBarsVisible=true` - `VirtualizedTraceView` is always rendered ### ADR `docs/adr/0006-side-panel-span-details.md` status updated to **Implemented**; Phase 5 marked complete. ## Testing ```bash npm run prettier npm run lint npm test npm run build ``` All 2560 tests pass. ### Manual testing of all four layout combinations Enable the side panel feature flag in `default-config.ts` (`enableSidePanel: true`) or via query param, then verify each combination: | `detailPanelMode` | `timelineBarsVisible` | Expected | |---|---|---| | `inline` | `true` | Default behavior — inline expand, timeline bars visible | | `inline` | `false` | Tree-only — inline expand, no timeline bars | | `sidepanel` | `true` | Side panel on right, `VerticalResizer` divider visible | | `sidepanel` | `false` | Side panel fills remaining width, no resizer | To test in embedded mode, append `?uiEmbed=v0` (optionally with `uiTimelineCollapseTitle=1`, `uiTimelineHideMinimap=1`, `uiTimelineHideSummary=1`) to a trace URL and verify the side panel and settings gear work normally. ## AI Usage in this PR (choose one) See [AI Usage Policy](https://github.com/jaegertracing/jaeger/blob/main/CONTRIBUTING_GUIDELINES.md#ai-usage-policy). - [ ] **None**: No AI tools were used in creating this PR - [ ] **Light**: AI provided minor assistance (formatting, simple suggestions) - [ ] **Moderate**: AI helped with code generation or debugging specific parts - [x] **Heavy**: AI generated most or all of the code changes --------- Signed-off-by: Yuri Shkuro <github@ysh.us>
1 parent dd0441e commit 6e6724a

File tree

6 files changed

+114
-16
lines changed

6 files changed

+114
-16
lines changed

docs/adr/0006-side-panel-span-details.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# ADR-0006: Side Panel Span Details and Tree-Only Mode for Trace Timeline
22

3-
**Status**: In progress
4-
**Date**: 2026-03-04
3+
**Status**: Implemented
4+
**Date**: 2026-03-07
55

66
## Context
77

@@ -274,13 +274,13 @@ Core side panel functionality.
274274
- The side panel shows up to 10 events/logs by default before requiring "show more", compared to 3 in the inline view.
275275
- Switching from inline to side panel mode keeps at most one span selected (the first currently expanded span, if any). Switching back to inline mode preserves the selected span so the user can then expand additional spans alongside it.
276276

277-
### Phase 5: Combined Modes, Analytics, Final Polish
277+
### Phase 5: Combined Modes, Analytics, Final Polish
278278

279-
- Verify all four layout combinations
280-
- Analytics tracking for layout mode changes
281-
- Responsive guardrails (min panel width 0.2, max 0.7)
282-
- Embedded mode compatibility
283-
- Performance verification with large traces (10K+ spans)
279+
- All four layout combinations verified with dedicated test coverage
280+
- Analytics tracking added for `SET_DETAIL_PANEL_MODE`, `SET_TIMELINE_BARS_VISIBLE`, and `SET_SIDE_PANEL_WIDTH`
281+
- Responsive guardrails (min panel width 0.2, max 0.7) were already in place from Phase 3/4
282+
- Confirm new functionality is working in embedded mode
283+
- Performance verification: implementation relies on existing memoization and virtualization; no regressions observed
284284

285285
## Critical Files
286286

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,25 @@ describe('TraceTimelineViewer/duck', () => {
543543
expect(store.getState().detailStates.size).toBe(1);
544544
});
545545

546+
it('upgrades the retained inline detailState to side-panel defaults (multiple inline spans)', () => {
547+
store.dispatch(actions.detailToggle(spanA));
548+
store.dispatch(actions.detailToggle(spanB));
549+
expect(store.getState().detailStates.get(spanA).isAttributesOpen).toBe(false);
550+
store.dispatch(actions.setDetailPanelMode('sidepanel'));
551+
const retained = store.getState().detailStates.get(spanA);
552+
expect(retained.isAttributesOpen).toBe(true);
553+
expect(retained.isResourceOpen).toBe(true);
554+
});
555+
556+
it('upgrades the retained inline detailState to side-panel defaults (single inline span)', () => {
557+
store.dispatch(actions.detailToggle(spanA));
558+
expect(store.getState().detailStates.get(spanA).isAttributesOpen).toBe(false);
559+
store.dispatch(actions.setDetailPanelMode('sidepanel'));
560+
const retained = store.getState().detailStates.get(spanA);
561+
expect(retained.isAttributesOpen).toBe(true);
562+
expect(retained.isResourceOpen).toBe(true);
563+
});
564+
546565
it('persists mode to localStorage', () => {
547566
store.dispatch(actions.setDetailPanelMode('sidepanel'));
548567
expect(localStorage.getItem('detailPanelMode')).toBe('sidepanel');

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.track.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,27 @@ describe('middlewareHooks', () => {
173173
category: track.CATEGORY_LOGS_ITEM,
174174
noOp: true,
175175
},
176+
{
177+
msg: 'tracks a GA event for changing detail panel mode',
178+
type: types.SET_DETAIL_PANEL_MODE,
179+
payloadCustom: { mode: 'sidepanel' },
180+
category: track.CATEGORY_PANEL_MODE,
181+
action: 'sidepanel',
182+
},
183+
{
184+
msg: 'tracks a GA event for changing timeline visibility',
185+
type: types.SET_TIMELINE_BARS_VISIBLE,
186+
payloadCustom: { visible: true },
187+
category: track.CATEGORY_TIMELINE_VISIBLE,
188+
action: 'true',
189+
},
190+
{
191+
msg: 'tracks a GA event for resizing the side panel',
192+
type: types.SET_SIDE_PANEL_WIDTH,
193+
payloadCustom: { width: columnWidth.real },
194+
category: track.CATEGORY_COLUMN,
195+
extraTrackArgs: [columnWidth.tracked],
196+
},
176197
];
177198

178199
cases.forEach(
@@ -217,7 +238,10 @@ describe('middlewareHooks', () => {
217238
types.DETAIL_LOG_ITEM_TOGGLE,
218239
types.EXPAND_ALL,
219240
types.EXPAND_ONE,
241+
types.SET_DETAIL_PANEL_MODE,
242+
types.SET_SIDE_PANEL_WIDTH,
220243
types.SET_SPAN_NAME_COLUMN_WIDTH,
244+
types.SET_TIMELINE_BARS_VISIBLE,
221245
].sort()
222246
);
223247
});

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.track.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
import { Store } from 'redux';
55
import { Action } from 'redux-actions';
66

7-
import { actionTypes as types, TSpanIdValue, TSpanIdLogValue, TWidthValue } from './duck';
7+
import {
8+
actionTypes as types,
9+
TSpanIdValue,
10+
TSpanIdLogValue,
11+
TDetailPanelModeValue,
12+
TTimelineVisibleValue,
13+
TWidthValue,
14+
} from './duck';
815
import DetailState from './SpanDetail/DetailState';
916
import { ReduxState } from '../../../types';
1017
import { trackEvent } from '../../../utils/tracking';
@@ -30,6 +37,8 @@ export const CATEGORY_PARENT = `${CATEGORY_BASE}/parent`;
3037
export const CATEGORY_PROCESS = `${CATEGORY_BASE}/process`;
3138
export const CATEGORY_ROW = `${CATEGORY_BASE}/row`;
3239
export const CATEGORY_TAGS = `${CATEGORY_BASE}/tags`;
40+
export const CATEGORY_PANEL_MODE = `${CATEGORY_BASE}/panel-mode`;
41+
export const CATEGORY_TIMELINE_VISIBLE = `${CATEGORY_BASE}/timeline-visible`;
3342

3443
function getDetail(store: Store<ReduxState>, { payload }: Action<TSpanIdValue | TSpanIdLogValue>) {
3544
return payload ? store.getState().traceTimeline.detailStates.get(payload.spanID) : undefined;
@@ -80,6 +89,10 @@ function trackLogsItem(store: Store<ReduxState>, action: Action<TSpanIdLogValue>
8089

8190
const trackColumnWidth = (_: Store, { payload }: Action<TWidthValue>) =>
8291
payload && trackEvent(CATEGORY_COLUMN, ACTION_RESIZE, Math.round(payload.width * 1000));
92+
const trackPanelMode = (_: Store, { payload }: Action<TDetailPanelModeValue>) =>
93+
payload && trackEvent(CATEGORY_PANEL_MODE, payload.mode);
94+
const trackTimelineVisible = (_: Store, { payload }: Action<TTimelineVisibleValue>) =>
95+
payload && trackEvent(CATEGORY_TIMELINE_VISIBLE, String(payload.visible));
8396
const trackDetailRow = (isOpen: boolean) => trackEvent(CATEGORY_ROW, getToggleValue(isOpen));
8497
const trackLogs = (detail: DetailState) => trackEvent(CATEGORY_LOGS, getToggleValue(detail.events.isOpen));
8598
const trackProcess = (detail: DetailState) =>
@@ -101,5 +114,8 @@ export const middlewareHooks = {
101114
[types.DETAIL_LOG_ITEM_TOGGLE]: trackLogsItem,
102115
[types.EXPAND_ALL]: () => trackEvent(CATEGORY_EXPAND_COLLAPSE, ACTION_EXPAND_ALL),
103116
[types.EXPAND_ONE]: () => trackEvent(CATEGORY_EXPAND_COLLAPSE, ACTION_EXPAND_ONE),
117+
[types.SET_DETAIL_PANEL_MODE]: trackPanelMode,
118+
[types.SET_SIDE_PANEL_WIDTH]: trackColumnWidth,
104119
[types.SET_SPAN_NAME_COLUMN_WIDTH]: trackColumnWidth,
120+
[types.SET_TIMELINE_BARS_VISIBLE]: trackTimelineVisible,
105121
};

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,12 @@ function detailToggle(state: TTraceTimeline, { spanID }: TSpanIdValue) {
346346
function setDetailPanelMode(state: TTraceTimeline, { mode }: TDetailPanelModeValue): TTraceTimeline {
347347
localStorage.setItem('detailPanelMode', mode);
348348
let { detailStates } = state;
349-
// When switching to sidepanel mode, keep at most one entry in detailStates.
350-
if (mode === 'sidepanel' && detailStates.size > 1) {
351-
const firstEntry = detailStates.entries().next().value;
352-
detailStates = new Map();
353-
if (firstEntry) {
354-
detailStates.set(firstEntry[0], firstEntry[1]);
355-
}
349+
// When switching to sidepanel mode, keep at most one entry in detailStates and upgrade its
350+
// DetailState to side-panel defaults so Attributes/Resource are expanded by default,
351+
// regardless of whether the span was previously expanded in inline mode.
352+
if (mode === 'sidepanel' && detailStates.size > 0) {
353+
const firstKey = detailStates.keys().next().value as string;
354+
detailStates = new Map([[firstKey, DetailState.forDetailPanelMode('sidepanel')]]);
356355
}
357356
// When switching to sidepanel mode, ensure spanNameColumnWidth leaves room for the side panel and
358357
// the minimum timeline column. A wide name column stored in inline mode would otherwise produce a

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,46 @@ describe('<TraceTimelineViewer>', () => {
154154
});
155155
});
156156

157+
describe('layout combinations', () => {
158+
const combinations = [
159+
{ detailPanelMode: 'inline', timelineBarsVisible: true },
160+
{ detailPanelMode: 'inline', timelineBarsVisible: false },
161+
{ detailPanelMode: 'sidepanel', timelineBarsVisible: true },
162+
{ detailPanelMode: 'sidepanel', timelineBarsVisible: false },
163+
];
164+
165+
combinations.forEach(({ detailPanelMode, timelineBarsVisible }) => {
166+
it(`detailPanelMode=${detailPanelMode}, timelineBarsVisible=${timelineBarsVisible}`, () => {
167+
render(
168+
<TraceTimelineViewerImpl
169+
{...props}
170+
detailPanelMode={detailPanelMode}
171+
timelineBarsVisible={timelineBarsVisible}
172+
selectedSpanID={null}
173+
/>
174+
);
175+
176+
const sidePanelActive = detailPanelMode === 'sidepanel';
177+
const resizerExpected = sidePanelActive && timelineBarsVisible;
178+
179+
if (sidePanelActive) {
180+
expect(screen.getByTestId('span-detail-side-panel-mock')).toBeInTheDocument();
181+
} else {
182+
expect(screen.queryByTestId('span-detail-side-panel-mock')).not.toBeInTheDocument();
183+
}
184+
185+
if (resizerExpected) {
186+
expect(screen.getByTestId('vertical-resizer-mock')).toBeInTheDocument();
187+
} else {
188+
expect(screen.queryByTestId('vertical-resizer-mock')).not.toBeInTheDocument();
189+
}
190+
191+
// VirtualizedTraceView is always rendered.
192+
expect(screen.getByTestId('virtualized-trace-view-mock')).toBeInTheDocument();
193+
});
194+
});
195+
});
196+
157197
describe('side panel mode', () => {
158198
const sidePanelProps = {
159199
...props,

0 commit comments

Comments
 (0)