Alert — Error → Slack/Sentry Bootstrap
Make a service tell you when it breaks in production, without a silent no-op. The pattern is the same
across stacks: one reporting seam → fan out to logging + Sentry + Slack → dedupe → gate behind a flag
→ seed the secret and deploy in the correct order. Most rollouts get bitten by the plumbing, not the
code — this front-loads those traps.
North star: every unhandled error and every swallowed-but-important event reaches a human channel,
exactly once per incident, with enough context to act — verified live, not assumed.
Step 0 — Detect the stack and delivery path
- Language/runtime — long-running server, serverless function, background worker? Determines where
the reporter seam and flush hooks go.
- Where secrets live — a cloud secret manager, CI provider secrets, or plain env vars?
- How config flags are set — a config service, a YAML file, an env var, a feature-flag provider?
- Deploy mechanism — does a fresh deploy clone the currently running config forward (common on
container platforms), or rebuild config from source every time? This decides whether a new secret
wires itself in automatically or needs a one-time bootstrap (see Step 4).
Step 1 — One reporting seam, not scattered webhook calls
Create a single module the whole app calls through, with two entry points:
report(error, context) → logging + Sentry + Slack. For unhandled/handled exceptions.
notify(message, context) → Slack only. For "this is fine but a human should know" events (e.g. a
webhook you ack with 200 but couldn't fully process).
Requirements baked into the seam:
- Dedupe — collapse identical errors within a window (~5 min) by a stable key (type + message +
call-site), so one bad deploy doesn't post thousands of Slack lines.
- Never throw from the reporter — an alerting failure must not break the request path. Wrap the
Slack/Sentry calls; on failure, log and move on.
- Context every time — environment, service, request/trace id, and a short human summary.
- Server-side only — a Slack webhook URL is a credential (see Step 2). It runs on the server; never
ship it to a browser/client bundle.
Wire the seam at the real failure points, not just a global handler:
- Global exception handler / middleware →
report.
- Background workers — the silent killer is a
try/except around the poll loop that swallows
everything. Route that catch to report, add a shutdown-signal flush so queued alerts drain before
exit, and a one-line recovery notice when it comes back.
- Webhook endpoints that ack with 200 even on partial failure →
notify on the swallowed branch.
- Frontend error boundary (if any) → Sentry's error hook.
Step 2 — Secrets: the Slack webhook URL is the credential
- A Slack incoming webhook URL carries no separate token — possessing the URL is the ability to post.
Treat it as a secret: store it in the secret manager, server-only, never logged, never committed.
- A Sentry DSN is public by design — safe as a plain env var / client config. Don't over-protect it.
- Convention that scales to multi-env: one secret per environment (e.g. a dev webhook and a prod
webhook), and the app selects by its own environment setting. Provision both in every stack so the
service never fails to start on a missing one.
⚠️ Trap A — seed the secret as a plain string, not JSON-wrapped
A generic secret-seeding helper may store {"KEY":"value"} while the app injects the bare secret value
as the env var. Result: the env var holds a JSON blob, not a URL, the post silently fails, and you get a
200 with no Slack message. Seed the raw value if that's what the runtime expects, and verify what's
actually stored and what the running instance received — don't trust "seed succeeded."
⚠️ Trap B — an empty secret can crash container startup
On several container platforms, a secret with an empty value throws an initialization error and the task
never starts. Provision each alert secret with a placeholder value at create time (so a fresh stack
boots), then overwrite with the real URL — and make sure your infra tooling won't fight the manual seed
on the next apply.
Step 3 — Gate behind a flag (off by default), then enable deliberately
- Put delivery behind a config flag, default false. Ship the code dark, enable per-environment once
the secret is verified.
- If the flag lives in a YAML file, watch indentation — a wrong-indent edit can silently no-op. Probe
the deployed value after applying; don't assume the file edit took.
- Correct order to go live: seed secret → set flag → deploy (re-hydrate config). Doing config before
secret, or forgetting the redeploy, yields a "configured but dead" alerting path.
Step 4 — ⚠️ Trap C: a clone-forward deploy won't pick up a new secret on its own
If your deploy pipeline clones the currently-running config/task definition forward and treats it as
immutable outside CI, adding a new secret in IaC or a normal deploy can fail to wire it in — the running
instance doesn't have it, so the clone doesn't either. Bootstrap it once:
- Register a new revision that includes the new secret entry (+ env mapping).
- Seed the secret value (Step 2).
- Force a new deployment against that revision.
- From then on, deploys clone that revision forward and it sticks.
This single trap is the most common reason "I added Slack alerts and nothing happens in prod."
Step 5 — Verify live
- Trigger a real error or send a real test event through the actual ingress (not a unit test). Confirm
the message lands in the channel for each environment you enabled.
- Confirm the running instance shows the expected revision, the secret resolves to a URL (not JSON, not
a placeholder), and the flag reads true in the deployed config.
- Re-trigger to confirm dedupe collapses repeats.
- For workers: kill/restart to confirm the shutdown flush and recovery notice fire.
Per-environment go-live checklist
[ ] Reporting seam in place; report() + notify() wired at all failure points (incl. worker catch + 200-ack webhooks)
[ ] Slack webhook secret seeded plain (verified server-side), per-env, never client-exposed
[ ] Secret placeholder set so a fresh instance boots; infra tooling won't overwrite the manual seed
[ ] Sentry DSN wired (plain env ok); error boundary hooked up
[ ] Flag default false; enabled deliberately per env; deployed value probed, not assumed
[ ] Clone-forward deploys bootstrapped once with the new secret entry
[ ] Live test fired through real ingress → message seen in channel, per env
[ ] Dedupe + shutdown flush + recovery notice verified
Anti-patterns
- Scattering ad-hoc Slack post calls through the codebase instead of one reporter seam.
- Seeding the webhook JSON-wrapped when the app wants the raw URL → silent no-op.
- Enabling the flag before the secret is verified → "configured but dead."
- Assuming a deploy picks up a new secret on a clone-forward pipeline (Trap C).
- Calling it done after a green deploy without seeing a real alert land in the channel.
- Letting a worker's poll-loop
try/except swallow errors with no report().
1---2name: alert3description: (forwward) Bootstraps production error alerting — wires app errors to Slack and/or Sentry through a single reporting seam, deduped notifications, and secret/flag/deploy plumbing done in the right order. Triggers on "add alerting", "wire up Slack alerts", "add Sentry", "we need to know when prod breaks", or setting up observability for a new service.4---56# Alert — Error → Slack/Sentry Bootstrap78Make a service tell you when it breaks in production, without a silent no-op. The pattern is the same9across stacks: **one reporting seam → fan out to logging + Sentry + Slack → dedupe → gate behind a flag10→ seed the secret and deploy in the correct order.** Most rollouts get bitten by the *plumbing*, not the11code — this front-loads those traps.1213**North star:** every unhandled error and every swallowed-but-important event reaches a human channel,14exactly once per incident, with enough context to act — verified live, not assumed.1516## Step 0 — Detect the stack and delivery path1718- **Language/runtime** — long-running server, serverless function, background worker? Determines where19 the reporter seam and flush hooks go.20- **Where secrets live** — a cloud secret manager, CI provider secrets, or plain env vars?21- **How config flags are set** — a config service, a YAML file, an env var, a feature-flag provider?22- **Deploy mechanism** — does a fresh deploy clone the *currently running* config forward (common on23 container platforms), or rebuild config from source every time? This decides whether a new secret24 wires itself in automatically or needs a one-time bootstrap (see Step 4).2526## Step 1 — One reporting seam, not scattered webhook calls2728Create a single module the whole app calls through, with two entry points:2930- `report(error, context)` → logging + Sentry + Slack. For unhandled/handled exceptions.31- `notify(message, context)` → Slack only. For "this is fine but a human should know" events (e.g. a32 webhook you ack with 200 but couldn't fully process).3334Requirements baked into the seam:3536- **Dedupe** — collapse identical errors within a window (~5 min) by a stable key (type + message +37 call-site), so one bad deploy doesn't post thousands of Slack lines.38- **Never throw from the reporter** — an alerting failure must not break the request path. Wrap the39 Slack/Sentry calls; on failure, log and move on.40- **Context every time** — environment, service, request/trace id, and a short human summary.41- **Server-side only** — a Slack webhook URL is a credential (see Step 2). It runs on the server; never42 ship it to a browser/client bundle.4344Wire the seam at the real failure points, not just a global handler:4546- Global exception handler / middleware → `report`.47- **Background workers** — the silent killer is a `try/except` around the poll loop that swallows48 everything. Route that catch to `report`, add a shutdown-signal flush so queued alerts drain before49 exit, and a one-line recovery notice when it comes back.50- **Webhook endpoints that ack with 200** even on partial failure → `notify` on the swallowed branch.51- Frontend error boundary (if any) → Sentry's error hook.5253## Step 2 — Secrets: the Slack webhook URL *is* the credential5455- A Slack incoming webhook URL carries no separate token — possessing the URL is the ability to post.56 Treat it as a secret: store it in the secret manager, server-only, never logged, never committed.57- **A Sentry DSN is public by design** — safe as a plain env var / client config. Don't over-protect it.58- Convention that scales to multi-env: one secret per environment (e.g. a dev webhook and a prod59 webhook), and the app selects by its own environment setting. Provision both in every stack so the60 service never fails to start on a missing one.6162### ⚠️ Trap A — seed the secret as a plain string, not JSON-wrapped6364A generic secret-seeding helper may store `{"KEY":"value"}` while the app injects the bare secret value65as the env var. Result: the env var holds a JSON blob, not a URL, the post silently fails, and you get a66200 with **no Slack message**. Seed the raw value if that's what the runtime expects, and verify what's67actually stored *and* what the running instance received — don't trust "seed succeeded."6869### ⚠️ Trap B — an empty secret can crash container startup7071On several container platforms, a secret with an empty value throws an initialization error and the task72never starts. Provision each alert secret with a placeholder value at create time (so a fresh stack73boots), then overwrite with the real URL — and make sure your infra tooling won't fight the manual seed74on the next apply.7576## Step 3 — Gate behind a flag (off by default), then enable deliberately7778- Put delivery behind a config flag, default **false**. Ship the code dark, enable per-environment once79 the secret is verified.80- If the flag lives in a YAML file, watch indentation — a wrong-indent edit can silently no-op. Probe81 the *deployed* value after applying; don't assume the file edit took.82- Correct order to go live: **seed secret → set flag → deploy (re-hydrate config)**. Doing config before83 secret, or forgetting the redeploy, yields a "configured but dead" alerting path.8485## Step 4 — ⚠️ Trap C: a clone-forward deploy won't pick up a new secret on its own8687If your deploy pipeline clones the currently-running config/task definition forward and treats it as88immutable outside CI, adding a new secret in IaC or a normal deploy can fail to wire it in — the running89instance doesn't have it, so the clone doesn't either. Bootstrap it once:90911. Register a new revision that includes the new secret entry (+ env mapping).922. Seed the secret value (Step 2).933. Force a new deployment against that revision.944. From then on, deploys clone *that* revision forward and it sticks.9596This single trap is the most common reason "I added Slack alerts and nothing happens in prod."9798## Step 5 — Verify live99100- Trigger a real error or send a real test event through the actual ingress (not a unit test). Confirm101 the message lands in the channel for each environment you enabled.102- Confirm the running instance shows the expected revision, the secret resolves to a URL (not JSON, not103 a placeholder), and the flag reads true in the *deployed* config.104- Re-trigger to confirm dedupe collapses repeats.105- For workers: kill/restart to confirm the shutdown flush and recovery notice fire.106107## Per-environment go-live checklist108109```110[ ] Reporting seam in place; report() + notify() wired at all failure points (incl. worker catch + 200-ack webhooks)111[ ] Slack webhook secret seeded plain (verified server-side), per-env, never client-exposed112[ ] Secret placeholder set so a fresh instance boots; infra tooling won't overwrite the manual seed113[ ] Sentry DSN wired (plain env ok); error boundary hooked up114[ ] Flag default false; enabled deliberately per env; deployed value probed, not assumed115[ ] Clone-forward deploys bootstrapped once with the new secret entry116[ ] Live test fired through real ingress → message seen in channel, per env117[ ] Dedupe + shutdown flush + recovery notice verified118```119120## Anti-patterns121122- Scattering ad-hoc Slack post calls through the codebase instead of one reporter seam.123- Seeding the webhook JSON-wrapped when the app wants the raw URL → silent no-op.124- Enabling the flag before the secret is verified → "configured but dead."125- Assuming a deploy picks up a new secret on a clone-forward pipeline (Trap C).126- Calling it done after a green deploy without seeing a real alert land in the channel.127- Letting a worker's poll-loop `try/except` swallow errors with no `report()`.