From e680d87e83002c1fdc08d89cac216ace45d59916 Mon Sep 17 00:00:00 2001 From: Karl Power Date: Wed, 11 Mar 2026 18:37:32 +0100 Subject: [PATCH 1/2] feat: show inline span durations in trace timeline --- .changeset/olive-hounds-double.md | 5 ++ .../src/components/DBTraceWaterfallChart.tsx | 1 + .../TimelineChart/TimelineChartRowEvents.tsx | 74 ++++++++++++++----- 3 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 .changeset/olive-hounds-double.md diff --git a/.changeset/olive-hounds-double.md b/.changeset/olive-hounds-double.md new file mode 100644 index 000000000..9a8fe47ab --- /dev/null +++ b/.changeset/olive-hounds-double.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +feat: show inline span durations in trace timeline diff --git a/packages/app/src/components/DBTraceWaterfallChart.tsx b/packages/app/src/components/DBTraceWaterfallChart.tsx index d02e8cd25..b12fc334b 100644 --- a/packages/app/src/components/DBTraceWaterfallChart.tsx +++ b/packages/app/src/components/DBTraceWaterfallChart.tsx @@ -820,6 +820,7 @@ export function DBTraceWaterfallChartContainer({ minWidthPerc: 1, isError, markers, + showDuration: type !== SourceKind.Log, }, ], }; diff --git a/packages/app/src/components/TimelineChart/TimelineChartRowEvents.tsx b/packages/app/src/components/TimelineChart/TimelineChartRowEvents.tsx index 29bd246cd..55f541e9f 100644 --- a/packages/app/src/components/TimelineChart/TimelineChartRowEvents.tsx +++ b/packages/app/src/components/TimelineChart/TimelineChartRowEvents.tsx @@ -5,6 +5,7 @@ import { TimelineSpanEventMarker, type TTimelineSpanEventMarker, } from './TimelineSpanEventMarker'; +import { renderMs } from './utils'; export type TTimelineEvent = { id: string; @@ -17,6 +18,7 @@ export type TTimelineEvent = { minWidthPerc?: number; isError?: boolean; markers?: TTimelineSpanEventMarker[]; + showDuration?: boolean; }; type TimelineChartRowProps = { @@ -57,6 +59,12 @@ export const TimelineChartRowEvents = memo(function ({ const percMarginLeft = scale * (((e.start - lastEventEnd) / maxVal) * 100); + const durationMs = e.end - e.start; + const barCenter = (e.start + e.end) / 2; + const timelineMidpoint = maxVal / 2; + // Duration on left when majority of bar is past halfway, otherwise on right + const durationOnRight = barCenter <= timelineMidpoint; + return (
onEventHover?.(e.id)} - className="d-flex align-items-center h-100 cursor-pointer text-truncate hover-opacity" style={{ - userSelect: 'none', + position: 'relative', minWidth: `${percWidth.toFixed(6)}%`, width: `${percWidth.toFixed(6)}%`, marginLeft: `${percMarginLeft.toFixed(6)}%`, - position: 'relative', - borderRadius: 2, - fontSize: height * 0.5, - color: e.color, - backgroundColor: e.backgroundColor, + // overflow: 'visible', }} > -
- {e.body} +
onEventHover?.(e.id)} + className="d-flex align-items-center h-100 cursor-pointer text-truncate hover-opacity" + style={{ + userSelect: 'none', + width: '100%', + position: 'relative', + borderRadius: 2, + fontSize: height * 0.5, + color: e.color, + backgroundColor: e.backgroundColor, + }} + > +
+ {e.body} +
+ {e.markers?.map((marker, idx) => ( + + ))}
- {e.markers?.map((marker, idx) => ( - - ))} + {!!e.showDuration && ( + + {renderMs(durationMs)} + + )}
); From 2290d75a965378b0548bba8cbf7124b1bb41f048 Mon Sep 17 00:00:00 2001 From: Karl Power Date: Mon, 16 Mar 2026 10:35:17 +0100 Subject: [PATCH 2/2] add microsecond formatting --- .../TimelineChart/__tests__/utils.test.ts | 24 +++++++++++++++---- .../app/src/components/TimelineChart/utils.ts | 8 +++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/TimelineChart/__tests__/utils.test.ts b/packages/app/src/components/TimelineChart/__tests__/utils.test.ts index 25bb5bf9e..b51746d61 100644 --- a/packages/app/src/components/TimelineChart/__tests__/utils.test.ts +++ b/packages/app/src/components/TimelineChart/__tests__/utils.test.ts @@ -1,10 +1,6 @@ import { calculateInterval, renderMs } from '../utils'; describe('renderMs', () => { - it('returns "0ms" for 0', () => { - expect(renderMs(0)).toBe('0ms'); - }); - it('formats sub-second values as ms', () => { expect(renderMs(500)).toBe('500ms'); expect(renderMs(999)).toBe('999ms'); @@ -25,6 +21,26 @@ describe('renderMs', () => { expect(renderMs(1500)).toBe('1.500s'); expect(renderMs(1234.567)).toBe('1.235s'); }); + + it('returns "0µs" for 0', () => { + expect(renderMs(0)).toBe('0µs'); + }); + + it('formats sub-millisecond values as µs', () => { + expect(renderMs(0.001)).toBe('1µs'); + expect(renderMs(0.5)).toBe('500µs'); + expect(renderMs(0.999)).toBe('999µs'); + }); + + it('rounds sub-millisecond values to nearest µs', () => { + expect(renderMs(0.0005)).toBe('1µs'); + expect(renderMs(0.9994)).toBe('999µs'); + }); + + it('falls through to ms when µs rounds to 1000', () => { + // 0.9995ms rounds to 1000µs, so it should render as 1ms instead + expect(renderMs(0.9995)).toBe('1ms'); + }); }); describe('calculateInterval', () => { diff --git a/packages/app/src/components/TimelineChart/utils.ts b/packages/app/src/components/TimelineChart/utils.ts index 68570f5b7..dc1fe1757 100644 --- a/packages/app/src/components/TimelineChart/utils.ts +++ b/packages/app/src/components/TimelineChart/utils.ts @@ -1,4 +1,12 @@ export function renderMs(ms: number) { + if (ms < 1) { + const µsRounded = Math.round(ms * 1000); + + if (µsRounded !== 1000) { + return `${µsRounded}µs`; + } + } + if (ms < 1000) { return `${Math.round(ms)}ms`; }