[WSAPI-7] Session health alerting #6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/WSAPI-7"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Ticket
WSAPI-7 Session health alerting
A dead Wealthsimple session previously only logged, so a missed or undelivered alert read identically to
"nothing happened" — the gap BTAPI-62/BTAPP-53 closed on budget-tracker-api's side, now closed here.
What this does
lib/alert-webhook(new):sendAlert({ kind, body })— one POST toConfig.alertWebhookUrl, bounded byAbortSignal.timeout(5000), and it never rejects. Every failure mode (unset target, transport error, timeout, non-2xx) is logged and swallowed, because a push that replaced theSessionDeadErrorwould leave sync running against a session that is already dead.killSessioncalls it on the transition from live to dead, gated on the existingmarkDead()transition boolean — so it fires exactly once per transition, not once per failed call. That guarantee is structural rather than a counter kept in the caller:assertUsable()short-circuits every call once the document readsdead, sokillSessiononly ever runs once anyway.Config.alertWebhookUrlis optional and deliberately un-asserted. Unset is the expected state until the receiver exists, and the service boots normally with it empty — alerting degrades to the server log line plusGET /status.kindis a closed const-array-derived union (ALERT_KINDS→AlertKind, withSESSION_DEAD_KINDas the one shared value), mirroringlib/sync-status'sJOBS/Job, with the wire value pinned by a test: it is the routing key a consumer in another repo binds to, so a silent change would break that consumer rather than fail a test.Scope notes for the reviewer
Notifications.sendAlertin-process, so there is nothing to call — so this ships the generic, configurable caller with the target left unset and documented. BTAPI-80 ("Add alert-webhook receiver so wealthsimple-api can push dead-session alerts") is blocked-by this ticket and builds the receiver. The target is a plain URL rather than a consumer-specific integration so home-assistant-api can point the same variable at its ownnotifyroute instead.bodyis deliberately condition-only —Wealthsimple sync is paused — reconnection needed.— and names no app. The sender cannot know which consumer is listening, so the per-app call to action belongs to the receiver, which knows what it is and routes onkind. The ticket explicitly required not hardcoding to one consumer.sessionStatus, per-joblastSyncAtviaGET /status) was already satisfied by WSAPI-5/6, so there is no route or shape change here.Security
The webhook URL is never logged. Webhook endpoints routinely embed a secret in their path (Slack, Discord, HA
notifytargets), and Bun's connection-levelfetcherrors carry an own enumerablepathholding the full request URL — which pino's error serializer copies straight through, so loggingerras-is would print the credential-bearing URL atwarnon the most likely real-world failure (host down, DNS, unreachable). Errors are therefore rebuilt field-by-field by an exporteddescribeError, which also cannot throw on anything handed to it, since it is called on the way to a halt. Only the URL's origin reaches the log.Verification
bun run type-check,bun run lint,bun run format:check— all clean.bun test— 311 pass, 1 fail, 312 tests across 35 files.lib/config/.test.ts > the Wealthsimple credentials default to empty strings, never undefined, caused byWS_EMAILbeing set in this machine's gitignored.env(Bun auto-loads it). It was proven to fail identically on amainworktree with the same.envsymlinked in, andlib/config/is untouched by this branch.ALERT_WEBHOOK_URL=https://example.test/hook bun test. That env-set run is worth running: the alert is afetchlike any other, so any test file whose stubbed request counting could be perturbed by it pinsConfig.alertWebhookUrlexplicitly — and this proves that holds with the variable genuinely set.lib/alert-webhook/.test.ts(one POST with the exact payload; origin trimming including secret-bearing URLs;describeErroragainst a realpath-carrying connection error and five hostile inputs; abort, transport and non-2xx swallowing; the wire-value pin) and three new cases inlib/wealthsimple/auth/.test.ts(exactly once with the exact payload, no request when unset, the halt preserved when delivery fails) plus the simultaneous-failure case now asserted with the alert live.🤖 Generated with Claude Code
POSTs { kind, body } as JSON to ALERT_WEBHOOK_URL with a bounded timeout. Never rejects: an unset target, a transport failure, a timeout or a non-2xx response are all logged and swallowed, so a failed push can never mask the halt. Co-Authored-By: Claude Code <noreply@anthropic.com>killSession now pushes { kind: wealthsimpleSessionDead, body } through lib/alert-webhook, gated on markDead()'s transition boolean so it fires exactly once per transition rather than once per failed call. Tests restore the push-alert assertions the port's header comment noted were dropped. Co-Authored-By: Claude Code <noreply@anthropic.com>