Skip to content

feat(app): show RED metrics on the trace search results view - #3183

Open
alex-fedotyev wants to merge 6 commits into
mainfrom
alex/red-metrics-trace-search-v2
Open

alex-fedotyev wants to merge 6 commits into
mainfrom
alex/red-metrics-trace-search-v2

Conversation

@alex-fedotyev

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

Copy link
Copy Markdown
Contributor

Summary

Trace search shows a single count histogram above the results. For trace sources that expose a duration column, this replaces it with RED metrics:

  • Throughput is the status-grouped span count (bars). It renders the same count the histogram did and reuses that query, so the results-count header and this tile share a single scan, and clicking a status series filters the results.
  • Errors toggles between rate (countIf(error) / count(), rendered as a percent line) and volume (countIf(error), bars, grouped by status).
  • Duration shows Avg, P95, and P99 over the source's raw duration column (line), with the unit applied at display from the source's durationPrecision.

Every aggregation is over a raw column (count, countIf, quantile/avg of the duration expression), so AggregatingMergeTree materialized views can satisfy the queries; the ratio and the unit conversion happen at the display layer or in a post-aggregation column.

The three charts are DBTimeCharts under a shared sync scope, so hovering one shows a synced cursor on all three at the same timestamp, and each renders as a dashboard-tile card. Clicking a status series on the Throughput or Errors-volume chart filters the results, which is the drill-down the histogram offered. Logs and other sources keep the existing histogram, as do trace sources on a compact (narrow/mobile) viewport, where three side-by-side tiles would not fit.

The per-chart aggregations for Errors and Duration live in a small pure module, traceRedMetrics.ts, built from the same base config the histogram uses, so they honor the active WHERE filter and time range and can be unit tested without rendering.

Two supporting props on shared chart components, each with its only consumer in this PR:

  • yAxisMaxDomain, so the Errors rate tile caps at 100% instead of auto-scaling a near-zero series to a nonsense range. The cap is a numeric domain, so the axis keeps clean, de-duplicated tick labels and never collapses to a single point.
  • compactXAxisLabels, so the edge time labels do not clip on tiles this narrow.

The Duration percentiles are pinned to cool hues (Avg blue, P95 cyan, P99 purple) so latency never borrows the red the Errors line uses, and each percentile keeps its color across sessions regardless of how the data sorts the lines. The colors are set on each series' select-item color (the schema's existing per-series color field), which setLineColors now reads by result-column name, so there is a single source of truth and no separate color map to keep in sync.

The Errors and Duration tiles are auxiliary to the results query, so they run under their own query namespace: that keeps the slow quantile scan out of the reported search latency, while live tail still waits on them before advancing the window, so a tick cannot stack a second set of chart queries on the first. Throughput shares the results-count query instead, so it adds no extra scan.

Screenshots

Light theme:

RED metrics, light

Dark theme:

RED metrics, dark

