Skip to content

fix(api): fetch a grouped alert's example log lines once, not once per group - #3111

Open
jordan-simonovski wants to merge 2 commits into
mainfrom
jordansimonovski/alert-sample-query-per-evaluation
Open

fix(api): fetch a grouped alert's example log lines once, not once per group#3111
jordan-simonovski wants to merge 2 commits into
mainfrom
jordansimonovski/alert-sample-query-per-evaluation

Conversation

@jordan-simonovski

@jordan-simonovski jordan-simonovski commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

When a saved-search alert fires, the notification includes a handful of example log lines — the rows that actually tripped the threshold — so you can see what happened without opening the app. Fetching those lines means a second question to ClickHouse, separate from the one that counted the rows and decided to fire.

An alert can also be grouped. Grouped by service, one run sends a separate notification per breaching service: one for checkout, one for search, one for billing. Each message was built independently, and the example-lines query was being made while building each one — so ten breaching services meant asking ClickHouse ten times. It really was the same question every time: that query filters by the saved search and the evaluation window, and never by the group. Ten queries, ten identical answers, ten scans of the same data. Now it asks once and all the messages for that run share the answer.

The one wrinkle is that a run which is catching up on skipped ticks evaluates several windows at once, and each window genuinely needs its own example lines. So this is "once per window" rather than "once per run", which is why there's a small lookup keyed on the window start instead of a single saved value. A failed fetch is shared the same way a successful one is: the message falls back to carrying no example lines, exactly as it did before, rather than retrying a failing query once per group.

Two things not to read into this. An ungrouped alert is completely unaffected, because it only ever asked once — if you are looking at a slow single-group alert, this changes nothing for it. And the query stays the expensive part of sending a notification either way: it fetches raw rows in a sort order, so unlike the count that decides whether to fire, it can never be answered from a materialised view. This makes it happen fewer times, not faster.

One thing this deliberately leaves alone: because the query carries no group predicate, a grouped alert's message can quote lines belonging to a different group than the one it is reporting on. There is a standing TODO about that above the query. It is a correctness wart rather than a performance one, and fixing it would mean going back to a query per group — the opposite of this change — so it wants a deliberate decision rather than a quiet fix here.

Implementation detail

The fetch moved out of renderAlertTemplate into an exported fetchSampleLines, and the render now takes an optional provider that the evaluation supplies as a memoised closure keyed on the window. Callers that don't pass one still fetch inline, so the query itself has a single code path. The evaluation also hands over the alias WITH clauses it had already resolved from the same saved search and source, which the render had been recomputing per group.

The new integration test asserts that a two-group evaluation issues one CSV query, which is the only CSV query an evaluation makes. Without the fix it fails with two — identical SQL, identical parameters, identical window.

The five sample rows quoted in a notification body come from a query carrying
the saved search's filter and the evaluation window, but no group predicate.
It sat in the per-group render, so a grouped alert re-ran the identical query
for every group. Hoist it to the evaluation, keyed by window so backfilled
buckets still fetch their own, and reuse the alias clauses the evaluation
already resolved.
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Sep 11, 2026 4:59am UTC
1 Skipped Deployment
Project Deployment Actions Updated
hyperdx-storybook Ignored Ignored Preview Sep 11, 2026 4:59am UTC

Request Review

@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1f8c991

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

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

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Sep 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI.

Why this tier:

  • Background tasks or delivery pipeline substantially modified — 215 lines (bar: 30):
    • packages/api/src/tasks/checkAlerts/index.ts
    • packages/api/src/tasks/checkAlerts/template.ts

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 2
  • Production lines changed: 215 (+ 83 in test files, excluded from tier calculation)
  • Branch: jordansimonovski/alert-sample-query-per-evaluation
  • Author: jordan-simonovski

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR memoizes saved-search sample-line queries by evaluation-window start, allowing grouped notifications for the same window to share one ClickHouse query while preserving separate queries for backfilled windows.

  • Extracts sample-line querying into a reusable helper with the existing empty-result fallback on failure.
  • Reuses alias clauses already computed during alert evaluation.
  • Adds integration coverage confirming that two grouped notifications issue one CSV query.
  • Adds a patch changeset documenting the optimization.

Confidence Score: 5/5

The PR appears safe to merge; no actionable correctness, security, or repository-rule issues remain.

The per-window cache stores the in-flight promise before grouped renders can request another fetch, preserves distinct keys for backfilled windows, and shares both successful and failed results as intended. The latest changes only clarify the changeset wording.

Important Files Changed

Filename Overview
packages/api/src/tasks/checkAlerts/index.ts Adds a per-window promise cache and supplies the shared sample-line provider to grouped notification rendering.
packages/api/src/tasks/checkAlerts/template.ts Extracts sample-line retrieval into a reusable helper while preserving inline fallback behavior and error handling.
packages/api/src/tasks/checkAlerts/tests/checkAlerts.int.test.ts Verifies that two breaching groups in one evaluation window produce only one sample-row CSV query.
.changeset/alert-sample-rows-per-window.md Documents the grouped-alert query reduction as an API patch release.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Evaluate saved-search window] --> B[Compute alias clauses]
  B --> C[Evaluate grouped counts]
  C --> D{Group sends notification?}
  D -->|Yes| E[Request sample lines for window]
  E --> F{Promise cached by window start?}
  F -->|No| G[Query ClickHouse once]
  F -->|Yes| H[Reuse cached promise]
  G --> I[Render group notification]
  H --> I
