Repository navigation
Conversation
A resolve is sent once, when an alert recovers. If that webhook delivery failed, the next evaluation saw the alert as OK and never sent it again, so the incident stayed open in the receiving tool. A failed resolve is now stored in a new PendingNotification collection and sent again before the alert's evaluation on later ticks, with the same Idempotency-Key, backing off from 1 to 30 minutes. It is dropped when the group fires again or its webhook is deleted, held while the alert is silenced, and after 24 hours recorded as a WEBHOOK_ERROR on the resolved window's evaluation history. Delivery failure logs now include the error's cause chain, so "fetch failed" shows the underlying DNS, connect or TLS error. Ref: HDX-6020
🦋 Changeset detectedLatest commit: aff42fe 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.
|
Duplicate code analysisNo new duplicate code introduced by this PR. jscpd compared this branch against |
E2E Test Results✅ All tests passed • 467 passed • 1 skipped • 983s
Tests ran across 4 shards in parallel. |
🔴 Tier 4 — CriticalTouches 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:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
| } | ||
|
|
||
| if ((alert.silenced?.until?.getTime() ?? 0) > now.getTime()) { | ||
| return 'skipped_silenced'; |
There was a problem hiding this comment.
🔵 minor — Resends call deliverNotification directly and skip the NotificationDispatcher
Resends go straight to deliverNotification, but notifications.ts says a NotificationDispatcher owns delivery. fireChannelEvent also accepts a dispatcher (template.ts:623). So the downstream queueing dispatcher the PR description mentions would be skipped for resends, and so would any dedup or rate limiting it adds. Fix: route resends through the same dispatcher used for first sends, for example by passing it into resendPendingNotifications.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
PR Review
2 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 2 minor 1 posted as inline comment(s) on the changed lines. 1 listed below. Findings outside the changed lines1 minor
Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides. |
Deep Review✅ No critical issues found. No P0/P1 defects surfaced: the resend path is wrapped so a single bad record cannot abort evaluation ( 🟡 P2 — recommended
🔵 P3 nitpicks (2)
Reviewers (10): correctness, reliability, adversarial, kieran-typescript, testing, maintainability, performance, api-contract, data-migrations, project-standards. Testing gaps:
No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond |
Summary
When an alert recovers, the check-alerts task sends one resolve notification. If that webhook delivery failed, the next evaluation saw the alert as OK and had nothing to resolve. The incident stayed open in the receiving tool (incident.io, PagerDuty via webhook, etc.) until someone closed it by hand. Firing notifications don't have this problem, because ALERT is sent again on every evaluation.
This PR sends a failed resolve again until it is delivered:
PendingNotificationcollection, one record per alert, channel and group (keyed byeventId). Failures before dispatch (deleted webhook, notification cap, unknown mention) are not stored, since sending them again cannot succeed. Failed ALERT notifications are not stored either.CheckAlertTaskloads the pending records for all alerts in one query. Before each alert is evaluated, its records are sent again with the stored message and the sameIdempotency-Key. Running first means a resolve reaches the receiver before any new ALERT. Retries back off from 1 to 30 minutes, independent of the alert's interval, so a 1d alert does not wait a day between tries.WEBHOOK_ERRORis appended to the ERROR history row of the window that resolved, so it shows in the alert's evaluation history.serialize-errordropscause, which is where undici puts the real reason behindfetch failed. A newserializeErrorWithCausekeeps the cause chain andAggregateError.errors. It is used in the webhook transport, the dispatch-failure logs andwithRetry.hyperdx.alerts.notification_resendswithoutcome(delivered, failed, expired, superseded, webhook_missing, skipped_silenced), plus logs for each attempt and for a resolve that was given up on.The re-send loop goes through new
AlertProvidermethods, like the other state writes. The default provider implements them on Mongo. The model lives undermodels/and is reached from the API process through the same import chain as the providers, so Mongoose builds its indexes there; the worker runs withautoIndex: false.Not in this PR:
dispatch()before delivery, so it never reports a failure here. It needs a separate change to store failed resolves from the queue.How to reproduce
Starting point: full stack with
yarn dev. This needs the API and the alert task.On
main:nc -lor a request bin you can disable.On this branch:
Idempotency-Keyheader as the failed attempt.pendingnotificationsMongo collection is empty again.Testing
pendingNotifications.int.test.ts(new) covers:Idempotency-Key;checkAlertsTask.test.tschecks that pending records are loaded once and re-sent beforeprocessAlert, and that a failed load does not stop evaluation.serializeErrorWithCause.make ci-lint,make ci-unitandmake dev-int FILE=checkAlerts/; all pass.References