feat(app): add severity legend with counts to search histogram - #2753
feat(app): add severity legend with counts to search histogram#2753MikeShi42 wants to merge 5 commits into
Conversation
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 detectedLatest commit: 2b9e55a The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
E2E Test Results✅ All tests passed • 251 passed • 1 skipped • 807s
Tests ran across 4 shards in parallel. |
Greptile SummaryAdds a query-derived totals legend beneath the search histogram.
Confidence Score: 4/5The 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.
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
|
| 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
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>
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:
formatResponseForTimeChart→setLineColors→getColorPropsassigns a semantic color when a series value looks like a log level and a palette color otherwise. A newformatResponseForSeriesTotalsruns that same pipeline — identical series keys, the same stacking order, the samesetLineColors— and just sums per series instead of emitting buckets. So the legend cannot disagree with the bars it summarizes, and aSeriesTotal[]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:
infoanddebugare 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
ServiceNameneeds 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 existinghandleFocusSeriesapplies it and it works for any column. Here clickingcheckoutyieldsServiceName 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
useSearchHistogramQueryowns 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. SinceJSON.stringifydrops 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 againstsystem.query_log, counting histogram queries for one page load:DBTimeChartsets nostaleTime, 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
+N morepopover, 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.setIncludeFilterfilter-state helper from the first cut; one item per group value means the existingsetOnlyFilterspath covers it, sosearchFilters.tsxis untouched by this PR.Tests
ChartUtils.test.ts—formatResponseForSeriesTotals: summation across buckets, per-value series (no roll-up), semantic vs palette colors, ungrouped responses, and a parity check asserting the keys/colors/order matchformatResponseForTimeChartfor 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 toDBTimeChart's using React Query's ownhashKey(atoEqualpasses for keys that hash apart, so it would not have caught the original bug). Confirmed these fail when the bug is reintroduced.make ci-lintclean (0 errors);make ci-unitgreen (148 suites / 2476 tests inpackages/app).How to test on Vercel preview
Preview routes: /search
Steps:
References
To show artifacts inline, enable in settings.
Linear Issue: HDX-4837