Skip to content

feat: add sentry-webhooks skill - #212

Merged
garethx merged 4 commits into
mainfrom
feat/sentry-webhooks
Oct 2, 2026
Merged

garethx merged 4 commits into
mainfrom
feat/sentry-webhooks

Conversation

@garethx

@garethx garethx commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Adds a sentry-webhooks skill for Sentry's Integration Platform webhooks — what an internal integration (org-scoped, the common case) or public integration emits. Self-hosted Sentry runs the same code, so only the host differs.

There is no SENTRY source type in Hookdeck today (checked hookdeck.com/docs/sources on 2026-10-01: every "sentry" hit on that page is Sentry's own JS debug-id shim, not a source entry), so there was no Hookdeck-side config to disambiguate a variant against. The scheme below was resolved from Sentry's own server code instead of from the docs alone — which turned out to matter, because the published snippets are wrong.

The scheme

HMAC-SHA256, lowercase hex, over the raw request body, keyed with the integration's Client Secret as raw UTF-8. SentryApp.build_signature (src/sentry/sentry_apps/models/sentry_app.py):

hmac.new(key=secret.encode("utf-8"), msg=body.encode("utf-8"), digestmod=sha256).hexdigest()

The header value is a bare 64-char hex digest — no v1=, no t=, no list.

Four things a reviewer will otherwise re-flag

  1. Both Sentry-Hook-Signature and Sentry-App-Signature are accepted, deliberately. Sentry's UI-component external requests (select_options.requested, external_issue.created/linked, alert_rule_action.requested) use the same build_signature but send the second header name — verified in src/sentry/sentry_apps/external_requests/*.py. Sentry's own reference app checks both on one endpoint, commented verbatim: "HACK: The signature header may be one of these two values". This is not a fabricated header.

  2. The examples deliberately disagree with Sentry's documented snippets. The docs verify JSON.stringify(request.body) / json.dumps(request.body). Both re-serialize a parsed body, and both are latent bugs. Sentry serializes with simplejson separators=(",", ":") (compact, matching JSON.stringify) but leaves ensure_ascii=True in place — see _default_encoder in src/sentry/utils/json.py — so Sentry emits \uXXXX escapes where JSON.stringify emits the literal character: any accent or emoji in an issue title, comment or username makes the documented JS snippet reject a valid delivery. The documented Python snippet is worse — stdlib json.dumps defaults to ", " / ": " separators, so it diverges on any payload with more than one key. Every example verifies the raw body and says why.

  3. The timestamp is not signed, so there is no cryptographic replay protection. Sentry-Hook-Timestamp is sent but excluded from the signed string. The tolerance check is opt-in and documented as a dampener only; real protection is deduplication on the Request-ID header, since the body carries no delivery id.

  4. Empty bodies are legitimate. Some requests arrive with b'' and Content-Type: application/json, and select_options.requested signs "" outright. express.json() would coerce that to {} and fail verification — Sentry's own TS example special-cases it. Using the raw body avoids the problem, which is a second reason for the rule above.

Routing

The resource is only in the Sentry-Hook-Resource header — the body has no type or event field, just action. The event token is header + "." + body.action. Note event_alert is the issue-alert resource: a handler keyed on issue_alert.triggered never fires.

Resources and actions come from SentryAppEventType / sentry_apps/utils/webhooks.py, cross-checked against the docs. Four tokens are in the server enum but absent from the docs — metric_alert.open, seer.pr_ready_for_review, seer.iteration_started, seer.iteration_completed — and are handled but labelled undocumented rather than asserted. issue.ignored is the wire token the docs call archived; both are handled, neither dropped.

Two sibling surfaces are described but not conflated with the above: service hooks (X-ServiceHook-Signature, keyed with the hook's own secret, feature-flagged) and the legacy WebHooks plugin (completely unsigned, no longer documented — the skill says there is nothing to verify there rather than offering a verifier).

Review findings

The accuracy pass raised 9 issues, 6 fixed in-run. Each was confirmed against a primary source before acting:

  • Confirmed and fixed — error.created reads data.error.issue_id, not data.event.issue_id. The errors doc page's attribute list claims data['event']['issue_id'], but its own payload sample has no data.event key at all, so the docs contradict themselves and the reviewer was right.
  • Confirmed and fixed — the FastAPI verifier called hmac.compare_digest on str values; Starlette decodes headers as latin-1 and compare_digest raises TypeError on non-ASCII str, turning a junk signature byte into a 500 instead of a 401. Both sides are now encoded.
  • Confirmed and fixed — the "reproduce a digest by hand" recipe used printf '%s' "$(cat body.json)", whose command substitution strips trailing newlines, so it would not hash the captured bytes.
  • Confirmed and fixed — the "coincides only for ASCII-only payloads" claim was accurate for the JS snippet but too generous for the Python one (separators, as above).
  • Found separately in this run, confirmed against the docs' payload samples, fixed in the last commit — data.pull_requests entries nest the PR under pull_request ({pull_request: {pr_number, pr_url, pr_id}, repo_name, provider}), so the generated pr.pr_number read undefined in all three examples; and data.gitInfo.branch does not exist (the documented fields are headRef / baseRef).

Delivery behaviour is documented as Sentry actually implements it: respond within 1 second, no retries, and repeated failures trip a per-integration circuit breaker that can disable the webhook (_notify_webhook_disabled in src/sentry/utils/sentry_apps/webhooks.py). No source-IP allowlist is published, and the skill says so rather than inventing one.

Testing

  • validate-provider.sh sentry-webhooks — passes
  • Express: 44 tests passing
  • Next.js: 35 tests passing (Node 24)
  • FastAPI: 38 tests passing (fresh scratch venv)

Not live-verified — no Sentry test account was supplied for this run, so the digest has not been recomputed against a real delivery. Everything above is from Sentry's server source and its docs' payload samples.

🤖 Generated with Claude Code

https://claude.ai/code/session_012xFvQczfSFGTYdJ9BgoHVM

garethx and others added 3 commits October 2, 2026 12:07
Verified against the Sentry docs payload samples before changing anything:

- seer.pr_created / seer.pr_ready_for_review: data.pull_requests entries are
  {pull_request: {pr_number, pr_url, pr_id}, repo_name, provider}. The PR
  fields are nested under pull_request; reading pr.pr_number off the array
  element returned undefined in all three examples.
- preprod_artifact.build_distribution_completed: data.gitInfo has no `branch`
  field. The documented branch fields are headRef / baseRef.
- references/overview.md listed the pull_requests shape flattened.

Also adds the integration files the review pass does not write: the README
row, the providers.yaml research brief, and the marketplace.json entry
(regenerated with sync-marketplace, not hand-merged).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xFvQczfSFGTYdJ9BgoHVM
hookdeck/core#5771 adds SENTRY as an HMAC alias — sha256, hex,
header_key sentry-hook-signature, one secret labelled Client Secret, which
is the scheme this skill already describes. Replaces the "no SENTRY source
type in Hookdeck" note in SKILL.md and references/setup.md.

Records why that source type covers Sentry-Hook-Signature only: the
Sentry-App-Signature requests are synchronous request/response calls to the
component's own path, and select-options and issue-link requests expect a
JSON reply Sentry validates, so a Hookdeck source cannot usefully proxy
them. The examples still accept both header names, because a handler's own
endpoint can legitimately receive either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xFvQczfSFGTYdJ9BgoHVM
@garethx
garethx marked this pull request as ready for review October 2, 2026 14:47
@garethx
garethx merged commit c63f9be into main Oct 2, 2026
8 checks passed
@garethx
garethx deleted the feat/sentry-webhooks branch October 2, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant