Skip to content

feat(app): add severity legend with counts to search histogram - #2753

Draft
MikeShi42 wants to merge 5 commits into
mainfrom
cursor/search-histogram-severity-legend-15d7
Draft

feat(app): add severity legend with counts to search histogram#2753
MikeShi42 wants to merge 5 commits into
mainfrom
cursor/search-histogram-severity-legend-15d7

Conversation

@MikeShi42

@MikeShi42 MikeShi42 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a legend below the search histogram showing each series' total across the entire selected time range, so a breakdown like "how many errors in the last 45 minutes" reads as one number instead of bars to sum by eye. Clicking an item narrows the search to that series.

It reflects the query's actual groups, not a fixed severity list

The legend derives entirely from the series the histogram query returned. It does not assume severity and is not gated on the source's severity/status expression. Severity-like values get semantic colors and lead with the most severe; any other grouping gets the chart's palette colors ordered by total.

This reuses the behavior the app already has: formatResponseForTimeChartsetLineColorsgetColorProps assigns a semantic color when a series value looks like a log level and a palette color otherwise. A new formatResponseForSeriesTotals runs that same pipeline — identical series keys, the same stacking order, the same setLineColors — and just sums per series instead of emitting buckets. So the legend cannot disagree with the bars it summarizes, and a SeriesTotal[] helper is now available anywhere else totals-per-series are needed.

Deriving from the real groups also fixed a correctness bug in the first cut: info and debug are distinct stacked bar series that happen to share the info color, but a hardcoded three-bucket legend merged them into one "Info" row that matched no bar. Below, seven distinct severity values each get their own item:

Legend showing error 5, critical 2, fatal 1, warn 10, info 77, debug 8 and a +1 more overflow

Repointing the source's grouping at ServiceName needs no code change — the same legend renders service names with palette colors ordered by total:

Legend showing checkout 59, shipping 32, payments 15 with palette colors

Clicking emits the same SeriesGroupFilter[] contract the chart's existing "Focus" action uses, so the page's existing handleFocusSeries applies it and it works for any column. Here clicking checkout yields ServiceName IN ('checkout'):

Filtered to 59 results with a ServiceName = checkout pill

legend_adapts_to_non_severity_groups_and_filters.mp4

No extra query — it reuses the histogram's response

The legend needs exactly the rows the histogram already fetched, so a new useSearchHistogramQuery owns the canonical query key and the total-count hook now goes through it too. Chart, total count, and legend all resolve from one React Query cache entry.

This mattered: an earlier revision hand-wrote the legend's key with disableQueryChunking: false. Since JSON.stringify drops undefined object values, that hashed differently from the histogram's unset flag, silently splitting the cache entry and dispatching a duplicate ClickHouse query. Centralizing the key removes the class of bug, not just the instance. Verified against system.query_log, counting histogram queries for one page load:

Variant Histogram queries per load
Duplicate-key bug (reintroduced deliberately) 2 (11ms apart)
This PR 1

DBTimeChart sets no staleTime, so any remount legitimately refetches; the duplicate's signature is therefore two executions milliseconds apart within one load, which is what the table measures.

Details worth a reviewer's attention

  • Ordering is one comparator: severity rank descending, then total descending, then label. Severity-looking series lead with the most severe (matching how the chart's own legend lists the top of the stack first, and serving the actual ask of surfacing error counts); everything else falls back to biggest-contributor, which also makes the overflow cutoff meaningful.
  • Overflow shows 6 inline items then a +N more popover, mirroring the chart tile legend's pattern. The strip is full-width so it fits more than the tile's 4, but it stays one row so it never pushes the results table down.
  • Ungrouped queries render nothing — there'd be a single series whose total is already displayed as the result count above the histogram.
  • An unusable response shape degrades to no legend rather than taking the search page down, mirroring how the chart handles it.
  • Reverted the setIncludeFilter filter-state helper from the first cut; one item per group value means the existing setOnlyFilters path covers it, so searchFilters.tsx is untouched by this PR.

Tests

  • ChartUtils.test.tsformatResponseForSeriesTotals: summation across buckets, per-value series (no roll-up), semantic vs palette colors, ungrouped responses, and a parity check asserting the keys/colors/order match formatResponseForTimeChart for both a severity and a non-severity grouping.
  • SearchHistogramLegend.test.tsx — 11 tests: whole-range totals, most-severe-first ordering, dynamic non-severity groups, semantic and palette swatch colors, click payload, overflow popover, and the empty/loading/ungrouped/malformed cases.
  • DBSearchPageQueryKey.test.tsx — asserts the legend's key hashes identically to DBTimeChart's using React Query's own hashKey (a toEqual passes for keys that hash apart, so it would not have caught the original bug). Confirmed these fail when the bug is reintroduced.
  • make ci-lint clean (0 errors); make ci-unit green (148 suites / 2476 tests in packages/app).

How to test on Vercel preview

Preview routes: /search

Steps:

  1. Navigate to /search and ensure a log source with several severity levels is selected.
  2. Set the time range wide enough to include error, warn, and info logs, then click "Run".
  3. Verify a legend appears directly below the histogram with a colored square and whole-range total per severity value, most severe first.
  4. Verify each legend swatch color matches the color of that series in the stacked bars.
  5. Verify the legend totals sum to the "N Results" count above the histogram (adding any items behind "+N more").
  6. Click the "error" item and verify the search narrows: a SeverityText filter pill appears, the result count drops, and the histogram shows only that series.

References

  • Linear Issue: HDX-4837

To show artifacts inline, enable in settings.

Linear Issue: HDX-4837

Open in Web Open in Cursor 

cursoragent and others added 2 commits July 30, 2026 00:19
Add a legend below the search histogram that shows total counts per
severity level (Info, Warn, Error) aggregated across the entire selected
time range. Each legend item is colored to match the corresponding
histogram bars and is clickable to filter search results to that severity.

- New SearchHistogramLegend component with useSearchSeverityCounts hook
  that reuses the same React Query cache as the histogram (no extra fetch)
- Groups raw severity values into normalized classes (info/warn/error)
- Click-to-filter via new setIncludeFilter method on search filters that
  handles multiple raw values per class atomically
- Legend appears in both results and patterns analysis modes

Resolves HDX-4837

Co-authored-by: Mike Shi <mike@hyperdx.io>
Co-authored-by: Mike Shi <mike@hyperdx.io>
@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2b9e55a

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

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Jul 31, 2026 5:40am
hyperdx-storybook Ready Ready Preview Jul 31, 2026 5:40am

Request Review

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 251 passed • 1 skipped • 807s

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

Tests ran across 4 shards in parallel.

View full report →

Comment thread packages/app/src/components/SearchHistogramLegend.tsx Outdated
Comment thread packages/app/src/components/SearchHistogramLegend.tsx Outdated
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a query-derived totals legend beneath the search histogram.

  • Reuses the histogram query cache for chart, total-count, and legend consumers.
  • Collapses histogram buckets into per-series totals with chart-consistent colors and ordering.
  • Supports overflow display and series-click filtering for grouped searches.
  • Adds focused unit coverage for formatting, rendering, query-key parity, and direct-trace behavior.

Confidence Score: 4/5

The PR is not yet safe to merge because trace errors remain semantically misclassified and stale legend entries can still apply filters from a previous query.

STATUS_CODE_ERROR still falls through the log-level classifier to the Info color and lowest rank, while keepPreviousData continues exposing clickable prior-query series without an isPlaceholderData interaction guard.

Files Needing Attention: packages/app/src/components/SearchHistogramLegend.tsx, packages/app/src/hooks/useSearchHistogramQuery.ts, packages/app/src/ChartUtils.tsx, packages/app/src/utils.ts

Important Files Changed

Filename Overview
packages/app/src/ChartUtils.tsx Adds shared series ordering and conversion of bucketed chart responses into colored per-series totals.
packages/app/src/components/SearchHistogramLegend.tsx Renders sortable, clickable series totals with an overflow popover.
packages/app/src/hooks/useSearchHistogramQuery.ts Centralizes the search histogram query configuration and cache key for multiple consumers.
packages/app/src/DBSearchPage.tsx Mounts the new legend under search histograms in results and pattern modes.
packages/app/src/components/SearchTotalCountChart.tsx Migrates total-count retrieval to the shared histogram query hook.

Sequence Diagram

sequenceDiagram
  participant Page as DBSearchPage
  participant Hook as useSearchHistogramQuery
  participant CH as ClickHouse
  participant Format as formatResponseForSeriesTotals
  participant Legend as SearchHistogramLegend
  Page->>Hook: histogram chart config
  Hook->>CH: execute shared histogram query
  CH-->>Hook: bucketed grouped response
  Hook-->>Format: response and source
  Format-->>Legend: colored per-series totals
  Legend-->>Page: focus series filters on click
Loading

Reviews (4): Last reviewed commit: "fix(app): keep lint warning budget and k..." | Re-trigger Greptile

… count, and chart

The severity legend built its own React Query key with a hardcoded
disableQueryChunking: false. Because JSON.stringify drops undefined object
values, that hashed differently from the histogram's unset flag, so the legend
opened a second cache entry and dispatched a duplicate ClickHouse query
instead of reusing the histogram's response.

Extract the canonical query into useSearchHistogramQuery and route the legend
and the total count through it, so the key exists in exactly one place and
cannot drift again. Extend the existing query-key test to assert the legend
hashes identically to DBTimeChart using React Query's own hashKey, since
toEqual passes for keys that hash apart.

Co-authored-by: Mike Shi <mike@hyperdx.io>
… fixed severities

The legend hardcoded three severity buckets and was gated on the source's
severity/status expression, which had two problems. It disagreed with the chart:
'info' and 'debug' are distinct stacked bar series, both info-colored, but the
legend merged them into one 'Info' row. And it could only ever describe severity,
even though the histogram groups by whatever the source designates.

Derive the legend from the series the query actually returned via a new
formatResponseForSeriesTotals, which runs the same pipeline as
formatResponseForTimeChart (identical series keys, stacking order, and
setLineColors). That reuses the app's existing 'semantic color when the value
looks like a log level, palette color otherwise' behavior, so the legend can
never disagree with the bars it summarizes, and drops the groupByColumn prop.

Clicking now emits the same SeriesGroupFilter[] contract the chart's Focus action
uses, so the page's existing handleFocusSeries applies it. That removes the need
for the setIncludeFilter helper added earlier, which is reverted.

Co-authored-by: Mike Shi <mike@hyperdx.io>
CI's lint script enforces --max-warnings 740, which main already sits at
exactly, so the 7 warnings my test files added failed the build. My earlier
local check was wrong: I piped 'make ci-lint' into grep and read grep's exit
status instead of make's, which hid this.

- Mock hooks with jest.fn() in the factory and pick them up via
  jest.requireMock, so no use-prefixed function is declared (the
  no-unnecessary-use-prefix rule infers the name from the property key) and
  the fixtures stay the partial shapes the tests care about
- Drop the 'as TSource' / 'as any' assertions: ResponseJSON.meta is optional,
  and the severity source is now one shared const reused by the existing test
- Replace the empty mockImplementation body

Also drop three exports knip flagged as unused: useSearchSeriesTotals and
SeriesTotalItem are internal to the legend, and inferGroupColumn became dead
when the group-column lookup moved into formatResponseForSeriesTotals.

Co-authored-by: Mike Shi <mike@hyperdx.io>
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.

2 participants