Skip to content

refactor(alerts): extract notification transports behind a registry - #2844

Open
jordan-simonovski wants to merge 3 commits into
mainfrom
jordansimonovski/alerts-notifications-module
Open

refactor(alerts): extract notification transports behind a registry#2844
jordan-simonovski wants to merge 3 commits into
mainfrom
jordansimonovski/alerts-notifications-module

Conversation

@jordan-simonovski

@jordan-simonovski jordan-simonovski commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Why

Notification delivery was a single if/else chain over WebhookService. Adding a new webhook service meant editing that chain; adding a whole new channel type meant editing it and everything around it.

This replaces the chain with a registry keyed on channel type first, webhook service second, so both kinds of extension become an added entry rather than an edit to shared code.

The double keying looks like unnecessary indirection given this repo has exactly one channel type today. That is deliberate and is the point of the change — it is the seam a downstream build extends without touching these lines. There is a comment in transports/index.ts saying so, aimed at the next person tempted to flatten it.

What changed

  • checkAlerts/notifications.ts is split into checkAlerts/transports/{types,slack,generic,index}.ts and deleted. The name is deliberately freed — a follow-up PR reuses it for the notification job and dispatcher contract, which is a different concern entirely.
  • New deliverToChannel(channel, message, ctx) resolves channel type then webhook service, throwing Unsupported channel type: <t> / Unsupported webhook service: <s>.
  • Webhook transports now take the populated channel rather than a bare IWebhook, so a transport can read per-channel fields without a signature change later.
  • WebhookResponseError carries the destination's HTTP status, replacing an as any on a plain Error. retry.ts reads .status unchanged.

Not a behaviour change

checkAlerts.int.test.ts (277) and renderAlertTemplate.int.test.ts (74) pass unmodified — that is the gate. webhooks.int.test.ts needed 3 assertion updates (.calls[0][0].url -> .channel.url), a direct consequence of the transport signature.

getWebhookFetchTimeoutMs is exported without a caller: the fetch wiring arrives with the PR that removes the per-event deadline, and landing it here would change behaviour this PR is asserting it does not change.

@changeset-bot

changeset-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 522dada

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 19, 2026 11:12pm
hyperdx-storybook Ready Ready Preview Aug 19, 2026 11:12pm

Request Review

@github-actions

github-actions Bot commented Aug 9, 2026

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 — 759 lines (bar: 30):
    • packages/api/src/tasks/checkAlerts/errors.ts
    • packages/api/src/tasks/checkAlerts/index.ts
    • packages/api/src/tasks/checkAlerts/notifications.ts
    • packages/api/src/tasks/checkAlerts/template.ts
    • packages/api/src/tasks/checkAlerts/transports/generic.ts
    • packages/api/src/tasks/checkAlerts/transports/index.ts
    • packages/api/src/tasks/checkAlerts/transports/slack.ts
    • packages/api/src/tasks/checkAlerts/transports/types.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: 9
  • Production lines changed: 767 (+ 78 in test files, excluded from tier calculation)
  • Critical-path lines changed: 759
  • Branch: jordansimonovski/alerts-notifications-module
  • 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 Aug 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR separates alert-notification delivery from template rendering and introduces registry-based transport and dispatcher seams while preserving synchronous delivery.

  • Moves generic and Slack webhook implementations into dedicated transport modules.
  • Resolves transports by channel type and webhook service.
  • Adds a typed webhook HTTP-response error.
  • Introduces a notification job and inline dispatcher contract.
  • Updates webhook tests and call sites for populated-channel arguments.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/api/src/tasks/checkAlerts/template.ts Replaces direct webhook dispatch with construction and synchronous dispatch of a NotificationJob.
packages/api/src/tasks/checkAlerts/notifications.ts Defines the notification job, dispatcher contract, default delivery function, and inline implementation.
packages/api/src/tasks/checkAlerts/transports/index.ts Adds channel-type and webhook-service registries with explicit unsupported-value errors.
packages/api/src/tasks/checkAlerts/transports/generic.ts Extracts generic webhook rendering, validation, metrics, retry, and HTTP error handling.
packages/api/src/tasks/checkAlerts/transports/slack.ts Extracts Slack webhook validation, delivery, and instrumentation.
packages/api/src/tasks/checkAlerts/errors.ts Adds a typed HTTP response error carrying the destination status code.
packages/api/src/routers/api/webhooks.ts Updates test-webhook delivery to pass a populated webhook channel to the extracted transports.

Sequence Diagram

sequenceDiagram
  participant Alert as Alert evaluation
  participant Template as renderAlertTemplate
  participant Dispatcher as NotificationDispatcher
  participant Registry as Transport registry
  participant Transport as Webhook transport
  participant Destination as Webhook destination

  Alert->>Template: Render notification
  Template->>Dispatcher: dispatch(NotificationJob)
  Dispatcher->>Registry: deliverToChannel(channel, message, ctx)
  Registry->>Registry: Resolve channel type
  Registry->>Registry: Resolve webhook service
  Registry->>Transport: Invoke selected transport
  Transport->>Destination: POST notification
  Destination-->>Transport: HTTP response
  Transport-->>Dispatcher: Resolve or reject
  Dispatcher-->>Template: Propagate result
Loading

Reviews (8): Last reviewed commit: "refactor(alerts): add a notification dis..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Scope: git diff 40ec0858 — alert notification delivery refactored into a two-level transport registry (transports/{types,slack,generic,index}.ts), a NotificationJob/NotificationDispatcher seam (notifications.ts), and a WebhookResponseError class (errors.ts); call sites in webhooks.ts and checkAlerts/index.ts and three test files updated.

Intent: Structural refactor of webhook notification dispatch; author asserts no behavior change (core alert integration tests unmodified).

✅ No critical issues found. Error propagation (InlineNotificationDispatcher awaits and rethrows → caller's executionErrors), SSRF/redirect handling (redirect: 'manual', 3xx → WebhookRedirectError), validateWebhookUrl, the Idempotency-Key/eventId derivation, and retry.ts's .status read are all preserved across the move. The updated call sites (webhooks.ts testChannel, transport signatures) are internally consistent.

🟡 P2 -- recommended

  • packages/api/src/tasks/checkAlerts/transports/__tests__/registry.test.ts:16 -- deliverToChannel is only exercised on its two throw paths; the new routing itself (Slack→slack, Generic/IncidentIO→generic, absent-service→Generic default) and deliverNotification's threading of populatedChannel/message/group into deliverToChannel have no positive-path coverage.
    • Fix: Add a test asserting each WebhookService dispatches to its expected transport and that deliverNotification forwards the job fields.
🔵 P3 nitpicks (4)
  • packages/api/src/tasks/checkAlerts/transports/generic.ts:90 -- getWebhookFetchTimeoutMs is exported and marked @public but has no caller and is not wired into the fetch() call, so webhook delivery still runs without a per-attempt timeout and the export reads as dead code.
    • Fix: Wire the value into fetch via an AbortSignal, or drop the export until the follow-up that consumes it lands.
  • packages/api/src/tasks/checkAlerts/transports/index.ts:50 -- channelTransports: Record<string, ChannelTransport> widens the key to string, discarding the channel-type union so a mistyped key compiles and resolves to undefined at runtime.
    • Fix: Key on the channel-type union (e.g. Record<PopulatedAlertChannel['type'], ChannelTransport>).
  • packages/api/src/tasks/checkAlerts/transports/index.ts:33 -- channel.channel.service ?? WebhookService.Generic sends a generic webhook for an absent service and throws for an unknown one, where the prior notifyChannel silently no-op'd both; unreachable today given the model's required+enum constraint, but a divergence the downstream seam would inherit.
    • Fix: Make the intended semantics for absent/unknown service explicit rather than falling through the ?? default.
  • packages/api/src/tasks/checkAlerts/transports/slack.ts:8 -- the slack transport imports webhookDeliveryCounter, webhookDeliveryDuration, and logBlockedWebhookDelivery from generic.ts, coupling the two sibling transports through the module meant to be generic-specific.
    • Fix: Move the shared instrumentation and log helper into a neutral module (e.g. types.ts or a dedicated metrics module).

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

Testing gaps:

  • No positive-path coverage for the transport registry routing or deliverNotification (see P2).
  • checkAlerts/index.ts retains an import of handleSendGenericWebhook, whose signature changed to require a WebhookChannel; confirm any remaining call site passes { type: 'webhook', channel } rather than a bare IWebhook.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 229 passed • 1 skipped • 839s

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

Tests ran across 4 shards in parallel.

View full report →

pulpdrew
pulpdrew previously approved these changes Aug 18, 2026

@pulpdrew pulpdrew 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.

LGTM, great refactor

Comment on lines +25 to +42
// Webhook delivery is the last (and most failure-prone) hop of an alert. It
// happens in the background task, so failures only show up in logs today.
// `service` and `outcome` are bounded enums (see agent_docs/observability.md).
export const webhookDeliveryCounter = getCounter(
'hyperdx.alerts.webhook_deliveries',
{
description:
'Count of alert webhook delivery attempts, labeled by service (slack, generic, incidentio) and outcome (success, error).',
},
);
export const webhookDeliveryDuration = getHistogram(
'hyperdx.alerts.webhook_delivery.duration_ms',
{
description:
'Duration of an alert webhook delivery attempt, labeled by service.',
unit: 'ms',
},
);

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.

nit: Would these fit better in a shared file, since they aren't specific to generic webhooks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. I'll do this in a follow-up 🫡

template.ts mixed Handlebars templating with the HTTP transport for
Slack/generic/incident.io webhooks. Move the transport (notifyChannel,
handleSendSlackWebhook, handleSendGenericWebhook, sendGenericWebhook,
delivery metrics) into tasks/checkAlerts/notifications.ts unchanged, so
the upcoming multi-channel dispatch work lands in a focused module.
@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/alerts-notifications-module branch from c6bd23d to 50a5db0 Compare August 19, 2026 12:52
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