[HDX-4997] Alert evaluation event stream: time-range pagination, per-group breakdown, analytics - #2786
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🔴 Tier 4 — CriticalTouches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
Greptile SummaryThis PR makes alert evaluation history follow the selected time range, adds bounded cursor pagination across sparse history, and surfaces per-group and evaluation analytics. The new range handling currently skips evaluation fetching for valid sub-minute ranges.
Confidence Score: 4/5The PR should not merge until valid sub-minute picker ranges continue to fetch their evaluation history. Flooring both endpoints to minute boundaries can make a valid exact range collapse to equal request bounds, causing the evaluations query to be disabled while the chart still displays that range. Files Needing Attention: packages/app/src/api.ts
|
| Filename | Overview |
|---|---|
| packages/api/src/controllers/alertHistory.ts | Implements bounded range pagination that advances across sparse history and returns grouped evaluation details. |
| packages/api/src/routers/api/alerts.ts | Extends the evaluations endpoint with validated, retention-clamped time bounds and server-provided cursors. |
| packages/app/src/api.ts | Adds range-keyed infinite evaluation queries, but minute quantization disables requests for valid ranges contained within one minute. |
| packages/app/src/AlertDetailPage.tsx | Connects the exact selected range and automatic pagination to the alert chart and evaluation stream. |
| packages/app/src/components/alerts/AlertEvaluationsTable.tsx | Adds analytics columns, expandable per-group/error details, bucket-start labels, and viewport-driven pagination. |
| packages/api/src/tasks/checkAlerts/index.ts | Records query, notification, and backfill analytics on evaluation history records. |
| packages/common-utils/src/types.ts | Defines shared evaluation analytics, group breakdown, pagination cursor schemas, and the group response cap. |
Sequence Diagram
sequenceDiagram
participant User
participant Picker as TimePicker
participant Detail as AlertDetailPage
participant Query as useAlertEvaluations
participant API as GET /alerts/:id/evaluations
participant Mongo as AlertHistory
User->>Picker: Select exact time range
Picker->>Detail: searchedTimeRange
Detail->>Query: alert id + range
Query->>API: startTime, endTime, before
API->>Mongo: bounded window scan
Mongo-->>API: grouped evaluations
API-->>Query: data, hasMore, nextBefore
Query-->>Detail: paginated event stream
Detail->>Query: fetchNextPage at viewport sentinel
Reviews (9): Last reviewed commit: "fix(api): unexport internal alert evalua..." | Re-trigger Greptile
E2E Test Results✅ All tests passed • 271 passed • 1 skipped • 1108s
Tests ran across 4 shards in parallel. |
|
<!-- deep-review --> Deep Review🔴 P0/P1 — must fix
🟡 P2 — recommended
🔵 P3 nitpicks (7)
Reviewers (13): correctness, adversarial, security, reliability, api-contract, performance, testing, maintainability, kieran-typescript, data-migrations, julik-frontend-races, project-standards, agent-native. Testing gaps:
Coverage limitations: |
|
<!-- deep-review --> Deep Review🔴 P0/P1 — must fix
🟡 P2 — recommended
🔵 P3 nitpicks (10)
Reviewers (11): correctness, adversarial, security, reliability, api-contract, performance, testing, maintainability, kieran-typescript, julik-frontend-races, project-standards. Testing gaps:
Reviewer note: |
|
Scope note: 🔴 P0/P1 — must fix
🟡 P2 — recommended
🔵 P3 nitpicks (15)
Reviewers (12): correctness, adversarial, security, reliability, performance, api-contract, kieran-typescript, testing, maintainability, project-standards, agent-native, learnings-researcher. Verified as sound (no findings): team scoping on the new endpoint via Testing gaps:
|
…h bounded infinite scroll (HDX-4997) The alert detail page's evaluation strip and event stream now follow the same time range as the chart instead of always paging from now, and older windows load automatically in 200-window pages as the user scrolls to the bottom of the table. Hard caps so a wide range can never fetch unbounded history: - GET /alerts/:id/evaluations accepts startTime/endTime (epoch ms) with the span clamped to the 31d history retention window, and limit fixed at 200. - Each request scans a hard-bounded slice of at most ~(limit+1) intervals of history — group-by alerts can have many rows per window and the $group stage processes every matched row, so the scan itself must be bounded, not just the returned page. - Because of that bound, pagination is cursor-based via a server-provided nextBefore (epoch ms) that always advances past the scanned slice, so paging keeps progressing across gaps with no evaluations (e.g. alerting job downtime) instead of stalling. The load-more button is replaced with an in-viewport sentinel that fetches the next page when scrolled into view.
The evaluation event stream previously merged a grouped alert's per-group history rows into one summary row per window, so it was impossible to see which group sub-alert fired. - GET /alerts/:id/evaluations now returns a per-group breakdown on each window: group name, state, breach count, latest value, and whether a notification fired. Sorted firing-first and capped at the exported ALERT_EVALUATION_GROUPS_LIMIT (50) per window with groupsTotal reporting the pre-cap count, so firing groups are always visible and wide group-bys can't produce unbounded responses. ERROR rows keep surfacing as window errors, never as groups. No extra scan cost — the rows were already matched; only the $group stage retention changed. - The table renders windows as parent rows (chevron + 'k/n groups firing' summary) expandable into indented per-group child rows with their own state/value/breaches, plus a '+N more groups' row when capped. The most recent firing window auto-expands. Non-grouped alerts render as before. - Window-level 'Latest Value' shows '–' for grouped windows; per-group values live in the child rows.
…X-4997)
Adds a top-level 'analytics' subdocument to AlertHistory records capturing
diagnostics for the evaluation that wrote them:
- queryDurationMs: ClickHouse query duration; on query-failure ERROR rows
this is the time-to-failure (≈ the configured timeout for QUERY_TIMEOUT)
- webhookDurationMs: total wall time delivering webhook notifications in
the evaluation, including retries
- backfilledBuckets: earlier buckets backfilled in this run after missed
ticks (expected buckets − 1); 0 in steady state
The object is evaluation-level (identical on every row one evaluation
writes, including per-group rows and the webhook-failure ERROR row);
recordAlertErrors gains an optional analytics param so query-failure ERROR
rows carry it too. Optional field — no migration; the evaluations endpoint
derives backfilledBuckets from distinct lastValues bucket times for rows
written before the field existed. Windows containing a failed attempt plus
a successful retry prefer the successful evaluation's analytics.
UI (alert detail page event stream):
- New 'Backfilled Buckets' column with an explanatory tooltip so users can
see when the alert job missed ticks and caught up
- Expanded details show a diagnostics line (query / notification timing)
- The group-cap row now states the cap explicitly ('Showing the top 50 of
N groups (firing first) — additional groups aren't fetched'), with
ALERT_EVALUATION_GROUPS_LIMIT moved to common-utils so the API cap and
the UI copy share one constant.
…o-expand (HDX-4997) - Query Duration and Webhook Duration are now dedicated columns on the evaluation event stream's parent rows (formatted ms, dash when not measured), replacing the diagnostics line in the expanded details. - Rows no longer auto-expand; the per-group breakdown and error details are purely click-driven.
…-4997) The evaluations query and chart were fed extendDateRangeToInterval's widened range, so a 5-minute pick on a 1m-interval alert showed 15 minutes of history. Chart, timeline strip, and event stream now all follow the picker's exact range; users widen the range themselves when they want more context.
The Evaluation Window column showed createdAt (the evaluation time, i.e. the bucket end) while the chart plots each value at the bucket start, so a row labeled 12:15 held the value the chart draws at 12:10. Render the latest evaluated bucket start instead (lastValues[last].startTime, falling back to createdAt - interval for failed evaluations with no lastValues), with a tooltip showing the full evaluated span to disambiguate backfilled windows. createdAt stays the row key and pagination cursor.
926b869 to
0cfe164
Compare
|
<!-- deep-review --> Deep Review🔴 P0/P1 — must fix
🟡 P2 — recommended
🔵 P3 nitpicks (9)
Reviewers (10): correctness, adversarial, security, testing, performance, api-contract, reliability, kieran-typescript, maintainability, agent-native. Testing gaps:
Coverage note: The environment had no working shell, grep/glob, or network, so review was performed by reading files and following the import graph; |
| enabled: alertId != null, | ||
| getNextPageParam: lastPage => | ||
| lastPage.hasMore ? lastPage.nextBefore : undefined, | ||
| enabled: alertId != null && startTime < endTime, |
There was a problem hiding this comment.
Sub-minute ranges disable history
When a valid custom range starts and ends within the same clock minute, flooring both bounds makes startTime equal endTime, so this guard disables the evaluations query and leaves the event stream empty while the chart renders the exact selected range.
Knowledge Base Used: HyperDX App Frontend (packages/app)
Summary
Makes the alert detail page's evaluation event stream (from #2798) production-grade:
Time-range-driven pagination with hard caps
GET /alerts/:id/evaluationsacceptsstartTime/endTime(epoch ms) with the span clamped to the 31d history retention window andlimitfixed at 200.(limit+1) × intervalof history — group-by alerts can have many rows per window and the$groupstage processes every matched row, so the scan is bounded, not just the returned page.nextBeforethat always advances past the scanned slice, so paging keeps progressing across gaps with no evaluations (e.g. alerting job downtime) instead of stalling.Per-group breakdown for group-by alerts
ALERT_EVALUATION_GROUPS_LIMIT(50, shared const in common-utils) withgroupsTotalreporting the pre-cap count — firing groups stay visible and high-cardinality group-bys can't produce unbounded responses. No extra scan cost (rows were already matched; only$groupretention changed).k/n groups firingsummary and expand on click into indented per-group child rows; the group-cap row states the cap explicitly ("Showing the top 50 of N groups (firing first) — additional groups aren't fetched").Evaluation analytics
analyticssubdoc:queryDurationMs(time-to-failure on query-error rows — ≈ the timeout forQUERY_TIMEOUT),webhookDurationMs(total notification delivery wall time incl. retries), andbackfilledBuckets(missed ticks caught up in this run). Optional field, no migration;backfilledBucketsis derived fromlastValuesfor pre-existing rows.Chart/table alignment fix
How to test on Vercel preview
N/A — needs the alerting job + seeded alert data. Covered by API integration tests and full-stack Playwright tests instead.
How this was tested
make ci-lint,make ci-unit— pass across all packagesmake dev-int FILE=checkAlerts.int,FILE=alertHistory.int,FILE=alerts.int,FILE=singleInvocationAlert.int,FILE=default.int— all pass (evaluations endpoint time-range clamping, bounded scan,nextBeforecursor, per-group breakdown + cap, analytics fields)make dev-e2e FILE=alerts): errored history segment + detail page specs passReferences