Loading

Reviews (2): Last reviewed commit: "docs: rewrite the sample-rows changeset ..." | Re-trigger Greptile

@jordan-simonovski jordan-simonovski changed the title fix(api): fetch a saved-search alert's sample rows once per window fix(api): fetch a grouped alert's example log lines once, not once per group Sep 11, 2026
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 357 passed • 1 skipped • 1379s

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

Tests ran across 4 shards in parallel.

View full report →

@github-actions

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

This is a well-contained memoization refactor. The extracted fetchSampleLines preserves the original query construction, truncation, escaping, and empty-on-failure fallback. The per-window Map<number, Promise<string>> is created once per processAlert (not per group) and its synchronous get-or-set cannot race, since grouped notifications are dispatched sequentially via await inside for loops. For the SAVED_SEARCH task type the discriminated union guarantees source and savedSearch are non-null, and the provider closure only fires inside renderAlertTemplate's Log/Trace-guarded branch, so the pre-try source.kind access is not reachable with an invalid source. The select fallback change (isEventSource && source.defaultTableSelectExpression) is behavior-preserving on the guarded event-source path. The shared-failure semantics (one failed fetch yields '' for every group in the window) matches the prior per-group outcome because the query inputs are identical across groups.

🟡 P2 -- recommended

  • packages/api/src/tasks/checkAlerts/__tests__/checkAlerts.int.test.ts:6516 -- the new test only exercises two groups in a single window; the per-window keying that makes a backfill/catch-up evaluation fetch once per window is untested, so a regression that collapsed all windows to one key (or split one window into many) would pass.
    • Fix: Add an integration test where a catch-up evaluation spans two or more windows and assert the CSV sample query count equals the window count.
🔵 P3 nitpicks (2)
  • packages/api/src/tasks/checkAlerts/__tests__/checkAlerts.int.test.ts:6588 -- the test asserts only the CSV query count and history state, not that each group's rendered message body actually contains the shared sample rows, so a regression dropping the shared result for some groups would not be caught.
    • Fix: Assert that the delivered notification bodies for both groups include the sample log lines.
  • packages/api/src/tasks/checkAlerts/template.ts:592 -- fetchSampleLines now has two call sites with divergent argument sets (the evaluation closure passes aliasWith; the inline fallback omits it and recomputes), which can silently drift as the signature evolves.
    • Fix: Route both call sites through one argument builder, or document at the fallback why aliasWith is intentionally omitted.

Reviewers (7): correctness, adversarial, testing, performance, reliability, kieran-typescript, maintainability.

Testing gaps: Multi-window backfill fetch-count is not covered; the new test asserts query count but not message-body content.


try {
const withClauses =
aliasWith ??

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.

🔵 minoraliasWith ?? compute(...) can't tell "no aliases" from "not supplied", so the advertised reuse doesn't happen for most saved searches

aliasMapToWithClauses returns undefined when the saved search's SELECT has no AS aliases (packages/common-utils/src/core/utils.ts:1357), which is the common case — e.g. the select: 'Body' fixture in this file's own test setup. aliasWithClauses in index.ts:1161 is then undefined, so fetchSampleLines re-runs computeAliasWithClauses (another full renderChartConfig of the saved search's SELECT) for every window, exactly the recomputation index.ts:1143-1144 says it is avoiding. Make the absence explicit — e.g. have computeAliasWithClauses return [] rather than undefined for "no aliases", or pass { aliasWith } as a present-but-undefined marker and switch to an 'aliasWith' in opts check.

fetchSampleLines({
aliasWith: aliasWithClauses,
clickhouseClient,
endTime: fns.addMinutes(startTime, windowSizeInMins),

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.

🔵 minor — The sample window's end time is computed independently of the notification's end time

fns.addMinutes(startTime, windowSizeInMins) is now written twice — once for the sample query (index.ts:1310) and once for the message's own endTime (index.ts:1376) — so the rows quoted in the body and the "Time Range (UTC)" line printed above them are two hand-synced sources of truth. Compute the end time once per startTime (e.g. in trySendNotification, before the fireChannelEvent call) and pass the same value into both sampleLinesFor and fireChannelEvent.


// The sample rows quoted in the message body carry no group predicate, so
// every group's notification used to re-run the identical query.
it('fetches the message body sample rows once for all groups in a window', async () => {

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.

🔵 minor — New test never exercises the per-window keying, the one subtle part of the change

The test covers a single window with two groups, so a regression that drops the sampleLinesByWindow key entirely (one shared promise per evaluation) still issues exactly one CSV query and passes — while backfilled notifications would quote another window's rows. Add a backfill case (a missed tick, as the 22:16 multi-window test above at line 6458 does) asserting two CSV queries whose query_params carry distinct window bounds, and assert both groups' slack.postMessageToWebhook bodies actually contain the shared sample rows rather than only counting queries.

@github-actions

Copy link
Copy Markdown
Contributor

PR Review

3 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 3 minor

3 posted as inline comment(s) on the changed lines.


Severity is the reviewer's own estimate and is used for ordering, not filtering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants