feat(alerting): Opsgenie adapter (plan 26) by duyet · Pull Request #2248 · chmonitor/chmonitor · GitHub
Skip to content

feat(alerting): Opsgenie adapter (plan 26) - #2248

Merged
duyet merged 6 commits into
mainfrom
advisor/26-opsgenie-adapter
Jul 4, 2026
Merged

feat(alerting): Opsgenie adapter (plan 26)#2248
duyet merged 6 commits into
mainfrom
advisor/26-opsgenie-adapter

Conversation

@duyet

@duyet duyet commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds an Opsgenie notification adapter with parity to the existing Slack/Discord/Telegram/PagerDuty pattern (plan 26):

  • Pure adapter (apps/dashboard/src/lib/health/adapters/opsgenie.ts): buildOpsgenieBody(payload) builds an Opsgenie Alert API v2 create-alert body — severity → priority (critical→P1, warning→P2), a stable alias (chmonitor:{hostId}:{metric}) so repeat firings collapse to one alert, host+metric tags, string-only details, runbook links in description. opsgenieAdapter.detect matches api.opsgenie.com / api.eu.opsgenie.com. No network/auth in this layer.
  • Registry: opsgenieAdapter registered in adapters/index.ts (ADAPTERS, before the generic fallback); types/functions re-exported.
  • Server config: getServerOpsgenieConfig() in server-alert-config.ts reads HEALTH_ALERT_OPSGENIE_API_KEY (empty ⇒ null, fail-open) and HEALTH_ALERT_OPSGENIE_REGION (us/eu, default us). AlertSettings shape unchanged.
  • Dispatch (opsgenie-dispatch.ts): POSTs create on trigger / closes the alias on recovery, with the GenieKey auth header and region-correct base URL, through the same SSRF guard (validateHostUrl) other outbound fetches use. Never throws — fails open into the sweep.
  • Sweep wiring (server-sweep.ts): the per-finding dedup decision now fans out to webhook and Opsgenie independently; each channel's failure is isolated, and "notified" is only persisted once at least one channel delivered (so a fully-failed sweep still retries).
  • Settings UI: health-settings-dialog.tsx gets an Opsgenie section with a configured/region status badge and a "Send test" button, backed by a new GET/POST /api/v1/health/opsgenie-test route (POST write-gated like the webhook proxy).

Note on the settings-UI interpretation

The plan's done-criteria says "Opsgenie key field." The Opsgenie API key
is documented (server-alert-config.ts) as a server-only secret that must
never round-trip to the browser — a localStorage-backed key input (like the
webhook URL field) would either be non-functional or a secret-leak path given
that design. Implemented instead as a read-only configured/region status +
a server-side test-send button that exercises the server's own env-configured
key. Flagging this explicitly rather than silently deviating from the
checklist wording.

Note on server-sweep.ts

This branch also carries the (uncommitted, pre-existing) Opsgenie dispatch
wiring into runHealthSweep — split into its own commit since other agents
are working on server-sweep.ts / server-alert-config.ts in parallel for
plan 30's routing model. Expect a rebase.

Known coverage gap

runHealthSweep's new if (opsgenieConfig) fan-out branch isn't covered by
an integration test — health-sweep.test.ts only tests CRON_SECRET auth,
and the pre-existing webhook fan-out isn't integration-tested there either.
The Opsgenie dispatch unit itself is fully covered by opsgenie-dispatch.test.ts.

Verification

$ cd apps/dashboard && bun run type-check
$ tsc --noEmit
EXIT:0

$ cd apps/dashboard && bun run build
... (prerender all routes) ...
EXIT:0

$ cd apps/dashboard && bun test src/lib/health/adapters --isolate
37 pass, 0 fail, 6 snapshots, 95 expect() calls

$ cd apps/dashboard && bun test src/lib/health/server-alert-config.test.ts --isolate
18 pass, 0 fail, 21 expect() calls

$ bun run lint   # (root — `cd apps/dashboard && bun run lint` has no such script;
                 #  root biome lint covers the whole monorepo including apps/dashboard)
Checked 1945 files in 1060ms. No fixes applied.

