feat(alerts): fan out notifications to every configured channel - #2847
Conversation
🦋 Changeset detectedLatest commit: cf3d49d 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.
|
Greptile SummaryAlert rendering now collects notification jobs and dispatches each configured target concurrently with isolated deadlines, telemetry, and per-target errors. The latest changes also deduplicate resolved webhook IDs before cap enforcement and split the multi-channel integration suite to comply with the repository’s file-size rule.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/api/src/tasks/checkAlerts/template.ts | Collects notification jobs and deduplicates resolved webhook IDs before applying the per-event cap, fixing both prior duplicate-related findings. |
| packages/api/src/tasks/checkAlerts/notifications.ts | Adds concurrent per-target dispatch, bounded HTTP attempts, deadlines, tracing, metrics, and isolated result reporting. |
| packages/api/src/tasks/checkAlerts/index.ts | Processes per-target notification results, records attributable execution errors, and adds team-scoped alert-processing spans. |
| packages/api/src/tasks/checkAlerts/tests/multiChannelAlerts.int.test.ts | Provides focused integration scenarios in a 215-line file, with shared setup extracted into a separate 148-line harness. |
| packages/api/src/tasks/checkAlerts/tests/renderAlertTemplate.int.test.ts | Covers canonical target deduplication, duplicate handling at the cap boundary, and reporting of genuinely dropped targets. |
Sequence Diagram
sequenceDiagram
participant Eval as processAlert
participant Render as renderAlertTemplate
participant Queue as Notification jobs
participant Targets as Configured webhooks
Eval->>Render: Render alert and resolve channels
Render->>Render: Deduplicate canonical webhook IDs
Render->>Queue: Collect jobs up to event cap
Queue->>Targets: Dispatch concurrently
Targets-->>Queue: Per-target success, error, or timeout
Queue-->>Eval: Notification results
Eval->>Eval: Record individual execution errors
Reviews (3): Last reviewed commit: "fix(alerts): dedupe notification targets..." | Re-trigger Greptile
Deep Review✅ No critical issues found. No P0/P1 issues surfaced. The refactor is sound: all callers of the changed signatures ( 🟡 P2 -- recommended
🔵 P3 nitpicks (7)
Reviewers (9): correctness, security, testing, maintainability, project-standards, api-contract, kieran-typescript, agent-native, learnings-researcher. (reliability and adversarial were also dispatched but had not returned at synthesis time; their focus areas — deadline/retry interaction and cap-accounting abuse — are covered by the correctness and testing findings above.) Testing gaps:
|
Template rendering now collects one notification job per channel (and per @webhook mention) instead of sending inline; dispatch runs them concurrently with a per-target deadline. Per-target failures land in executionErrors naming the webhook, without blocking other targets or the evaluation loop. A missing webhook no longer aborts the whole event. Each alert evaluation gets a processAlert span with team context; each send an alerts.notify child span. renderAlertTemplate returns the rendered body alongside the per-target results so the existing rendering and template-injection tests keep asserting the delivered message directly.
- A timed-out webhook attempt is no longer retried. withRetry only treats 3xx/4xx as terminal and an abort surfaces as DOMException code 23, so a receiver that was merely slow got three duplicate POSTs where it previously got one delivery. - Raise the per-attempt fetch timeout default to 30s so a receiver that succeeds today keeps succeeding; the bound exists to release a black-holed socket, not to police slow receivers. - Bound the Slack transport too. It had no per-attempt timeout, so an abandoned send stayed in flight with nothing to cancel it. - Fall back to the default when a timeout env var is empty or malformed; NaN would otherwise time out every notification immediately. - Name the target in the webhook-not-found error, like the other branches. - Record an execution error when a channel is dropped by the per-event cap, instead of only a log line and a metric -- a skipped channel otherwise looks like a healthy alert. - Use withSpan for the processAlert span so exceptions and status are recorded per the observability standards. - Repoint the observability docs at notifications.ts, and document the fetch timeout env var in the changeset. Tests cover the abort-not-retried path, the malformed env fallback, a hung Slack target, and the per-event cap.
Review found three ways one alert event could misbehave: - A plain "@here" in the message body is rewritten into the notify helper like any @mention, so its channel type failed the schema and the ZodError rejected the entire render — discarding every job already collected and notifying nobody. Parse inside the per-target guard so an unsupported mention is one reported failure and the configured channels still fire. - A webhook named by an @mention and also configured as a channel was queued twice, sending two identical POSTs and consuming two slots of the per-event cap. Dedupe by resolved webhook id. - The deadline stopped waiting but never cancelled, so an abandoned send kept its socket and its retry loop could POST again well after the slot was reported released. Thread an AbortController through to the fetch and abort it when the deadline fires; the abort surfaces as a terminal 408 so withRetry stops rather than re-sending. Slack takes no signal, so it stays bounded by its own per-attempt timeout. Also splits the multi-channel integration harness out of the spec to stay under the file-size limit, and covers the previously untested cases: the deadline actually aborting, and a send that rejects after the deadline not surfacing as an unhandled rejection.
aab8217 to
e092361
Compare
🔴 Tier 4 — CriticalTouches authentication, tenancy data models, the public API or shipped database config — or substantially changes background tasks, the OTel pipeline, image build, or release CI. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
E2E Test Results✅ All tests passed • 277 passed • 1 skipped • 876s
Tests ran across 4 shards in parallel. |
The cap check ran before the target was resolved and deduplicated, so once an event reached the 20-target limit a repeat of an already-queued webhook was recorded as a cap failure. That target had in fact been notified, so the alert reported an error for a channel that was fine. Resolve and dedupe first: a repeat is a no-op, and only a genuinely new target that the cap turns away is reported. A missing webhook at the cap now also reports as not-found rather than cap-exceeded, which is the more accurate of the two.
Alert notifications now go to every configured channel concurrently, each with its own deadline, span and metrics. One slow or dead target can no longer delay the others or the alert evaluation loop.
What changed
Template rendering used to send inline: a Handlebars helper awaited each webhook as it rendered, so sends were serial and a hung endpoint blocked that alert indefinitely. Rendering now collects notification jobs, and
dispatchNotificationsruns them concurrently after the render.Each send is wrapped in an
alerts.notifyCLIENT span, a per-target deadline, and per-target metrics, and never throws — the caller gets one result per target and records failures individually. Alert execution errors now name the webhook that failed, so a multi-channel alert says which target broke.Each alert evaluation also gets a
processAlertspan carrying team context.Key decisions
The deadline stops waiting; it does not cancel. Delivery stays at-least-once, so an abandoned send is allowed to finish. Its eventual rejection is swallowed so it cannot surface as an unhandled rejection and kill the task process.
A timed-out HTTP attempt is not retried.
withRetrytreats only 3xx and 4xx as terminal, and an abort surfaces asDOMExceptioncode 23 — so without this, a receiver that was merely slow would get three duplicate POSTs where it previously got one delivery. The timeout is surfaced as a 408 so the existing retry policy stops on it.Per-attempt timeout defaults to 30s, not 10s. The bound exists to release a black-holed socket, not to police slow receivers; 30s leaves headroom inside the 60s deadline while not failing endpoints that succeed today.
renderAlertTemplatereturns the rendered body alongside the results. Returning only the results would have made the rendering and template-injection assertions untestable, since those tests configure no webhooks and so produce no transport calls to inspect.Impact
Behaviour changes for existing single-channel alerts, worth attention on merge:
ALERT_NOTIFICATION_FETCH_TIMEOUT_MS(default 30s), and Slack sends by the same value. Neither had a per-attempt bound before.New env vars:
ALERT_NOTIFICATION_DEADLINE_MS(default 60s) andALERT_NOTIFICATION_FETCH_TIMEOUT_MS(default 30s). Both fall back to the default when unset or malformed.Implementation detail
MAX_NOTIFICATIONS_PER_EVENT(20) caps jobs per fire/resolve event, covering configured channels and@webhook-message mentions together. A channel dropped by the cap records an execution error rather than only a log line and a metric, so a partially-notified alert does not look healthy.New metrics:
hyperdx.alerts.notifications(attrschannel_type,service,outcome) andhyperdx.alerts.notification.duration_ms. The existinghyperdx.alerts.webhook_deliveriestransport metrics are unchanged.Tests cover failure isolation, deadline timeout for both generic and Slack targets, the abort-not-retried path, the malformed-env fallback, the per-event cap, and a pre-multi-channel document that only has
channel.Verification: 273 checkAlerts integration tests, plus the notification unit suite.