Repository navigation
feat(app): show RED metrics on the trace search results view #2826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@hyperdx/app': minor | ||
| --- | ||
|
|
||
| Show RED metrics (Throughput, Errors, Duration) above the trace search results instead of the single count histogram. The three charts share a synced hover cursor, Errors toggles between rate and volume, and a RED/Heatmap switch flips the area to the duration heatmap. Logs and other sources keep the existing histogram. | ||
|
|
||
| The shared "error span" definition now also matches the legacy `STATUS_CODE_ERROR` status value in addition to `Error`, so error counts on the Services and LLM dashboards include those spans too. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,7 @@ import { | |
| Group, | ||
| Modal, | ||
| Paper, | ||
| SegmentedControl, | ||
| Select, | ||
| Stack, | ||
| Text, | ||
|
|
@@ -140,6 +141,10 @@ import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar'; | |
| import PatternTable from './components/PatternTable'; | ||
| import { DBSearchHeatmapChart } from './components/Search/DBSearchHeatmapChart'; | ||
| import DirectTraceSidePanel from './components/Search/DirectTraceSidePanel'; | ||
| import { | ||
| type TraceChartMode, | ||
| TraceRedMetricsChart, | ||
| } from './components/Search/TraceRedMetricsChart'; | ||
| import SourceSchemaPreview, { | ||
| isSourceSchemaPreviewEnabled, | ||
| } from './components/SourceSchemaPreview'; | ||
|
|
@@ -206,6 +211,13 @@ const SearchConfigSchema = z.object({ | |
| type SearchConfigFromSchema = z.infer<typeof SearchConfigSchema>; | ||
|
|
||
| const QUERY_KEY_PREFIX = 'search'; | ||
| // The RED tiles are auxiliary viz, not the results query, so they key their | ||
| // queries under a separate prefix to stay out of the `search executed` | ||
| // telemetry (which matches [QUERY_KEY_PREFIX] only). Otherwise the slow | ||
| // avg/p95/p99 duration query would become what the reported search latency | ||
| // measures. The live-tail pause, on the other hand, does wait on this prefix | ||
| // (see the pause signal below) so live ticks don't stack fresh quantile scans. | ||
| const RED_QUERY_KEY_PREFIX = `${QUERY_KEY_PREFIX}-red`; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — RED queries bypass the live-tail pause gate, so live ticks can stack them
|
||
|
|
||
| // Clicks inside the results panel keep the row side panel open (so users can | ||
| // scroll the table or select a different row); clicks anywhere else on the page | ||
|
|
@@ -1061,6 +1073,14 @@ export function DBSearchPage() { | |
| ]).withDefault('results'), | ||
| ); | ||
|
|
||
| // RED metrics vs heatmap for the trace results chart area. URL state (like | ||
| // the other view toggles on this page) so a reload or shared link keeps the | ||
| // chosen view; the switch lives inline in the search stats row. | ||
| const [traceChartMode, setTraceChartMode] = useQueryState( | ||
| 'traceChartMode', | ||
| parseAsStringEnum<TraceChartMode>(['red', 'heatmap']).withDefault('red'), | ||
| ); | ||
|
|
||
| const [patternColumn, setPatternColumn] = useQueryState( | ||
| 'patternColumn', | ||
| parseAsString, | ||
|
|
@@ -1611,6 +1631,16 @@ export function DBSearchPage() { | |
| queryKey: [QUERY_KEY_PREFIX], | ||
| }) > 0; | ||
|
|
||
| // Live tail also waits on the RED metric queries. They live under a separate | ||
| // prefix (kept out of the search-latency telemetry above), so without this | ||
| // each ~10s tick would fire a fresh avg/p95/p99 scan without waiting for the | ||
| // previous one, stacking slow quantile queries while live tail runs. Resolves | ||
| // to 0 when the RED tiles aren't mounted, so it's a no-op off the trace view. | ||
| const isRedQueryFetching = | ||
| useIsFetching({ | ||
| queryKey: [RED_QUERY_KEY_PREFIX], | ||
| }) > 0; | ||
|
|
||
| const { searchElapsedMs } = useSearchTelemetry({ | ||
| isAnyQueryFetching, | ||
| isLive: isLive ?? false, | ||
|
|
@@ -1665,7 +1695,8 @@ export function DBSearchPage() { | |
| interval, | ||
| refreshFrequency, | ||
| onTimeRangeSelect, | ||
| pause: isAnyQueryFetching || !queryReady || !isTabVisible, | ||
| pause: | ||
| isAnyQueryFetching || isRedQueryFetching || !queryReady || !isTabVisible, | ||
| }); | ||
|
|
||
| // This ensures we only render this conditionally on the client | ||
|
|
@@ -1864,6 +1895,34 @@ export function DBSearchPage() { | |
|
|
||
| const aliasWith = useMemo(() => aliasMapToWithClauses(aliasMap), [aliasMap]); | ||
|
|
||
| // The trace results chart shows RED metrics (and a heatmap) only for a trace | ||
| // source that exposes a duration column; otherwise the single histogram | ||
| // stays. Derived once, as the narrowed source or null, so the stats-row | ||
| // toggle and the chart branch can't drift apart on which one renders. | ||
| const traceRedMetricsSource = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor —
|
||
| searchedSource != null && | ||
| isTraceSource(searchedSource) && | ||
| searchedSource.durationExpression | ||
| ? searchedSource | ||
| : null; | ||
|
|
||
| // Reset to the default RED view when the source actually changes, so | ||
| // returning to a trace source after viewing another one doesn't silently | ||
| // reopen in Heatmap. The ref starts undefined and is only compared once a | ||
| // real id has been seen, so the async source resolution on first load | ||
| // (undefined -> id) doesn't count as a change and a shared link that pins | ||
| // ?traceChartMode=heatmap survives. | ||
| const prevSourceIdRef = useRef<string | undefined>(undefined); | ||
| useEffect(() => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major — Source-change reset fires on the first source resolution, so
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Source-change reset clobbers The effect compares only Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| if ( | ||
| prevSourceIdRef.current !== undefined && | ||
| prevSourceIdRef.current !== searchedSource?.id | ||
| ) { | ||
| setTraceChartMode('red'); | ||
| } | ||
| prevSourceIdRef.current = searchedSource?.id; | ||
| }, [searchedSource?.id, setTraceChartMode]); | ||
|
|
||
| const histogramTimeChartConfig = useMemo(() => { | ||
| if (chartConfig == null) { | ||
| return undefined; | ||
|
|
@@ -2491,11 +2550,7 @@ export function DBSearchPage() { | |
| setAnalysisMode={setAnalysisMode} | ||
| chartConfig={filtersChartConfig} | ||
| sourceId={inputSourceObj?.id} | ||
| showDelta={ | ||
| !!(searchedSource?.kind === SourceKind.Trace | ||
| ? searchedSource.durationExpression | ||
| : undefined) | ||
| } | ||
| showDelta={traceRedMetricsSource != null} | ||
| onColumnToggle={toggleColumn} | ||
| displayedColumns={displayedColumns} | ||
| onCollapse={() => setIsFilterSidebarCollapsed(true)} | ||
|
|
@@ -2612,6 +2667,21 @@ export function DBSearchPage() { | |
| enableParallelQueries | ||
| /> | ||
| <Group gap="sm" align="center"> | ||
| {traceRedMetricsSource != null && ( | ||
| <SegmentedControl | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — RED/Heatmap toggle stays visible when the chart area is suppressed by a query error The |
||
| size="xs" | ||
| value={traceChartMode} | ||
| onChange={v => | ||
| setTraceChartMode( | ||
| v === 'heatmap' ? 'heatmap' : 'red', | ||
| ) | ||
| } | ||
| data={[ | ||
| { label: 'RED', value: 'red' }, | ||
| { label: 'Heatmap', value: 'heatmap' }, | ||
| ]} | ||
| /> | ||
| )} | ||
| {shouldShowLiveModeHint && | ||
| denoiseResults != true && ( | ||
| <ResumeLiveTailButton | ||
|
|
@@ -2632,26 +2702,50 @@ export function DBSearchPage() { | |
| </Group> | ||
| </Group> | ||
| </Box> | ||
| {!hasQueryError && ( | ||
| <Box | ||
| className={searchPageStyles.timeChartContainer} | ||
| mih="0" | ||
| > | ||
| <DBTimeChart | ||
| sourceId={searchedConfig.source ?? undefined} | ||
| showLegend={false} | ||
| config={histogramTimeChartConfig} | ||
| enabled={isReady} | ||
| showDisplaySwitcher={false} | ||
| showMVOptimizationIndicator={false} | ||
| showDateRangeIndicator={false} | ||
| queryKeyPrefix={QUERY_KEY_PREFIX} | ||
| onTimeRangeSelect={handleTimeRangeSelect} | ||
| onFocusSeries={handleFocusSeries} | ||
| enableParallelQueries | ||
| /> | ||
| </Box> | ||
| )} | ||
| {!hasQueryError && | ||
| (traceRedMetricsSource != null ? ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major — Trace searches now run the status-grouped count histogram with nothing rendering it, plus a second ungrouped count for Throughput
Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond |
||
| <Box | ||
| className={searchPageStyles.timeChartContainer} | ||
| mih="0" | ||
| h={240} | ||
| > | ||
| <TraceRedMetricsChart | ||
| mode={traceChartMode} | ||
| histogramTimeChartConfig={ | ||
| histogramTimeChartConfig | ||
| } | ||
| heatmapChartConfig={{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor —
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| ...chartConfig, | ||
| dateRange: searchedTimeRange, | ||
| with: aliasWith, | ||
| }} | ||
| source={traceRedMetricsSource} | ||
| isReady={isReady} | ||
| queryKeyPrefix={RED_QUERY_KEY_PREFIX} | ||
| onTimeRangeSelect={handleTimeRangeSelect} | ||
| onFocusSeries={handleFocusSeries} | ||
| /> | ||
| </Box> | ||
| ) : ( | ||
| <Box | ||
| className={searchPageStyles.timeChartContainer} | ||
| mih="0" | ||
| > | ||
| <DBTimeChart | ||
| sourceId={searchedConfig.source ?? undefined} | ||
| showLegend={false} | ||
| config={histogramTimeChartConfig} | ||
| enabled={isReady} | ||
| showDisplaySwitcher={false} | ||
| showMVOptimizationIndicator={false} | ||
| showDateRangeIndicator={false} | ||
| queryKeyPrefix={QUERY_KEY_PREFIX} | ||
| onTimeRangeSelect={handleTimeRangeSelect} | ||
| onFocusSeries={handleFocusSeries} | ||
| enableParallelQueries | ||
| /> | ||
| </Box> | ||
| ))} | ||
| </> | ||
| )} | ||
| {hasQueryError && queryError ? ( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 minor — seriesColors adds a second per-series color mechanism alongside select[i].color
ChartConfigSchemaalready carriescolor: ChartPaletteTokenSchema.optional()on each select item (common-utils types.ts:416), consumed positionally againstmeta[i].namein DBTableChart.tsx:149-175, and types.ts:1502-1503 states line/bar/pie per-series colors are meant to ship through that same field. Keying a side map ondisplayNameinstead means the pin lives apart from the alias that defines it — the hazard traceRedMetrics.ts:146-151 documents rather than removes. Resolve pins fromselect[i].colorinformatResponseForTimeChart(same positional meta mappinguseChartNumberFormatsalready uses) and setcolor: 'chart-cyan'on the p95 select entry instead of adding theseriesColorsprop.