Additional tests run (not in the plan's block, covering the new UI/route + dispatch wiring):

  • bun test src/routes/api/v1/health/opsgenie-test.test.ts --isolate — 6 pass
  • bun test src/lib/health/opsgenie-dispatch.test.ts src/routes/api/cron/__tests__/health-sweep.test.ts --isolate — 18 pass

Test plan

  • type-check, build, targeted unit tests, lint all green
  • buildOpsgenieBody snapshot + severity/alias/tags/details/description tests
  • getServerOpsgenieConfig env-reader tests (empty/whitespace/region normalization)
  • Opsgenie dispatch transport tests (create/close routing, SSRF guard, auth header, failure handling)
  • New settings-route tests (status GET, auth-gated test-send POST, 400/502 paths)
  • Manual: set HEALTH_ALERT_OPSGENIE_API_KEY locally and confirm "Send test" creates a real Opsgenie alert (not run — no sandbox API key available)

https://claude.ai/code/session_01XpfaNdJZh4Rtr6cQm4DYqa

@github-actions github-actions Bot added the app: dashboard Changes to the apps/dashboard TanStack Start app label Jul 3, 2026
@duyet
duyet enabled auto-merge (squash) July 3, 2026 16:59

duyet commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

duyet and others added 5 commits July 4, 2026 12:12
…an 26)

Mirrors the PagerDuty adapter: severity→priority mapping (P1/P2/P3), a
stable chmonitor:{hostId}:{metric} alias so repeat firings collapse to one
Opsgenie alert, host/metric/chmonitor tags, string-only details, runbook
URLs in the description, and detect() for api(.eu).opsgenie.com. Registered
in ADAPTERS before the generic-json fallback.

Co-Authored-By: duyetbot <bot@duyet.net>
HEALTH_ALERT_OPSGENIE_API_KEY (default '') and HEALTH_ALERT_OPSGENIE_REGION
('us'|'eu', default 'us'), mirroring the getServerThresholdOverrides
companion-function style. Returns null when unset so Opsgenie delivery is
opt-in and fails open. Kept separate from getServerAlertConfig's
AlertSettings shape, which is shared with client localStorage and asserted
deeply by its own tests — an API key is a server-only secret.

Co-Authored-By: duyetbot <bot@duyet.net>
dispatchOpsgenie(payload, {apiKey, region}) applies the GenieKey auth header
and region-appropriate Alert API base URL, POSTing a create request on
trigger and a close-alias request on recovery. Every outbound URL runs
through validateHostUrl first (same SSRF guard as the health webhook proxy
and browser-connections), and the function never throws — a delivery
failure returns false so callers (the health sweep) stay fail-open.

Co-Authored-By: duyetbot <bot@duyet.net>
Fans the sweep's per-finding dedup decision out to both the webhook and
Opsgenie channels independently -- each channel's delivery failure is
isolated (never throws into the sweep) and "notified" is only persisted
once at least one channel actually delivered, so a fully-failed sweep
still retries next run.

Co-Authored-By: duyetbot <bot@duyet.net>
…lan 26)

The Opsgenie API key is a server-only env secret (HEALTH_ALERT_OPSGENIE_API_KEY)
that must never round-trip to the browser, unlike the webhook URL the user
types in -- so instead of a key input field, the settings dialog shows a
read-only configured/region badge (GET /api/v1/health/opsgenie-test) and a
"Send test" button that fires a real dispatch through the server's own config
(POST /api/v1/health/opsgenie-test, gated the same way as the webhook proxy).

Co-Authored-By: duyetbot <bot@duyet.net>
@duyet
duyet force-pushed the advisor/26-opsgenie-adapter branch from 076eeef to 50dc3b4 Compare July 4, 2026 05:18
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

☁️ Cloudflare Preview Deployment

App Preview URL
Dashboard https://preview.dash.chmonitor.dev
MCP https://preview.dash.chmonitor.dev/api/mcp
Landing https://preview.chmonitor.dev
Docs https://preview.docs.chmonitor.dev
Property Value
Commit b55ecfb
Deployed at 2026-07-04T05:25:12.855Z

Previews are automatically updated on every push to this PR.
Unchanged apps keep their previous preview (path-filtered deploys).

@duyet
duyet merged commit 42e793d into main Jul 4, 2026
18 of 19 checks passed
@duyet
duyet deleted the advisor/26-opsgenie-adapter branch July 4, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: dashboard Changes to the apps/dashboard TanStack Start app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant