Exemplars: P2/P3 follow-ups from #2536 review rounds
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 9.9k
- Forks
- 471
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 117
Description
Follow-up backlog from the review rounds on #2536. Splitting these out so the PR
can merge on its P0/P1s rather than re-litigating the same batch every round.
Why this is safe to defer: the overlay is behind NEXT_PUBLIC_ENABLE_EXEMPLARS,
off by default, and additionally per-chart behind enableExemplars. None of the
below is reachable in a default deployment.
Context on the numbers. The deep reviewer returns a similar-sized batch every
run against the whole feature surface — roughly 1 P0/P1 and ~15 P2 each time,
across four runs. It is not a growing defect count; it is a full re-review each
time, with overlap. Several items below started as round-1 P3s and were re-graded
to P2 in later rounds because they were never fixed, which is the argument for
tracking them here instead of descoping them again.
Themes
- Accessibility.
ExemplarDothas no keyboard focus, no touch path, and no
ARIA; the hover card is the only route to "Inspect trace", so those users cannot
reach the feature at all. Flagged since round 1. useExemplarTraceMetahardening. Hand-quoted database/table names, no time
predicate on the trace lookup, a query key that omits the source expressions the
SQL interpolates, andNumber(row.durationMs)on a non-numeric expression.- Marker placement edges. Thinning runs before clamping; the one-bucket
tolerance applies to the lower bound as well as the upper; themaxExemplars <= 0
branch dedupes differently from the budgeted branch. - Backend consistency. Three separate implementations answer "cap exemplars per
time bucket" (client, SQL, render), and the code documents that the two backends
therefore show different marker sets for the same chart. - API surface.
/query_exemplarsmaps every thrown error to 400bad_data; a
PromQL chart on a ClickHouse-backed connection passes every client gate and gets
an empty success;enableExemplarsis settable via MCP on tiles where it does
nothing, with no warning back to the caller. - Schema and docs.
maxExemplarsaccepts up to 1000 whileEXEMPLAR_QUERY_LIMIT
is 200;ExemplarSchema.attributesis never populated or read; several comments in
types.tsandtelemetry-generatordescribe a pipeline that no longer exists. - Disputed. Round 2 asked for
histogram_quantile(...) * 1000to be rejected as
a unit mismatch and it was; round 4 flags that rejection, since it is the standard
seconds-to-milliseconds idiom. Needs a decision, not a fix.
Full list
Verbatim from the two most recent deep reviews. Line numbers are from those runs
and have since moved.
-
packages/app/src/components/Exemplars/exemplarPoints.ts:129— The window split triggers onordered.length > ceil(maxExemplars * 0.75)rather than> maxExemplars, so at the default budget of 12 a chart with 12 populated buckets renders only 9 markers and three buckets get none, contradicting the docstring's "more buckets than the marker budget".- Fix: Gate the split on
ordered.length <= maxExemplarsand keepwindowCountonly as the split width so every bucket that fits the budget still emits its rank-0 marker.
- Fix: Gate the split on
-
packages/app/src/HDXMultiSeriesTimeChart/useExemplarMarkers.ts:135— A hovered marker's React key and data x are fixed to its timestamp whilexAxisDomainshifts every live-tail tick, so the same<g>node slides out from under a stationary cursor with nomouseleave: the card keeps rendering at the x/y captured atmouseenterandisExemplarHoveredstays true, suppressing the series tooltip until the pointer moves.- Fix: Re-read the hovered point's current x/y from
exemplarPointson every recompute to reposition the card, and re-validate the pointer against the marker's current node so hover-end fires when it no longer sits underneath.
- Fix: Re-read the hovered point's current x/y from
-
packages/app/src/HDXMultiSeriesTimeChart/MemoChart.tsx:202—ChartComponentswaps betweenAreaChartandBarChart, a different element type that fully remounts everyExemplarDotwithout dispatchingmouseleave, and the reset guards only fire when the key is absent fromexemplarPoints, so switching display type while a card is pinned orphans it over markers that no longer exist.- Fix: Add an effect keyed on
displayTypethat clears the hovered and pinned exemplar state before the chart subtree remounts.
- Fix: Add an effect keyed on
-
packages/app/src/components/Exemplars/ExemplarHoverCard.tsx:147—useExemplarCarddestructures onlydataandisLoadingfromuseExemplarTraceMeta, so a failed trace query and a source whoseexemplarTraceSourceIdis not a Trace kind both arrive asmeta === undefined, isLoading === falseand render "Trace not found in source", reporting a misconfiguration or query error as a missing trace.- Fix: Thread
isErrorand an unsupported-source state through to the card and render a distinct "could not load trace details" message for each.
- Fix: Thread
-
packages/app/src/HDXMultiSeriesTimeChart/useChartScales.ts:120—hasSelectionalone (withoutfitYAxisToData) switchesyAxisDomainto a numeric pair whose lower bound is the data minimum, so one click on the sole legend entry of a single-series chart makesclampExemplarYreturn null for every below-floor marker and silently reverts the overlay to a max envelope with no notice.- Fix: Fit the y-axis floor only when
fitYAxisToDatais set, or surface the count of markers dropped byclampExemplarYin the exemplar notice.
- Fix: Fit the y-axis floor only when
-
packages/app/src/hooks/useExemplars/exemplarNormalize.ts:23—collapsesHistogramBucketstests the literal substringhistogram_quantile(whileisPromqlExemplarEligibleallows\s*before the paren, sohistogram_quantile (0.95, …)passes the toggle gate, keepslein the group key, and returnsdropped: 'multiple-series'with a notice telling the user to aggregate to a single line they already have.- Fix: Make
collapsesHistogramBucketsuse the same whitespace-tolerant, literal-stripped regex the eligibility gate uses so the two checks cannot disagree about one expression.
- Fix: Make
-
packages/api/src/routers/api/prometheus.ts:232— Thepipeline()catch cannot distinguish a client disconnect from an upstream failure and always returns 502, so ordinary tab closes and live-tail supersessions incrementhyperdx.prometheus.query_errors, the counter whose own docblock scopes it to backend health for alerts and SLOs.- Fix: Detect a client-initiated close in the pipeline catch and return the already-written upstream status so
recordProxyOutcomedoes not count it.
- Fix: Detect a client-initiated close in the pipeline catch and return the already-written upstream status so
-
packages/api/src/routers/api/prometheus.ts:165—proxyToPrometheusnever receivesreq, so the only abort on the outboundfetchis the 90sAbortSignal.timeout; the client forwards its abort signal precisely because live-tail supersedes the request every tick, yet the API keeps executing the superseded upstream query to completion.- Fix: Pass
reqin and combine a signal from itscloseevent with the timeout viaAbortSignal.anyso a client disconnect cancels the upstream fetch.
- Fix: Pass
-
packages/app/src/hooks/useExemplars/quantize.ts:13—EXEMPLAR_KEY_QUANTUM_MSis 30s whileEXEMPLAR_STALE_TIME_MSis 60s, andfetchRangeis part of the query key, so the key changes twice per stale window and the declared 60s staleness tolerance can never suppress a fetch on a live-tail chart.- Fix: Derive one constant from the other so the key quantum is at least the stale time.
-
packages/app/src/hooks/useExemplars/useExemplarTraceMeta.ts:42— The hover query isWHERE TraceId = {traceId:String}with no time predicate andenabledthe instant atraceIdappears, so sweeping the cursor across a marker cluster fires one unbounded trace-table lookup per 9px hit circle crossed with no debounce, even though the exemplar's own timestamp and anEXEMPLAR_TRACE_WINDOW_MSprecedent are already available.- Fix: Bound the query with a window around the exemplar's timestamp and debounce the hovered
traceIdby the same grace the close timer already uses.
- Fix: Bound the query with a window around the exemplar's timestamp and debounce the hovered
-
packages/app/src/HDXMultiSeriesTimeChart/useExemplarMarkers.ts:135— The hover-reset, pin-reset, andsuppressNextClickRefguards have no tests, and neither douseExemplarCard's quantized auto-unpin, Escape handler, or the two URL shapes innavigateToExemplarTrace, despite the hook's own docblock stating every bug in this layer came from a marker outliving its data.- Fix: Add
renderHooktests that drop a hovered and a pinned marker from the rerenderedexemplarsarray, assert the reset callbacks fire, and cover the same-window vs changed-window unpin cases.
- Fix: Add
-
packages/app/src/components/DBTimeChart/DBTimeChart.tsx:317—plottedSeriesCountfilters outisDashedcomparison lines to fix a bug the surrounding comment describes, butDBTimeChart.test.tsxmocks@/hooks/useExemplarswholesale and never inspects its arguments, so re-counting the dashed previous-period line would pass every existing test.- Fix: Spy on
useExemplarsin one test, render with one solid plus oneisDashedseries, and assertplottedSeriesCount === 1.
- Fix: Spy on
-
packages/api/src/routers/api/prometheus.ts:556—prometheus.test.tscovers only the extracted pureresolveExemplarWindow, leaving the handler'sisPrometheusEndpointbranch selection, the ClickHouse-backed empty-success response, and the deliberate 5xx-only error-counter rule unexercised.- Fix: Add a route-level test with mocked
getConnectionByIdandfetchasserting the narrowedstartis proxied, the ClickHouse branch returnsdata: []without fetching, and a 400 does not increment the error counter.
- Fix: Add a route-level test with mocked
-
packages/api/src/routers/api/prometheus.ts:190— The new proxy route reaches a user-suppliedconnection.hostwith no protocol or private-IP check and withfetch's defaultredirect: 'follow', while the sibling connection-test path inclickhouseProxy.tsalready appliesisPrivateIpto the same field.- Fix: Validate
connection.hostwithisPrivateIpat write time and passredirect: 'manual'inproxyToPrometheus.
- Fix: Validate
-
packages/app/src/components/DBTimeChart/useExemplarCard.ts:86—'traceSourceId' in sourceis never true forSourceKind.MetricorSourceKind.Promql(neitherMetricSourceSchemanorPromqlSourceSchemadeclares that field, and zod strips unknown keys), so the documented fallback is dead code and a chart without an explicitexemplarTraceSourceIdcan never resolve trace metadata.- Fix: Resolve the fallback by hopping through the metric source's
logSourceIdto that log source'straceSourceId, or drop the fallback and requireexemplarTraceSourceId.
- Fix: Resolve the fallback by hopping through the metric source's
-
packages/app/src/HDXMultiSeriesTimeChart/useExemplarMarkers.ts:134— Thinning runs before clamping and the 30s fetch quantum can place exemplars up to 45s past the rendered x-domain at 15-second granularity, so marker-budget slots are spent on points thatclampExemplarXthen drops, permanently raising a notice that blames a fitted y-axis floor on a correctly configured chart.- Fix: Trim exemplars to the drawn x-domain before thinning, and exclude quantization-surplus drops from the count reported through
onExemplarsDropped.
- Fix: Trim exemplars to the drawn x-domain before thinning, and exclude quantization-surplus drops from the count reported through
-
packages/app/src/hooks/useExemplars/useExemplarTraceMeta.ts:50— The per-hover trace lookup filters only onTraceIdwith no time predicate and sets noretry, so each hovered marker can trigger a full trace-table scan retried three times with backoff before the card can report failure.- Fix: Pass the exemplar's timestamp in and add a bounded
timestampValueExpression BETWEENpredicate, and setretry: 1to match the sibling hook.
- Fix: Pass the exemplar's timestamp in and add a bounded
-
packages/app/src/components/Exemplars/exemplarPoints.ts:270— The one-bucket tolerance is applied to the lower bound as well as the upper one, so exemplars fetched byquantizeStart's widened window are snapped forward onto the first plotted bucket and drawn at a time they did not occur, without being counted as dropped.- Fix: Make the tolerance one-sided —
if (x < min || x > max + tolerance) return null;— since only the end-exclusive upper bound needs it.
- Fix: Make the tolerance one-sided —
-
packages/app/src/hooks/useExemplars/exemplarNormalize.ts:106— The full Prometheus body is materialised and run through oneExemplarSchema.safeParseper exemplar on the UI thread before any cap applies, and is discarded wholesale when the response spans multiple series.- Fix: Decide the multiple-series drop from
seriesLabelsbefore parsing any exemplar, and short-circuit once the parsed count exceeds a hard multiple ofEXEMPLAR_QUERY_LIMIT.
- Fix: Decide the multiple-series drop from
-
packages/api/src/routers/api/prometheus.ts:646— A PromQL chart on a ClickHouse-backed connection passes every client gate and receives{status:'success', data: []}, so the toggle is on, no markers appear, no notice explains it, and a proxy round-trip is paid per quantised window.- Fix: Return a distinguishable marker such as
unsupported: truefor the non-Prometheus branch and surface it as an exemplar notice, or gate the editor toggle onconnection.isPrometheusEndpoint.
- Fix: Return a distinguishable marker such as
-
packages/app/src/hooks/useExemplars/useExemplarTraceMeta.ts:36— Database and table names from the source document are hand-quoted with backticks and spliced into the query, unlike every sibling renderer which binds them as{Identifier}parameters, so a name containing a backtick terminates the quoted identifier.- Fix: Build the
FROMclause through the parameterized{ Identifier: ... }path thatrenderFromuses instead of string concatenation.
- Fix: Build the
-
packages/api/src/routers/api/prometheus.ts:633— The new route adds another entry point intoproxyToPrometheus, which fetches a member-writableconnection.hostserver-side with no scheme or address validation and default redirect-following, then streams the body back to the caller.- Fix: Harden
proxyToPrometheusonce for all four callers by rejecting non-http(s) schemes, blocking loopback/link-local/RFC1918 targets, and settingredirect: 'manual'.
- Fix: Harden
-
packages/api/src/routers/api/prometheus.ts:647— The handler's catch maps every thrown error to HTTP 400bad_dataand incrementsprometheusQueryErrorsunconditionally, so a Mongo failure ingetConnectionByIdreads as a client mistake while a malformed timestamp pollutes the counter thatrecordProxyOutcomedeliberately keeps 5xx-only.- Fix: Return 5xx for errors that are not recognised input-validation failures, and restrict the counter increment to those.
-
packages/app/src/components/Exemplars/ExemplarDot.tsx:51— The marker hard-codes hex fallbacks and reuses--color-text-defaultfor its stroke, whichagent_docs/data_viz_colors.mdexplicitly forbids in chart components ("No new hex strings in chart components").- Fix: Source the fill from
getChartColorWarning()and the outline from a chart border token rather than a text token or a literal hex.
- Fix: Source the fill from
-
packages/app/src/components/Exemplars/ExemplarDot.tsx:37— The marker<g>carries onlyonMouseEnter/onMouseLeave/onClickwith notabIndex,role,aria-label, or key handler, and the card only appears on marker hover, so the entire exemplar-to-trace path is unreachable without a pointer.- Fix: Give the marker a focusable role with an accessible label and open the card on focus and
Enter/Space.
- Fix: Give the marker a focusable role with an accessible label and open the card on focus and
-
packages/app/src/hooks/useExemplars/exemplarNormalize.ts:9— The hooks layer importslabelDistinguishesSeries/promqlSeriesLabelRuledirectly from@/components/Exemplars/promqlSeriesLabels, whichcomponents/Exemplars/index.tsdoes not re-export despite documenting itself as the folder's public surface.- Fix: Move
promqlSeriesLabels.tsintohooks/useExemplars/since it is pure string logic, or re-export it from the barrel.
- Fix: Move
-
packages/api/src/mcp/tools/dashboards/schemas.ts:537—enableExemplarsandexemplarTraceSourceIdare settable through the MCP dashboard tools, butclickstack_query_tileandclickstack_timeseriesonly run the tile's main series query, so an agent can enable the overlay and never read back whether markers exist or what traces they point at.- Fix: Extend the tile/timeseries query tools to run the exemplar query and return the resulting
Exemplar[].
- Fix: Extend the tile/timeseries query tools to run the exemplar query and return the resulting
-
packages/app/src/HDXMultiSeriesTimeChart/useExemplarMarkers.ts:57— Neither this hook norpackages/app/src/components/DBTimeChart/useExemplarCard.tshas any test, leaving the clamp wiring, drop-count reporting, hover/pin reset-on-disappear, post-zoom click swallow, Escape-to-close, and range-quantized unpin entirely unverified whileDBTimeChart.test.tsxmocks the data hooks away.- Fix: Add
renderHooktests for both hooks covering the reset guards, the drop-count effect, the click-swallow branch, and the pin/hover precedence rules.
- Fix: Add
-
packages/api/src/routers/api/prometheus.ts:570—packages/api/src/routers/api/__tests__/prometheus.test.tscovers only the pure helpers, so the new handler's branch dispatch, missing-param 400s, connection-not-found 404, unauthenticated rejection, and the 502/504 proxy paths have no route-level coverage.- Fix: Add supertest cases for the Prometheus-proxy and ClickHouse branches plus the 400/404/502/504 paths.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Treat this as a broad review backlog rather than one starter task. Begin with the listed Exemplar components, hooks, chart files, and prometheus.ts, then run the referenced DBTimeChart, prometheus, and hook tests. Done requires separately resolving the listed P2/P3 defects, adding the requested coverage, and obtaining a decision on the disputed histogram_quantile item.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- clickhouse, prometheus, react, typescript
- Domain
- accessibility, backend-api-design, full-stack, observability, performance, security, testing-qa
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100