Skip to main content

sentry-webhooks

Receive and verify Sentry webhooks (sentry.io — the error-monitoring and application-performance platform). Use when setting up a Sentry Integration Platform webhook handler for an internal or public integration, debugging Sentry-Hook-Signature verification, or handling events like issue.created, issue.resolved, event_alert.triggered, metric_alert.critical, error.created, comment.created, seer.pr_created or preprod_artifact.size_analysis_completed. Sentry signs with HMAC-SHA256 over the RAW body, lowercase hex, keyed with the integration's Client Secret, and sends the digest as Sentry-Hook-Signature (or Sentry-App-Signature on UI-component requests). The resource is only in the Sentry-Hook-Resource header — the body has no event/type field. Not Sentry Insurance (sentry.com), not @sentry/* SDK event ingest.

Informations de source

Dépôt
hookdeck/webhook-skills
Dernière activité de la source
2 octobre 2026 à 14:58
Langue détectée de SKILL.md
anglais
Étoiles
89
Forks
14

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
20 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
sentry-webhooks
description
Receive and verify Sentry webhooks (sentry.io — the error-monitoring and application-performance platform). Use when setting up a Sentry Integration Platform webhook handler for an internal or public integration, debugging Sentry-Hook-Signature verification, or handling events like issue.created, issue.resolved, event_alert.triggered, metric_alert.critical, error.created, comment.created, seer.pr_created or preprod_artifact.size_analysis_completed. Sentry signs with HMAC-SHA256 over the RAW body, lowercase hex, keyed with the integration's Client Secret, and sends the digest as Sentry-Hook-Signature (or Sentry-App-Signature on UI-component requests). The resource is only in the Sentry-Hook-Resource header — the body has no event/type field. Not Sentry Insurance (sentry.com), not @sentry/* SDK event ingest.
license
MIT
metadata
{"author":"hookdeck","version":"0.1.0","repository":"https://github.com/hookdeck/webhook-skills"}
# Sentry Webhooks Sentry (sentry.io) is the error-monitoring and application-performance platform. This skill covers Sentry's **Integration Platform webhooks** — the webhooks emitted by a Sentry **internal integration** (org-scoped, the common case) or **public integration**, configured under *Settings → Developer Settings*. Canonical docs: [Webhooks](https://docs.sentry.io/organization/integrations/integration-platform/webhooks/) and [Sentry Hook Signature](https://docs.sentry.io/organization/integrations/integration-platform/webhooks/#sentry-hook-signature). > **Self-hosted Sentry runs the same code**, so the scheme below is identical > on a self-hosted host — only the domain changes. > **Not Sentry Insurance (sentry.com)**, not the Sentry login/identity > products, and **not `@sentry/*` SDK ingest** — events POSTed *to* Sentry at > `/api/{project}/store/` go the other direction and authenticate with DSN > keys, not HMAC. None of this applies there. ## When to Use This Skill - How do I receive Sentry webhooks? - How do I verify a Sentry webhook signature? - Why is my `Sentry-Hook-Signature` verification failing? - What is `Sentry-App-Signature` and why do I get it on some requests? - How do I know which event a Sentry webhook is — there's no `type` field? - How do I handle `issue.created`, `issue.resolved` or `issue.ignored`? - How do I handle `event_alert.triggered` and `metric_alert.critical`? - Does Sentry retry a failed webhook? Why did Sentry disable my webhook? - What's the difference between Integration Platform webhooks, service hooks and the legacy WebHooks plugin? ## The Single Most Important Routing Fact **The resource is only in the `Sentry-Hook-Resource` header.** The JSON body has **no `type` and no `event` field** — only `action`. To recover the event token you must combine them: ``` event = request.headers['sentry-hook-resource'] + '.' + body.action // e.g. 'issue' + '.' + 'created' → 'issue.created' ``` Any handler that switches on a body field alone is wrong and will never fire. ## Verification (core) HMAC-SHA256, **lowercase hex**, over the **raw request body bytes**, keyed with the integration's **Client Secret** used as raw UTF-8. No prefix, no `v1=`, no comma-separated list — the header value is a bare 64-character hex digest. The timestamp is **not** part of the signed string. ```javascript const crypto = require('crypto'); // Sentry's own server: hmac.new(key=secret.encode(), msg=body.encode(), // digestmod=sha256).hexdigest() — SentryApp.build_signature function verifySentrySignature(rawBody, headers, clientSecret) { if (!clientSecret) return false; // fail closed // Try Sentry-Hook-Signature, then fall back to Sentry-App-Signature: // UI-component requests sign identically but use the second header name. const received = headers['sentry-hook-signature'] || headers['sentry-app-signature']; if (!received) return false; const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody ?? '', 'utf8'); const expected = crypto .createHmac('sha256', clientSecret) // Client Secret AS-IS, never decoded .update(body) // RAW bytes — never re-serialized JSON .digest('hex'); // lowercase hex, NOT base64 const a = Buffer.from(String(received).trim(), 'utf8'); const b = Buffer.from(expected, 'utf8'); return a.length === b.length && crypto.timingSafeEqual(a, b); // length guard FIRST } ``` > **For complete handlers with tests**, see [examples/express/](examples/express/), [examples/nextjs/](examples/nextjs/), [examples/fastapi/](examples/fastapi/). ### Why manual HMAC and not an SDK The `@sentry/*` packages are **error-reporting SDKs**. None of them — and no official Sentry client library in any language — ships a webhook-signature verification helper. There is nothing to call. Do the HMAC directly with `node:crypto` or Python `hmac` + `hashlib`, as the examples here do. Do **not** add `@sentry/node` as a dependency for this. ## Gotchas That Actually Bite **Sentry's own published snippets are subtly wrong — use the raw body.** The documented JS snippet is `hmac.update(JSON.stringify(request.body), "utf8")` and the Python one is `body = json.dumps(request.body)`. Both **re-serialize a parsed body**. The Python one fails on any payload, because `json.dumps` defaults to `", "` / `": "` separators and Sentry signs compact JSON. The JS one only coincides with Sentry's bytes for ASCII-only payloads: Sentry serializes with simplejson configured `separators=(",", ":")` (compact, matching `JSON.stringify`) but leaves `ensure_ascii=True` in place, so Sentry emits `\uXXXX` escapes for any non-ASCII character while `JSON.stringify` emits the literal UTF-8 character. An issue title, comment or username with an accent or an emoji therefore produces a different byte string and the JS snippet **rejects a perfectly valid delivery**. Float and large-int formatting can diverge too. Verify against the raw body: `express.raw()`, `await request.text()`, `await request.body()`. **Accept `Sentry-App-Signature` as well.** Sentry's UI-component external requests — `select_options.requested`, `external_issue.created`, `external_issue.linked`, `alert_rule_action.requested` — sign with the same `build_signature` but send the header as `Sentry-App-Signature`. Sentry's own reference app checks both names on one endpoint, commented verbatim: *"HACK: The signature header may be one of these two values"*. Try `sentry-hook-signature`, fall back to `sentry-app-signature`, compare both in constant time. **Empty-body deliveries are real.** Some Sentry requests arrive with an empty body (`b''`) and `Content-Type: application/json`; the signature is then the HMAC of the empty string (`select_options.requested` signs `""` outright). Two traps: a JSON body parser must not 400 on it — Sentry's reference app notes *"Flask will throw a 400 Bad Request … because Sentry sends an empty body"* — and `express.json()` turns an empty body into `{}`, so signing `"{}"` fails (Sentry's own TS example special-cases `stringifiedBody === '{}' ? '' : …`). Using the raw body avoids both. **No timestamp/replay check is cryptographically possible.** `Sentry-Hook-Timestamp` is **not signed**, so an attacker replaying a captured body + signature can forge the timestamp freely. A tolerance check on the header is worth having as a cheap replay dampener, but it is *not* cryptographically bound. Sentry has **no** Stripe-style signed-timestamp replay protection. Dedupe on **`Request-ID`** — that is the real protection. **`Request-ID` is your idempotency key.** The body carries no delivery id. `Request-ID` is a per-request uuid4 hex. **No source-IP allowlist exists** for sentry.io webhook egress. Do not invent one. (Sentry documents inbound *ingest* IPs for its own relays; that is a different direction and is not a webhook-sender allowlist.) **No handshake, no challenge.** There is no echo-the-token or validation request. For a public integration the first traffic is `installation.created`; for an internal integration, webhooks simply start flowing once a URL and resources are configured. **`actor.id` is `str | int`.** When Sentry itself triggers the action it sends `{"type": "application", "id": "sentry", "name": "Sentry"}` — `id` is the **string** `"sentry"`, not a number. Do not type it as a number. **`issue.ignored` vs `issue.archived`.** The wire token is **`issue.ignored`**; the docs call it `archived`. `issue.archived` is kept as an equivalent alias for subscriptions stored before the rename. **Handle both, drop neither.** **The issue-alert resource is `event_alert`, not `issue_alert`.** The header says `event_alert`, which does not match the docs page title. A handler keyed on `"issue_alert"` will never fire. **`timingSafeEqual` throws on length mismatch.** Guard lengths first. An uncaught throw becomes a 500, and repeated failures trip Sentry's circuit breaker and can disable your webhook. ## Envelope Every Integration Platform delivery is a flat JSON object with four common keys: ```json { "action": "created", "installation": { "uuid": "a8e5d37a-696c-4c54-adb5-b3f28d64c7de" }, "data": { "issue": { "id": "100", "title": "ZeroDivisionError" } }, "actor": { "type": "user", "id": 1, "name": "Meredith Heller" } } ``` - `action` — the verb only. **The resource is in the header.** - `installation.uuid` — maps the delivery to an installation. - `actor` — `{type: "user" | "application", id: string | number, name}`. When another integration acts, `id` is that app's uuid. - `data` — resource-specific, and **customizable via UI components**. Treat it defensively: optional-chain everything. - `text` — optional top-level human-readable `"Sentry {resource}.{action}: {url}"` summary, added for some alert deliveries. Treat as optional. ### Headers on every delivery | Header | Value | |---|---| | `Content-Type` | `application/json` | | `Request-ID` | Per-request uuid4 hex — **your idempotency key** | | `Sentry-Hook-Resource` | Which resource fired — **the event's first half** | | `Sentry-Hook-Timestamp` | UNIX **seconds** (`str(int(time()))`). Not signed. | | `Sentry-Hook-Signature` | Lowercase hex HMAC-SHA256 digest | Header names are case-insensitive over HTTP; Sentry's docs show them title-cased, Node and Starlette lowercase them. ## Common Event Types Event tokens are `{resource}.{action}` — `Sentry-Hook-Resource` plus `body.action`. Subscriptions in the Sentry UI are selected **per resource**, and a resource subscription expands to **all** of that resource's events. | `Sentry-Hook-Resource` | `action` values | Notes | |---|---|---| | `installation` | `created`, `deleted` | Public integrations; install/uninstall | | `issue` | `created`, `resolved`, `assigned`, `unresolved`, `ignored` | Docs call `ignored` "archived"; `issue.archived` is an equivalent alias — handle both | | `error` | `created` | **Business plan and above only** — every error event, high volume | | `comment` | `created`, `updated`, `deleted` | Issue comments | | `event_alert` | `triggered` | **This is the issue-alert resource.** Not `issue_alert` | | `activity_alert` | `triggered` | `data.activity.type` is a `seer_*` activity | | `metric_alert` | `critical`, `warning`, `resolved`, `open` | `open` is in the server enum but **undocumented** | | `seer` | `root_cause_started`, `root_cause_completed`, `solution_started`, `solution_completed`, `coding_started`, `coding_completed`, `pr_created`, `pr_ready_for_review`, `iteration_started`, `iteration_completed` | The last three are in the server enum; the docs list only the first seven | | `preprod_artifact` | `size_analysis_completed`, `build_distribution_completed` | Mobile builds. **camelCase payload keys** | Separately, the UI-component external requests (`select_options.requested`, `external_issue.created`, `external_issue.linked`, `alert_rule_action.requested`) are **request/response calls Sentry makes to your integration**, not subscribable webhooks. They are the reason the `Sentry-App-Signature` header exists. ### Payload pointers - **`event_alert.triggered`** — `data.event` is a full Sentry event (exception, stacktrace, **`tags` as an array of `[key, value]` PAIRS, not an object**), plus `data.event.issue_id`, `data.event.web_url`, `data.triggered_rule` (the rule's label) and `data.issue_alert.settings`. Stack frames are ordered oldest → most recent. - **`issue.*`** — `data.issue.status` is `resolved` | `unresolved` | `ignored`; `substatus` is one of `archived_until_escalating`, `archived_until_condition_met`, `archived_forever`, `escalating`, `ongoing`, `regressed`, `new`; `statusDetails` carries `inRelease` / `inNextRelease` / `inCommit` / `ignoreCount` / `ignoreWindow` / `ignoreUserCount` / `ignoreUserWindow` / `ignoreDuration`. `issueCategory` includes `error`, `outage` (uptime/cron monitors) and feedback; `issueType` is more specific (`uptime_domain_failure`, `monitor_check_in_failure`). `issue.created` currently fires for the `OUTAGE`, `ERROR` and `FEEDBACK` categories. - **`metric_alert.*`** — `data.metric_alert` is the incident, `data.metric_alert.alert_rule` the rule, plus `data.description_text`, `data.description_title`, `data.web_url`. - **`activity_alert.triggered`** — `data.activity.type` is a `seer_*` activity (`seer_root_cause_started` … `seer_pr_iteration_completed`) with a type-dependent `data.activity.details`, plus `data.issue` and `data.alert.{title,url,web_url,settings}`. - **`seer.*`** — `data.run_id` + `data.group_id` correlate a run; completed events carry `root_cause` / `solution` / `code_changes` (keyed by `owner/name` repo, each change `{diff, path, type: M|A|D, added, removed}`) / pull-request details. - **`preprod_artifact.*`** — **camelCase** keys here (`buildId`,
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub