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.

Datos de origen

Repositorio
hookdeck/webhook-skills
Última actividad en el origen
2 de octubre de 2026 a las 14:58
Idioma detectado de SKILL.md
inglés
Estrellas
89
Forks
14

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
20 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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`,
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub