Skip to content

feat(status): add KPI stat strip to the Health tab - #1506

Merged
kylebernhardy merged 3 commits into
stagefrom
kyle/health-kpi-strip
Jul 15, 2026
Merged

feat(status): add KPI stat strip to the Health tab#1506
kylebernhardy merged 3 commits into
stagefrom
kyle/health-kpi-strip

Conversation

@kylebernhardy

@kylebernhardy kylebernhardy commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Implements #1457 — the Health tab now opens with a row of five stat tiles above the panel grid: CPU (p95), Heap used, Main thread utilization, Error rate, and Request p95. Each tile shows:

  • the latest-bucket cluster value (formatted through the shared formatValue path the charts use),
  • a delta vs the previous window of equal length (arrow + signed %, captioned "vs prev 1h/6h/…"; all five vitals are up-is-bad, so up wears destructive and down wears green),
  • an axis-less inline-SVG sparkline of the current window (muted de-emphasis hue, final segment in the accent chart hue),
  • an em-dash + no delta when data is absent, and skeletons while loading.

All five tiles render through one KpiTile component driven by the KPI_TILES config, so the delta / absent-data / skeleton treatment is identical by construction (plus an explicit symmetry test).

Review focus — network cost and query keys

Honest per-tick accounting while the Health tab is shown (all five current-window keys use exactly MetricPanel's arguments — same metric string, the context's timeRange/bucketMs, no conditions — so keys coincide with the panels' convention):

  • Current window: 2 POSTs per refresh tick. The cpu-usage / memory / main-thread-utilization panels are mounted on the Health tab, so those three current-window fetches dedupe with the panels' own queries. The success and duration panels live on the (unmounted) Requests tab, so those two are real POSTs — they use the panels' key convention, which means they seed the cache for a Requests visit (staleTime ∞ / gcTime 60s) and are never fetched twice for the same window.
  • Previous window: 5 POSTs only on a bucket-boundary crossing. The previous window is historical, so its boundaries are quantized to the preset's bucket grid (floor(startTime / bucketMs) * bucketMs); consecutive refresh ticks within one bucket produce the same query key and hit the staleTime-Infinity cache instead of the network. The current-window args are deliberately NOT quantized (they must stay byte-identical to MetricPanel's).
  • Every key shares ANALYTICS_QUERY_KEY_PREFIX + instanceId, so StatusTabs' in-flight refresh guard (queryClient.isFetching on that prefix) covers all of them.
  • Tests pin the accounting: current-window key equals getRawAnalyticsQueryOptions(...) output (the panel convention); a panel-shaped hook next to the tile yields exactly 2 POSTs; the strip issues 5 × 2 POSTs on first paint; and a quantization test asserts ticks within one bucket add no previous-window POST while crossing a boundary adds one.

Aggregation notes

  • CPU (p95): per-scope (harper/user) count-weighted p95 — the panel's exact aggregation — then the scope series are summed per bucket so the tile reads as total process CPU. The sum is pinned to the harper/user dims via includeDims: Harper's profiler also emits per-hot-function-location cpu-usage records (>100 hits at one code location) whose samples are already counted inside the scope totals, so summing every path dim would double-count CPU on busy nodes. A bucket missing one expected scope gaps rather than presenting a partial sum as the total.
  • Error rate: projects 1 − total/count per record under count-weighted-mean (weight = count), which collapses algebraically to the Σ-correct (Σcount − Σtotal)/Σcount — the same arithmetic as the derived error-rate panel, not mean-of-ratios. A test pins the canonical fixture (≈0.0188, not 0.455).
  • Heap used / Main thread / Request p95 mirror their panels' aggregators (last+mean, mean+mean, CWM+CWM).
  • Delta = current-window mean of bucket values vs previous-window mean, computed only from a settled pair: useAnalyticsRecords exposes isPlaceholderData, and while either window still holds keepPreviousData rows during a window slide the tile holds the last settled delta instead of pairing mismatched windows (which flipped the arrow through a bogus near-zero reading each refresh tick). previous = 0 with nonzero current renders no delta (undefined relative change), 0 → 0 reads flat 0%.

Testing

  • kpiMath.test.ts — pure unit tests: collapse (sum/mean, null-drop, ordering, includeDims exclusion + missing-dim gap), latest/mean, delta math (previous=0, negative baseline, epsilon-flat, non-finite), window labels.
  • kpiTiles.test.ts — per-tile pipeline behavior (Σ-correct error rate, CPU scope sum + hot-location exclusion + missing-scope gap, memory gauge, MTU projection, count-weighted p95) + config symmetry.
  • KpiTile.test.tsx — jsdom: value + delta rendering, absent-data em-dash, previous-window-empty (value without delta), loading skeleton, query-key convention + dedupe, held delta during the placeholder phase (mutation-tested: fails against the ungated delta), previous-window key quantization, sparkline flat/short-series branches, full-strip symmetry.
  • Full suite: 222 files / 1525 passed, tsc -b, oxlint, dprint all clean.

Cross-model reviews (pre-PR, pinned to this diff)

  • Codex: no actionable correctness issues.
  • Gemini 3.1 Pro: dedupe claim confirmed for the mounted-panel case; one minor (sparkline memo runs the pipeline while loading — benign: current.data is the frozen EMPTY array and the tile shows a skeleton) and two test-coverage nits, both addressed with added tests.

Deferred / notes

  • Sparkline connects across missing buckets rather than gapping — a deliberate simplification for a 32px decorative trend line; the panels below carry the precise per-bucket view.
  • No visual verification against a live Harper in this pass; the tiles use existing theme tokens (text-muted-foreground, text-green, text-destructive, --chart-node-1) that are defined for both themes.

Fixes #1457

🤖 Generated with Claude Code

https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV

Comment generated by kAIle (Claude Fable 5)

Adds a row of five stat tiles above the Health tab's panel grid — CPU
(p95, harper+user scopes summed), heap used, main-thread utilization,
error rate, and request p95 — so "is anything wrong right now?" reads at
a glance. Each tile shows the latest-bucket cluster value, a delta vs
the previous window of equal length (arrow + %, up-is-bad coloring), and
an axis-less inline-SVG sparkline of the current window; em-dash + no
delta when data is absent, skeletons while loading.

Tiles reuse useAnalyticsRecords + runPipeline: the current-window fetch
passes exactly the panels' arguments so it lands on their existing query
key (react-query dedupes to one POST); the shifted previous-window query
is the only new key — at most one extra get_analytics POST per metric
per window slide, and it shares the key prefix StatusTabs' in-flight
refresh guard scans. The error-rate tile projects 1 − total/count per
record under count-weighted-mean, which collapses algebraically to the
Σ-correct (Σcount − Σtotal)/Σcount — same arithmetic as the derived
error-rate panel.

Fixes #1457
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 49.48% 4996 / 10095
🔵 Statements 49.96% 5336 / 10680
🔵 Functions 41.53% 1215 / 2925
🔵 Branches 42.88% 3322 / 7746
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/status/analytics/hooks/useAnalyticsRecords.ts 95.23% 93.33% 100% 94.73% 125-129
src/features/instance/status/analytics/tabs/HealthTab.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/kpi/KpiSparkline.tsx 100% 87.5% 100% 100%
src/features/instance/status/analytics/tabs/kpi/KpiStrip.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/kpi/KpiTile.tsx 100% 85% 100% 100%
src/features/instance/status/analytics/tabs/kpi/kpiMath.ts 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/kpi/kpiTiles.ts 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/kpi/useKpiTileData.ts 95.23% 90% 100% 100% 70
Generated in workflow #1512 for commit 8dcb269 by the Vitest Coverage Report Action

@kylebernhardy
kylebernhardy marked this pull request as ready for review July 15, 2026 02:57
@kylebernhardy
kylebernhardy requested a review from a team as a code owner July 15, 2026 02:57

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces a new KpiStrip component to the HealthTab of the instance status analytics page, displaying five vital metrics (CPU, Heap used, Main thread, Error rate, and Request p95) as KPI tiles with latest values, delta comparisons, and inline SVG sparklines. Feedback suggests optimizing the sparkline component by conditionally rendering the first path only when there are more than two points to avoid redundant SVG elements. Additionally, it is recommended to improve the delta row's accessibility by adding a proper ARIA role and to refine the formatting of flat percentage values to prevent displaying signed zero percentages.

Comment thread src/features/instance/status/analytics/tabs/kpi/KpiSparkline.tsx Outdated
Comment thread src/features/instance/status/analytics/tabs/kpi/KpiTile.tsx Outdated
… and prev-window caching

- CPU tile: constrain the scope sum to the harper/user dims via a new
  includeDims on KpiTileDef. The profiler also emits per-hot-function-
  location cpu-usage records (>100 hits at one location) whose samples
  are already counted inside the harper/user scope totals, so summing
  every 'path' dim double-counted CPU on busy nodes. A bucket missing
  an expected scope now gaps instead of presenting a partial sum as
  the total.

- Delta: useAnalyticsRecords now exposes isPlaceholderData, and the
  tile only computes a fresh delta when BOTH windows hold settled
  (non-placeholder) data, holding the last settled delta otherwise.
  Previously each refresh tick had a settle-gap where keepPreviousData
  kept isLoading false while one side still held the older window's
  rows, so computeDelta paired mismatched windows and flipped the
  arrow through a bogus near-zero reading.

- Previous window: quantize its boundaries to the bucket grid so
  consecutive refresh ticks within one bucket share a query key and
  hit the staleTime-Infinity cache (the current-window args stay
  byte-identical to MetricPanel's). Corrected the dedupe comments to
  the honest accounting: the success/duration panels live on the
  unmounted Requests tab, so those two current-window fetches are real
  POSTs per tick that seed the cache for a Requests visit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
@kylebernhardy

Copy link
Copy Markdown
Member Author

Pushed cda364e addressing three review findings — each one made the strip more trustworthy, and all three came with regression tests:

1. CPU tile no longer double-counts profiler hot-location records. The cpu-usage metric carries more than the harper/user scope records: Harper's profiler also emits per-hot-function-location records (>100 hits at one code location) whose samples are already counted inside the scope totals. Summing every path dim therefore over-reported CPU exactly on the busiest nodes — the ones you'd be looking at. The sum is now pinned to the two scopes via a new includeDims on the tile def, and a bucket missing one expected scope gaps rather than presenting a partial sum as the total. Tests cover both the hot-location exclusion and the missing-scope gap.

2. Deltas only compute from settled window pairs. With keepPreviousData, each refresh tick had a settle-gap where one query already held the new window's rows while the other still held the old ones — computeDelta then paired mismatched windows and flipped the arrow through a bogus near-zero reading. useAnalyticsRecords now exposes isPlaceholderData, and the tile computes a fresh delta only when both sides are settled, holding the last settled delta in the meantime so nothing blanks or flickers. The new test was mutation-verified: it fails against the ungated implementation.

3. Honest network accounting + previous-window caching. The dedupe comments (and the PR description) claimed every current-window call dedupes with a panel — true only for the three panels mounted on Health; the success/duration panels live on the Requests tab, so those two are real POSTs per tick (which usefully seed the cache for a Requests visit). Comments and description now say so. And since the previous window is historical, its boundaries are now quantized to the bucket grid, so consecutive ticks within one bucket share a query key and hit the staleTime-Infinity cache instead of issuing 5 fresh POSTs every tick. The current-window args stay byte-identical to MetricPanel's, preserving the dedupe that does hold.

Gates: tsc -b, dprint, oxlint, and the full vitest suite (222 files / 1525 tests) are all green.

Comment generated by kAIle (Claude Fable 5)

… empty sparkline path (review)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
@dawsontoth

Copy link
Copy Markdown
Contributor

idk, is it possible the analytics stuff is bringing down instances in the free tier? I have a very minimally used free cluster in stage, and I swear when I navigate to the analytics status stuff, it goes down. I'm talking cross PRs. What is this costing us from a performance perspective? Are we solving problems in the UI that should be solved in core, or should be solved in an analytics aggregation service, @kylebernhardy ?

Comment thread src/features/instance/status/analytics/hooks/useAnalyticsRecords.ts
@dawsontoth

Copy link
Copy Markdown
Contributor

Consolidated the performance / architecture question I raised above into a tracking issue — #1516 — assigned to @kylebernhardy to triage, so it's owned cross-PR rather than living only in this thread.

Adding the concrete data point from local review: on a stage instance, switching the Health tab to Last 7 days threw 13× timeout of 60000ms exceeded in a single window slide — the tab fires a get_analytics POST per panel (5 here) plus this PR's KPI-strip queries per refresh tick, and the cluster-aggregation runs client-side. Fine at 1h; scales badly with range width.

Not calling this a blocker for this PR specifically — it's the broader "how much analytics work belongs in the UI" direction call, now tracked in #1516.

@kylebernhardy
kylebernhardy added this pull request to the merge queue Jul 15, 2026
Merged via the queue into stage with commit f51b7e9 Jul 15, 2026
2 checks passed
@kylebernhardy
kylebernhardy deleted the kyle/health-kpi-strip branch July 15, 2026 15:12
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.

Add a KPI stat strip to the Health tab

2 participants