Note: screenshots predate the latest revision; the Throughput tile is now status-grouped (shares the histogram's query). I will refresh these before merge.

Test plan

  • Unit tests for the aggregation builders (traceRedMetrics.test.ts) and the chart color pipeline (ChartUtils.test.ts), including that a select-item color overrides the positional palette and does not consume a positional slot.
  • A Duration test pins each percentile to a stable, non-reserved hue (Avg/P95/P99 to blue/cyan/purple), so latency never falls back to the reserved chart-red slot. Because the color now lives on the select item, it cannot drift from the series it colors.
  • computeYAxisBounds coverage for the capped rate axis: a flat/zero series shows [0, cap] with clean ticks, the cap holds under selection (no [0, 0] collapse) and over reference lines, and is skipped for stacked charts.
  • Full app unit suite green, plus tsc --noEmit, eslint with no new warnings, and knip. CI covers the full suite and the production build:clickhouse bundle.
  • The tiles were verified against the demo traces dataset in both themes: a synced hover cursor across the three tiles, the Duration lines blue, cyan and purple with no red, a working Rate/Volume toggle, and a Logs source still showing the single histogram.

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 Nov 14, 2026 2:02am UTC
hyperdx-storybook Ready Ready Preview Nov 14, 2026 2:02am UTC

Request Review

@changeset-bot

changeset-bot Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ec9cb49

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; no actionable new issue was found in the changes since the previous review.

Summary

The PR replaces the trace-search count histogram with synchronized Throughput, Errors, and Duration charts while retaining the existing histogram for other sources.

  • Builds RED aggregations from the active search configuration and source expressions.
  • Adds stable per-series colors, compact time-axis labels, and capped percentage axes to shared chart components.
  • Separates auxiliary chart queries from search-latency telemetry while coordinating them with live-tail refresh.
  • Centralizes the trace status error predicate and distinguishes it from log-severity handling.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Search[Trace search state] --> Base[Shared filtered chart config]
    Base --> Throughput[Throughput: count spans]
    Base --> Errors[Errors: rate or volume]
    Base --> Duration[Duration: avg, p95, p99]
    Throughput --> Sync[Shared hover synchronization]
    Errors --> Sync
    Duration --> Sync
    Errors -->|Volume series click| Filter[Apply status filter to results]
    REDQueries[RED query namespace] --> LiveTail[Pause live-tail window advancement]
Loading

Reviews (4) · Last reviewed commit: "refactor(app): follow the error predicat..."

@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

Scope: c220e164 → HEAD · 6 production files + 3 test files (packages/app) · mode: report-only
Intent: Replace the single count histogram above trace search results with RED metrics (Throughput / Errors / Duration) tiles for trace sources that expose a duration column; keep the histogram for logs, compact viewports, and custom group-by.

✅ No critical issues found. No P0/P1: security confirmed the SQL expressions are trusted source config used identically across the codebase (no new injection surface), all 11 prior-review fixes landed, and there is no happy-path crash or data loss. P2 advice below.

🟡 P2 — recommended

  • packages/app/src/components/Search/traceRedMetrics.ts:152 — durationConfig (and the rate errorsConfig) spread the histogram base whose fillNulls default zero-fills empty time buckets, so on a sparse-traffic source the Avg/P95/P99 lines drop to 0ms (and the error rate to 0%) in every traffic-free bucket, making latency look like it repeatedly crashes to zero.
    • Fix: Set fillNulls: false on durationConfig (and the rate branch of errorsConfig) so quiet buckets render as gaps rather than false zeros.
    • adversarial-reviewer, correctness-reviewer
  • packages/app/src/DBSearchPage.tsx:1792 — the live-tail pause now ANDs in isRedQueryFetching, so each ~10s tick blocks on the slowest auxiliary RED tile; a consistently slow p95/p99 quantile scan throttles the primary results refresh below the configured interval (bounded, self-regulating, cannot deadlock).
    • Fix: Confirm this cadence coupling is acceptable, or gate live tail only on the results/throughput prefix and cancel in-flight RED queries at tick time instead of blocking the tick.
    • julik-frontend-races, adversarial-reviewer, correctness-reviewer
  • packages/app/src/components/Search/TraceRedMetricsChart.tsx:1 — the new component has no test file, leaving its non-trivial wiring uncovered: the errorsMode toggle (conditional yAxisMaxDomain/onFocusSeries/hiddenSeries and key-based remount), the Errors-tile drop when errorsConfig returns undefined, and the throughputGrouped legend branch.
    • Fix: Add a render/interaction test for the rate↔volume toggle, the omitted Errors tile, and legend visibility, or extract throughputGrouped to a unit-tested pure helper.
    • testing-reviewer, project-standards-reviewer
  • packages/app/src/components/DBTimeChart.tsx:647 — the seriesColors useMemo that extracts {alias: color} from config.select is the untested bridge that makes duration color-pinning work end-to-end; if the guard or key field regresses, duration series silently fall back to the positional palette (chart-red at slot 2) with all existing tests still green.
    • Fix: Unit-test the derivation (mixed select items, non-builder config → undefined, no-color select → undefined), ideally covering the alias === valueColumnName coupling that setLineColors relies on.
    • testing-reviewer, kieran-typescript-reviewer
🔵 P3 nitpicks (9)
  • packages/app/src/components/Search/TraceRedMetricsChart.tsx:150 — toggling Rate/Vol remounts the Errors chart via key={errorsMode}, which leaves the old mount's query in flight (react-query v5 does not cancel on unmount) and shows a loading flash since placeholderData does not carry across a new query key.
    • Fix: Drive the display type via a prop so the chart stays mounted and switches line↔bar in place, or accept and document the transient flash/double-fetch.
  • packages/app/src/DBSearchPage.tsx:1726 — when RED tiles unmount (logs source / compact / custom group-by) with a RED query mid-flight, the uncancelled fetch keeps isRedQueryFetching > 0, transiently pausing live tail on a view that no longer shows the tiles, contradicting the comment's "resolves to 0 when the RED tiles aren't mounted."
    • Fix: Wire context.signal cancellation on unmount for the RED charts, or scope the gate to mounted queries.
  • packages/app/src/components/Search/TraceRedMetricsChart.tsx:118 — throughputGrouped is a fourth re-implementation of "does this config have a group by?" (alongside extractGroupColumns, hasGroupBy, isCustomSearchGroupBy) with divergent empty-string handling, so the predicates can silently disagree on edge inputs.
    • Fix: Route all callers through one exported predicate.
  • packages/app/src/HDXMultiSeriesTimeChart.tsx:1341 — the cappedYAxisUpperBound doc says "Recharts calls this with the data's max value," but it is a plain helper invoked once inside computeYAxisBounds; Recharts never calls it.
    • Fix: Reword the comment to the actual contract and drop the Recharts claim.
  • packages/app/src/DBSearchPage.tsx:2903 — the RED-vs-histogram branch is a ~55-line inline JSX ternary, and the histogram DBTimeChart chrome props are now duplicated between the fallback Box and the Throughput tile, so the two can drift.
    • Fix: Lift each branch into a named element and share the common histogram chrome props via one object.
  • packages/app/src/HDXMultiSeriesTimeChart.tsx:32 — XAxisTickContentProps is deep-imported from recharts/types/util/types, an internal path that can break on a minor version bump, and renderCompactXTick reads payload.value with no guard.
    • Fix: Define a minimal local prop interface for the tick callback instead of the internal import.
  • packages/app/src/components/Search/TraceRedMetricsChart.tsx:109 — the "single shared scan" comment overstates the effect: the standalone histogram is in the else-branch and not mounted alongside the tiles, and the Throughput options object (enableParallelQueries: true) does not match the results-count query's options, so a single shared scan with the results header is not achieved.
    • Fix: Soften the comment to describe the actual (cache-key) behavior, or align the enableParallelQueries flag on both sides if true dedupe is intended.
  • packages/app/src/components/Search/TraceRedMetricsChart.tsx:77 — source.durationExpression ?? '' is unreachable dead-defense (the parent only mounts the component when durationExpression is truthy), and the component has no self-guard, so a future caller would build avg('')/quantile('') SQL instead of hiding the tile.
    • Fix: Have the component omit the Duration tile when durationExpression is empty, mirroring errorsConfig's undefined contract.
  • packages/app/src/DBSearchPage.tsx:2907 — the traceRedMetricsSource narrowing and the four-condition RED-vs-histogram ternary (!isCompact, !isCustomGroupBy, trace source, duration present) have no test despite an existing page test harness.
    • Fix: Add tests (or extract the predicate) covering each fallback condition so exactly one chart renders per combination.

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

Testing gaps:

  • No test exercises empty-bucket rendering for the Duration/error-rate tiles (the zero-fill artifact above).
  • No test confirms the 3-select error-rate config is not collapsed by the isRatioChartConfig/divide() path (which would drop the Error rate select and unhide the helper counts).
  • No test verifies the Duration tile issues one scan for avg+p95+p99, nor that live-tail pause blocks on in-flight RED quantile queries across ticks.
  • docs/solutions/ has no app/frontend knowledge base; the setLineColors pinning, yAxisMaxDomain capping, query-key prefix sharing, and live-tail pause coordination are worth capturing after merge.

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

if (mode === 'rate') {
return {
...base,
select: [

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 — Error-rate count + countIf + post-aggregation ratio + hidden helper series is a fourth copy of an existing pattern

The repo's REQUIRED DRY rule asks for a grep on the operation before adding a helper. This exact trio already exists at ServicesDashboardPage/HttpTab.tsx:221-239 (requestErrorRateConfig: total_requests / error_requests / error_rate, ERROR_RATE_PERCENTAGE_NUMBER_FORMAT) with its matching hiddenSeries={['total_requests','error_requests']} at HttpTab.tsx:419, again inside the CTE at HttpTab.tsx:282-298, and again at llm/dashboard/OverviewCharts.tsx:152-166. Extract one builder — e.g. errorRateSelect(errorCondition, { totalAlias, errorAlias, rateAlias }) returning the select trio plus its hidden-series list — into a shared module and have all four call sites use it, rather than adding a fourth independently-aliased copy whose helper names must be kept in sync with a separate ERROR_RATE_HELPER_SERIES constant by hand.

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

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.

The shape is shared, agreed, but extracting a builder would touch HttpTab, OverviewCharts and the service-dashboard side panels, which widens this PR beyond its own files. Tracking it as a follow-up rather than folding it in here.

@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.

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

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

Findings outside the changed lines

1 minor
  • 🔵 packages/app/src/__tests__/ChartUtils.test.ts:485 — No test covers the select-item → seriesColors mapping, which is what actually colors Duration → Add a DBTimeChart test (or move the alias→color mapping into a pure helper and test that) asserting a select item's alias + color reaches formatResponseForTimeChart. The new ChartUtils tests pass seriesColors in directly. The traceRedMetrics test only checks that durationConfig puts color on the select items. If the useMemo mapping in DBTimeChart broke (wrong key, or dropped from the deps), every test would still pass and Duration would go back to the positional palette, including chart-red.

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

@alex-fedotyev
alex-fedotyev force-pushed the alex/duration-heatmap-format branch from 6bdd45c to 887421b Compare September 24, 2026 02:34
@alex-fedotyev
alex-fedotyev force-pushed the alex/red-metrics-trace-search-v2 branch from 7c3a45a to 108e06a Compare September 24, 2026 02:34
@alex-fedotyev
alex-fedotyev changed the base branch from alex/duration-heatmap-format to alex/error-span-predicate September 24, 2026 02:34
@alex-fedotyev
alex-fedotyev changed the base branch from alex/error-span-predicate to main September 24, 2026 02:34
Comment thread packages/app/src/ChartUtils.tsx Resolved
@alex-fedotyev alex-fedotyev reopened this Sep 24, 2026
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Sep 24, 2026
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 667 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 6
  • Production lines changed: 667 (+ 387 in test files, excluded from tier calculation)
  • Branch: alex/red-metrics-trace-search-v2
  • Author: alex-fedotyev

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 466 passed • 1 skipped • 1021s

Status Count
✅ Passed 466
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

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.
@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
@github-actions

Copy link
Copy Markdown
Contributor

Duplicate code analysis

No new duplicate code introduced by this PR.


jscpd compared this branch against origin/main
across app, api and common-utils. Only new clones are listed; the codebase
currently holds 108 in total (1.18%).
Run yarn dupes locally to reproduce, or yarn dupes:all for the full picture.

Comment thread packages/app/src/DBSearchPage.tsx Resolved
- Fall back to the single histogram on a compact (mobile) viewport
  instead of three unreadable side-by-side tiles.
- Compute a numeric capped y-axis domain for the rate tile so it dedupes
  tick labels and no longer collapses to [0, 0] when the sole series is
  selected; the cap now applies on the selection and fit paths too.
- Drop the unreachable 0/0 guard on the error-rate column; a returned
  GROUP BY bucket always has total_spans >= 1, so divide directly the
  way HttpTab does.
- Rename the Duration percentile series p95/p99 to P95/P99 to match the
  app's acronym casing.
- Strengthen the volume-mode test so it catches dropped sidebar filters,
  and add computeYAxisBounds coverage for the capped axis.
Comment thread packages/app/src/DBSearchPage.tsx Resolved
- Throughput now renders the status-grouped count histogram this view
  replaces, under the results-count query-key prefix, so react-query runs
  a single shared scan instead of a second ungrouped count() on every
  search and live-tail tick. This also restores the per-status drill-down
  the histogram offered.
- Duration series pin their colors through each select item's `color`
  rather than a name-keyed seriesColors prop, so there is one source of
  truth for per-series color. Drops the DBTimeChart seriesColors prop, the
  DURATION_SERIES_COLORS map, and the alias/color lockstep test;
  setLineColors reads the color off the select item by result-column name.
…ant duration format

- When a trace source has a custom group-by selected, fall back to the
  single histogram (which honors arbitrary group-by) instead of the RED
  tiles, whose grouping is fixed (Throughput and Errors by status,
  Duration ungrouped). The group-by selector now has a coherent effect
  rather than silently regrouping only part of the view.
- Drop the hand-computed duration numberFormat: DBTimeChart already infers
  it via useChartNumberFormats -> getTraceDurationNumberFormat, because
  each duration series aggregates the source's duration expression. Removes
  the getTraceDurationNumberFormat memo and durationConfig's numberFormat
  parameter.
- Compact x-axis ticks use the responsive axis tick font size instead of a
  hardcoded 11px, matching the non-compact ticks and honoring the user's
  content font scale.
- Correct the yAxisMaxDomain prop doc: the cap applies on every path
  (selection and fit included), not only the default path.
…ries

Group-by none resolves to no grouping, which collapses Throughput to a
single count() series; with the legend on, that surfaced the raw aggregate
expression ("count()") as the series label. Show the Throughput legend only
when the view is grouped (the status-code default), where it names each
status, matching how the histogram gated its own legend. The ungrouped case
keeps just the "Throughput" title.
return undefined;
}
const entries: [string, ChartPaletteToken][] = [];
for (const selectItem of config.select) {

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 — select[].color is documented as table-only, but every time chart now applies it, so existing dashboard line/bar tiles change color

Don't read config.select[].color in every DBTimeChart. Add an explicit seriesColors prop (or a separate per-series field) that only TraceRedMetricsChart passes. The schema comment at packages/common-utils/src/types.ts:459-465 says the field is used only on table tiles and other display types ignore it. The editor only shows the color control when the display type is Table (ChartEditorControls.tsx:317), and switching to Line doesn't clear it. Here's the result: a saved tile with a column color that was later switched to Line or Bar now renders in that color, and the user has no control to clear it. Because colors are keyed by valueColumnName, a grouped tile (e.g. count() grouped by ServiceName) colors every group line the same and they can't be told apart.

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

}
const entries: [string, ChartPaletteToken][] = [];
for (const selectItem of config.select) {
if (selectItem.alias && selectItem.color) {

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 — An unrecognized color token makes the whole time chart fail to render

Before adding a select item's color to seriesColors, run it through isChartPaletteToken / resolveChartPaletteToken. HDXMultiSeriesTableChart.tsx:253-259 and DBNumberChart.tsx:322 already do this. getColorFromCSSToken throws on an unknown token (utils.ts:584-585). Unknown values (stale hex, hand-edited, or forward-rolled tokens) are deliberately left as-is by migrateOrPreserveColor (dashboard.ts:78). Inside setLineColors, that throw is caught as resultFormattingError, so the whole tile shows an error instead of falling back to the default palette.

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

This branch was successfully deployed

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

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant