Skip to content

feat(app): add a duration heatmap view to trace search - #3184

Open
alex-fedotyev wants to merge 7 commits into
alex/red-metrics-trace-search-v2from
alex/trace-search-duration-heatmap
Open

alex-fedotyev wants to merge 7 commits into
alex/red-metrics-trace-search-v2from
alex/trace-search-duration-heatmap

Conversation

@alex-fedotyev

@alex-fedotyev alex-fedotyev commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Builds on the RED metrics tiles by adding a RED/Heatmap switch to the trace search results view. Heatmap replaces the three tiles with a duration-distribution heatmap over time, the same tile the dashboard renders (DBHeatmapChart + toHeatmapChartConfig), without the significant-fields comparison panel.

The choice lives in the URL as traceChartMode, like the other view toggles on this page, so a reload or a shared link reopens the same view. It resets to RED when the source actually changes, so returning to a trace source after viewing another one does not silently reopen in Heatmap; the guard uses a sentinel ref so the async source resolution on first load does not count as a change and a shared ?traceChartMode=heatmap link survives.

DBHeatmapChart picks up the queryKeyPrefix prop DBTimeChart already had. The RED tiles namespace their queries so the slow quantile scan stays out of the reported search latency while live tail still waits on them; without the prop the heatmap kept its own keys and slipped past that gate, so a live tick could fire a fresh pair of heatmap queries without waiting for the previous one. Both heatmap queries now carry the prefix.

The heatmap value expression comes from getDurationMsExpression, which applies the source's durationPrecision, paired with the DURATION_HEATMAP_NUMBER_FORMAT constant added in #3182. Value, count and scale are fixed here rather than read from the delta view's settings drawer, so this stays a plain duration heatmap independent of that view's query state; the config carries an @source note pointing at the delta-view tile it mirrors.

Test plan

  • Full app suite green (246 files, 4011 tests), plus tsc --noEmit, eslint with no new warnings, and the production build:clickhouse.
  • Verified on a preview build against the demo traces dataset: switching to Heatmap renders the duration distribution, the mode survives a reload, switching sources returns to RED, and switching back shows the three tiles unchanged.

Notes

@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Sep 24, 2026 3:18am UTC
hyperdx-storybook Ready Ready Preview Sep 24, 2026 3:18am UTC

Request Review

@changeset-bot

changeset-bot Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 46b3d9e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no outstanding correctness or repository-rule violations identified.

Summary

Adds a URL-backed RED/Heatmap switch to trace search and renders duration distributions using the shared dashboard heatmap implementation.

  • Resets the selected chart mode when the active source changes while preserving shared heatmap links on initial load.
  • Pauses live tail while the heatmap is visible and namespaces both heatmap queries with the search chart prefix.
  • Centralizes duration-heatmap formatting and separates span-status error predicates from log-severity handling.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Trace search results] --> B{traceChartMode}
    B -->|RED| C[Throughput, errors, duration]
    B -->|Heatmap| D[Duration heatmap]
    D --> E[Min/max query]
    E --> F[Bucket query]
    C --> G[Shared query-key prefix]
    D --> G
    G --> H[Search fetching gate]
    H --> I[Live-tail coordination]
Loading

Reviews (4) · Last reviewed commit: "fix(app): pin live tail off while the tr..."

</IsolatedChartSyncProvider>
) : (
<ChartCard style={{ height: '100%', minHeight: 0 }}>
<DBHeatmapChart

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major — Heatmap mode blanks to "Loading…" and recreates the uPlot chart on every live-tail tick

Results mode leaves isLive at its default true (DBSearchPage.tsx:1095; the force-off effect at :1100 only covers delta/pattern, which is why the delta-view heatmap never hit this). Every DEFAULT_REFRESH_FREQUENCY = 10s tick calls refresh() with new Date(Date.now() - interval), new Date() (DBSearchPage.tsx:747), so searchedTimeRange — and therefore heatmapChartConfig.dateRange — is a brand-new absolute range each tick. That produces new minMaxConfig/bucketConfig query keys (DBHeatmapChart.tsx:770,794), and unlike DBTimeChart (which passes placeholderData: prev => prev, DBTimeChart.tsx:509) the heatmap queries have no placeholder, so isMinMaxLoading is true and HeatmapContainer replaces the whole chart with the Loading... text (DBHeatmapChart.tsx:860). key={JSON.stringify(config)} (DBHeatmapChart.tsx:873) then destroys and recreates the uPlot instance, and the recomputed min/max makes the y-axis jump. Fix: add placeholderData: (prev: any) => prev to both useQueriedChartConfig calls in HeatmapContainer (mirroring DBTimeChart), or force live tail off while traceChartMode === 'heatmap' the way the analysisMode effect at DBSearchPage.tsx:1100 does.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 46b3d9e. I took the second option: results mode now pins live tail off while the heatmap is the visible chart, matching what delta and pattern modes already do (which is why the delta-view heatmap never hit this). placeholderData alone would stop the Loading flash but not the teardown, since key={JSON.stringify(config)} still changes when the tick moves the date range.

// (undefined -> id) doesn't count as a change and a shared link that pins
// ?traceChartMode=heatmap survives.
const prevSourceIdRef = useRef<string | undefined>(undefined);
useEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Reset effect discards the user's Heatmap choice even when the new source can render it

The effect fires on any searchedSource?.id change, so switching from trace source A to trace source B — both with a durationExpression, so both render the toggle via traceRedMetricsSource — silently flips the view back to RED. The sibling view toggle analysisMode (:1067) is deliberately not reset on source change even though delta needs a trace source too; that branch just renders nothing (:2637). Drop the effect (both the switch and the chart are already hidden when traceRedMetricsSource == null, so a lingering ?traceChartMode=heatmap is inert), or gate the reset on the new source not supporting the heatmap.

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

histogramTimeChartConfig={
histogramTimeChartConfig
}
heatmapChartConfig={{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — heatmapChartConfig is a fresh object literal every render, so the tile-config memo never memoizes

The inline {...chartConfig, dateRange: searchedTimeRange, with: aliasWith} gets a new identity on every render, which is the single dep of useMemo in TraceRedMetricsChart.tsx:121-135 — so toHeatmapChartConfig and both buildHeatmap*Config calls rerun each render, exactly the failure the sibling durationFormat memo comment at TraceRedMetricsChart.tsx:86 warns about. The page already memoizes the near-identical shape as filtersChartConfig (:2035, which adds only orderBy: undefined) and histogramTimeChartConfig (:1926). Hoist one useMemo for {...chartConfig, dateRange: searchedTimeRange, with: aliasWith} and pass it to both this callsite and the DBSearchHeatmapChart one at :2641.

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

} as const;

return (
return mode === 'red' ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — No test covers the mode branch, the heatmap tile config, or the new query-key prefix

DBSearchPageQueryKey.test.tsx exists specifically to pin the queryKeyPrefix contract for DBTimeChart/SearchTotalCountChart, but nothing asserts that the heatmap's two keys now carry it — and DBSearchHeatmapChart.test.tsx:55 mocks DBHeatmapChart and toHeatmapChartConfig wholesale, so it cannot catch a regression there either. Add (a) an assertion in DBSearchPageQueryKey.test.tsx that both heatmap queryKeys start with the passed prefix, and (b) a TraceRedMetricsChart test that mode="heatmap" renders the heatmap (not the three tiles) with valueExpression from getDurationMsExpression and numberFormat === DURATION_HEATMAP_NUMBER_FORMAT, since the ms-expression/format pairing is the one thing that silently produces wrong axis units if it drifts.

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

* are fixed here rather than read from the delta view's settings drawer, so
* this stays a plain duration heatmap independent of that view's query state.
*
* @source packages/app/src/components/Search/DBSearchHeatmapChart.tsx (tile config)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — @source tag used for a same-package partial copy raises the advisory ratchet count above baseline

The conventions reserve @source packages/... for "when an entire file has to be copied across packages"; this is a same-package, partial mirror of one config block. scripts/ci/ratchet.mjs:24 counts /@source packages\//g and the app baseline is 0 (scripts/ci/ratchet-baseline.json:12), so every CI run will now print ! app/@source: 1 > baseline 0 (advisory, non-fatal). Replace it with a plain doc comment naming the twin — which is what the DRY table's "Two genuinely must coexist" row actually asks for — or run yarn ratchet:update to re-baseline deliberately.

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

@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

4 finding(s): 🔴 0 critical · 🟠 1 major · 🔵 3 minor

2 posted as inline comment(s) on the changed lines. 2 listed below.

Findings outside the changed lines

2 minor
  • 🔵 packages/app/src/components/Search/TraceRedMetricsChart.tsx:314 — Duration heatmap tile config is copied from DBSearchHeatmapChart and tagged @source → This toHeatmapChartConfig({...cfg, select:[{valueExpression: getDurationMsExpression(source), countExpression:'count()', heatmapScaleType:'log'}], numberFormat: DURATION_HEATMAP_NUMBER_FORMAT}) repeats the default-settings tile in components/Search/DBSearchHeatmapChart.tsx:145-158. @source is meant for whole-file ports across packages, and this one raises the advisory count in scripts/ci/ratchet.mjs. Fix: extract a shared helper such as durationHeatmapConfig(chartConfig, source) next to getDurationMsExpression in source.ts or in ./traceRedMetrics, use it in both places, and drop the @source tag.
  • 🔵 packages/app/src/components/Search/TraceRedMetricsChart.tsx:337 — No tests for the mode switch, the source-change reset, or the live-tail pin → None of the new behaviour has a test: mode choosing RED tiles or heatmap, the prevSourceIdRef reset (it should keep ?traceChartMode=heatmap on the first undefined→id resolution and reset to red on id→other id), or the isHeatmapChartVisible live-tail pin. Add a TraceRedMetricsChart render test for both modes, including that queryKeyPrefix reaches the heatmap query keys. Add DBSearchPage or hook-level tests for the reset guard and the pin.

Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

No P0/P1 ship-blockers. The core mechanics verify cleanly: the prevSourceIdRef sentinel correctly preserves a shared ?traceChartMode=heatmap link through first-load async source resolution while resetting on real source changes; the heatmap branch and toggle are properly gated on traceRedMetricsSource != null, so a stale heatmap URL on a log source neither renders nor disables live tail; the queryKeyPrefix threading reaches both heatmap queries and the exported DBHeatmapChart (default export wraps HeatmapContainer via dynamic); and the prior review comment about live-tail teardown is addressed by the isHeatmapChartVisible pin. The recommendations below are quality and coverage improvements.

🟡 P2 — recommended

  • packages/app/src/DBSearchPage.tsx:2704 — While the heatmap is visible, isLive is pinned false, so shouldShowLiveModeHint turns true and renders ResumeLiveTailButton; clicking it sets isLive=true but the pin effect's deps ([isHeatmapChartVisible, setIsLive]) don't change, so it never re-pins and each live tick tears down and rebuilds the heatmap uPlot — the exact race this PR set out to prevent.
    • Fix: Hide ResumeLiveTailButton while isHeatmapChartVisible, or add isLive to the pin effect deps so a resume is immediately undone while the heatmap is shown.
    • correctness, julik-frontend-races
  • packages/app/src/DBSearchPage.tsx:1916 — The new traceChartMode behaviors have no unit coverage: the reset-on-source-change effect (including the first-load undefined -> id skip and shared-link survival) and the live-tail pin effect are asserted only by inline comments, and e2e is deferred to a separate issue.
    • Fix: Add a rendered-page test asserting the reset-to-red on a real source change while preserving ?traceChartMode=heatmap across first async resolution, and that live tail is forced off only when the heatmap is the visible chart.
    • testing, project-standards, kieran-typescript
🔵 P3 nitpicks (3)
  • packages/app/src/DBSearchPage.tsx:2736 — heatmapChartConfig is passed as a fresh object literal every render (unlike the memoized sibling histogramTimeChartConfig), so the toHeatmapChartConfig useMemo in TraceRedMetricsChart recomputes on every parent render, including RED mode; impact is limited to wasted object construction since React Query hashes keys by value.
    • Fix: Wrap the object in useMemo keyed on [chartConfig, searchedTimeRange, aliasWith], matching the sibling config's memoization discipline.
  • packages/app/src/DBSearchPage.tsx:1939 — Switching Heatmap back to RED (or switching source while in heatmap) leaves isLive false with no auto-restore, so a live-by-default results view sits frozen until the user manually resumes; consistent with the delta/pattern convention but those are non-live views.
    • Fix: Confirm the latch is intended, or restore the prior isLive on heatmap exit rather than only ever calling setIsLive(false).
  • packages/app/src/DBSearchPage.tsx:1917 — The reset effect fires on any id -> undefined transition, so if useSources() data is reset/removed (not a normal keepPreviousData refetch) the source momentarily resolves to undefined and drops a heatmap view mid-session on an unchanged source.
    • Fix: Only reset when the new id is itself defined, e.g. prev !== next && next !== undefined.

Reviewers (11): correctness, testing, maintainability, project-standards, kieran-typescript, julik-frontend-races, performance, adversarial, previous-comments, agent-native, learnings-researcher.

Testing gaps: no test asserts the heatmap queries carry RED_QUERY_KEY_PREFIX (so they participate in the isRedQueryFetching live-tail gate); no test covers the heatmap's two queries staying dormant in RED mode; the getDurationMsExpression ↔ DURATION_HEATMAP_NUMBER_FORMAT factor pairing has no guard if either side changes.

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

The service dashboard and the LLM expressions each spelled out their own
"is this an error" predicate as lower(<status>) = 'error'. Collectors that
emit the long OTel form (STATUS_CODE_ERROR) therefore fell outside it, so
error rates and error counts on those dashboards undercounted.

Pull the predicate into one exported helper in source.ts and match both
forms. The logs path passes a severity-text expression, which never takes
the long form, so it is unaffected.
prevSourceIdRef.current !== undefined &&
prevSourceIdRef.current !== searchedSource?.id
) {
setTraceChartMode('red');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Every source switch adds ?traceChartMode=red to the URL, even for log sources

The app uses nuqs 1.17.0, where clearOnDefault is off by default. setTraceChartMode('red') therefore writes the default value into the URL each time the source changes, including sources that have no RED view. Call setTraceChartMode(null) to clear the param instead.

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

histogramTimeChartConfig={
histogramTimeChartConfig
}
heatmapChartConfig={{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — The inline heatmapChartConfig object makes the heatmap useMemo recompute on every render

heatmapChartConfig={{...chartConfig, dateRange, with: aliasWith}} creates a new object on each DBSearchPage render. TraceRedMetricsChart lists it as a dependency of the toHeatmapChartConfig memo, so the memo never reuses its result, even while in RED mode. Memoize the config in DBSearchPage with useMemo over chartConfig, searchedTimeRange and aliasWith, or build it inside TraceRedMetricsChart from its inputs.

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

// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — New mode-switch and reset logic has no tests

No test renders TraceRedMetricsChart in either mode. None checks that the heatmap queries carry queryKeyPrefix, which is what lets live tail wait on them, and none covers the source-change reset effect (a first-load undefined→id transition must keep heatmap; a change from one id to another must reset). Add component tests, for example next to components/Search/__tests__/traceRedMetrics.test.ts, that assert the rendered chart and query keys for each mode, plus a hook-level test for the reset effect.

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

// can't disable live tail for a search that shows the histogram.
const isHeatmapChartVisible =
analysisMode === 'results' &&
traceRedMetricsSource != null &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major — Resume live tail button undoes the heatmap's live-tail pin

The effect only calls setIsLive(false) when isHeatmapChartVisible changes. Once it runs, shouldShowLiveModeHint becomes true and ResumeLiveTailButton shows up right next to the new switch (line ~2704). Clicking it, or picking a live range in the time picker (setIsLive(true) at ~2125), turns live back on while the heatmap is still visible, and the effect doesn't run again. Each tick then rebuilds the uPlot instance, which is the problem the comment says this prevents. Fix: hide the resume button and the live option while isHeatmapChartVisible, or leave isLive alone and gate useLiveUpdate's pause on isHeatmapChartVisible.

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

// can't disable live tail for a search that shows the histogram.
const isHeatmapChartVisible =
analysisMode === 'results' &&
traceRedMetricsSource != null &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Switching back to RED leaves live tail off

The pin writes isLive=false into the URL, and nothing restores it. A user who looks at the heatmap for a moment and switches back to RED is left on a frozen time range with live tail off. Pausing live updates through useLiveUpdate's pause while the heatmap is visible, instead of overwriting isLive, would bring live tail back when the user switches to RED.

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

@@ -90,6 +109,30 @@ export function TraceRedMetricsChart({
() => durationConfig(base, durationExpression, durationFormat),
[base, durationExpression, durationFormat],
);
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Duration heatmap tile config is copied from DBSearchHeatmapChart and marked with @source

This toHeatmapChartConfig({...cfg, select:[{valueExpression: getDurationMsExpression(source), countExpression:'count()', heatmapScaleType:'log'}], numberFormat: DURATION_HEATMAP_NUMBER_FORMAT}) repeats the defaults in DBSearchHeatmapChart.tsx:52-54,145-158. The conventions reserve @source for whole-file ports across packages, and scripts/ci/ratchet.mjs counts the tag as licensed duplication. For logic shared by two components in the same package, the conventions say to put it in a sibling module. Fix: add a helper, e.g. durationHeatmapConfig(cfg, source) next to getDurationMsExpression in source.ts or in traceRedMetrics.ts, have both callers use it, and remove the @source tag.

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

@@ -167,5 +210,16 @@ export function TraceRedMetricsChart({
</ChartCard>
</Flex>
</IsolatedChartSyncProvider>
) : (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — No tests for the heatmap mode or the source-change reset

The PR adds a mode branch, the heatmap config wiring and the prefixed query keys to TraceRedMetricsChart, plus the ref-guarded source-change reset and the live-tail pin in DBSearchPage. None of it has a unit test. Add tests for: mode='heatmap' renders DBHeatmapChart with queryKeyPrefix and a getDurationMsExpression value; undefined→id keeps heatmap; id A→id B resets to red.

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

The helper was also applied to the log-severity expression, where the legacy
status value it matches can never appear and the values severity text does use
for errors (fatal, crit and friends) are a different set. Mixing the two
vocabularies in one predicate made it wrong in both directions on the log path.

Rename it to errorStatusPredicateSql, document it as span-status only, and let
the log caller keep its own predicate, unchanged from before this branch.
Trace search showed a single count histogram above the results. For trace
sources with a duration column it now shows Throughput, Errors and Duration
side by side, under a shared sync scope so hovering one moves the cursor on
all three.

Each chart is built from the same base config the histogram uses, in a pure
module so the aggregations can be unit tested without rendering. Every
aggregation is over a raw column so materialized views can satisfy it; the
error ratio and the duration unit are applied at display instead.

Logs and other sources keep the existing histogram.
Picks up errorStatusPredicateSql from the base branch.
The significant-fields heatmap built { output: 'duration', factor: 0.001 }
inline. The factor is only correct next to getDurationMsExpression, which
applies the source's durationPrecision and leaves the value in ms, so give
it a name in source.ts and document that pairing.

No behavior change.
Adds a RED/Heatmap switch to the trace results chart area. Heatmap renders
the duration distribution over time using the same tile the dashboard does.
The mode is URL state so a reload or shared link reopens the same view, and
it resets to RED when the source changes.

DBHeatmapChart takes the queryKeyPrefix prop DBTimeChart already had, so the
heatmap queries sit in the same namespace as the RED tiles and live tail
waits on them instead of stacking a fresh pair each tick.
The heatmap keys its uPlot instance on the chart config, so a live-tail tick
(which moves the absolute time range) tore the chart down and rebuilt it with
a recomputed y-axis every 10s, blanking it to Loading in between.

Delta and pattern modes already pin live tail off, which is why the delta-view
heatmap never hit this. Results mode defaults to live, so the heatmap view
opts out for itself while it is the rendered chart.
const isHeatmapChartVisible =
analysisMode === 'results' &&
traceRedMetricsSource != null &&
traceChartMode === 'heatmap';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major — Resume Live Tail gets around the heatmap live-tail pin

The pin effect only runs when isHeatmapChartVisible changes. Once live has been pinned off, shouldShowLiveModeHint renders ResumeLiveTailButton right next to the Heatmap switch (line ~2706). Clicking it, or picking a live interval (the setIsLive(true) path at ~2125), turns live back on and the effect never runs again. The heatmap then tears down and rebuilds on every tick, because it keys on JSON.stringify(config) in DBHeatmapChart.tsx:873, which this effect was meant to prevent. Fix: hide or disable ResumeLiveTailButton and the live interval option while isHeatmapChartVisible. Alternatively, have the live-enable handlers leave the heatmap first (for example setTraceChartMode('red')), or pass isLive && !isHeatmapChartVisible to useLiveUpdate.

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

histogramTimeChartConfig={
histogramTimeChartConfig
}
heatmapChartConfig={{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Inline heatmapChartConfig literal means the heatmap useMemo never reuses its result

heatmapChartConfig={{...chartConfig, dateRange: searchedTimeRange, with: aliasWith}} creates a new object on every DBSearchPage render. It is a dependency of the toHeatmapChartConfig useMemo in TraceRedMetricsChart.tsx:314, so that memo recomputes every render and does no work. Fix: build the config once with useMemo in DBSearchPage, keyed on chartConfig, searchedTimeRange and aliasWith, and pass the memoized value. Otherwise drop the useMemo.

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

kodiakhq Bot pushed a commit that referenced this pull request Sep 25, 2026
…3221)

## Summary

Fixes the heatmap part of #3205: during a dashboard refresh, heatmap tiles now keep the current heatmap on screen and pulse, instead of replacing it with "Loading...".

- Both heatmap queries (the min/max bounds and the buckets) keep their previous result as placeholder data, the same way the time chart and the number, bar and pie tiles do.
- The bucket query waits for the refreshed bounds before it runs, so a refresh doesn't also query the new range bucketed with the previous range's min/max.
- The heatmap, and the "Not enough data points" message, pulse while either query is still showing previous data. `Heatmap` takes an optional `className` so the pulse goes on its existing container rather than a new wrapper, which would affect its size measurement.
- New tests in `DBHeatmapChart.render.test.tsx`: both queries keep their previous data, the tile pulses during a refresh and stops once fresh data loads, and the bucket query waits for fresh bounds. Three of the four fail without the change.

While checking the rest of the app for the same pattern, I found two more places: the Kubernetes dashboard's Pods, Nodes and Namespaces tables still switch to skeleton rows on every refresh, and the Services dashboard tiles (`DBListBarChart`, `DBHistogramChart`, the slowest events tile) keep their data but don't pulse. Happy to take those as follow-ups if you'd like.

### How to test on Vercel preview

**Preview routes:** /dashboards

**Steps:**

1. Create a dashboard with a Heatmap tile on the demo traces source (for example on `Duration`) and a Line time chart.
2. Choose a large time range so each query takes a moment.
3. Click "Refresh dashboard".
4. Verify the heatmap stays on screen and pulses while its queries run, like the line chart, and stops pulsing once the new heatmap appears.

### References

- Linear Issue: N/A
- Related PRs: #3211, #3212. #3184 also edits the `queryKey` of these two heatmap queries, so whichever lands second needs a small rebase.
- Closes part of #3205

Checks run locally: `jest` for the heatmap suites and `ChartPreviewPanel` (50 passed), `eslint` on changed files (0 errors, no new warnings), `prettier --check`, and `tsc --noEmit` for `packages/app`.
@alex-fedotyev
alex-fedotyev force-pushed the alex/red-metrics-trace-search-v2 branch from 3c58c91 to 2c78a82 Compare November 13, 2026 23:40
@alex-fedotyev
alex-fedotyev requested a review from a team as a code owner November 13, 2026 23:40

This branch was successfully deployed

2 active deployments
Preview – hyperdx-oss — 46b3d9ec Deployed Sep 24, 2026 by vercel[bot]
Preview – hyperdx-storybook — 46b3d9ec Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant