Skip to content

feat: Add Sentry webhook receiver and admin error management API - #812

Merged
otomatty merged 3 commits into
developfrom
claude/fix-issue-803-CA6y7
May 4, 2026
Merged

feat: Add Sentry webhook receiver and admin error management API#812
otomatty merged 3 commits into
developfrom
claude/fix-issue-803-CA6y7

Conversation

@otomatty

@otomatty otomatty commented May 4, 2026

Copy link
Copy Markdown
Owner

概要

Sentry Internal Integration webhook 受信機能と、管理画面用の API エラー管理 API を実装します。これにより、Sentry から送信されるエラーイベントを自動的に取り込み、管理画面でエラーの一覧表示・詳細確認・ステータス管理ができるようになります。

変更点

  • Sentry webhook 受信エンドポイント (POST /api/webhooks/sentry)

    • HMAC-SHA256 署名検証(Sentry-Hook-Signature ヘッダ)
    • ペイロード正規化と issue ID / title / route / statusCode / fingerprint の抽出
    • apiErrorService.upsertFromSentrySummary への委譲
    • 未対応イベント型は 200 で受理(Sentry の自動リトライを防止)
  • 管理 API エラー一覧・詳細・更新エンドポイント

    • GET /api/admin/errors — ページネーション + status/severity フィルタ
    • GET /api/admin/errors/:id — 詳細取得
    • PATCH /api/admin/errors/:id — ワークフロー状態更新(open → investigating → resolved / ignored)
  • テスト

    • Sentry webhook 署名検証・ペイロード抽出のユニットテスト
    • 管理 API エンドポイントの統合テスト(認可・バリデーション・並行更新検知)
  • 設定

    • .env.exampleSENTRY_WEBHOOK_SECRET を追加
    • app.ts に webhook ルートをマウント
    • routes/admin/index.ts に errors ルートをマウント

変更の種類

  • ✨ 新機能 (New feature)
  • 🧪 テスト (Tests)

テスト方法

  1. npm test で全テストが通ることを確認

    • sentry.test.ts: 署名検証、ペイロード抽出、エラーハンドリング
    • errors.test.ts: 一覧・詳細・更新エンドポイント、認可、並行更新検知
  2. 環境変数 SENTRY_WEBHOOK_SECRET を設定して、Sentry から webhook を送信

    • 署名が正しい場合は 200 + DB upsert
    • 署名が不正な場合は 403
    • sentry_issue_id を抽出できない場合は 200 + ignored
  3. 管理画面で /api/admin/errors にアクセス

    • 認可なしで 401、非管理者で 403
    • 管理者で一覧・詳細・ステータス更新が動作

チェックリスト

  • テストがすべてパスする
  • Lint エラーがない
  • 必要に応じてドキュメントを更新した(JSDoc コメント)
  • コミットメッセージが Conventional Commits に従っている

関連 Issue

Closes #616, #803

https://claude.ai/code/session_01Gyem2NDUc6JT4kZaxYU6Zo

Summary by CodeRabbit

  • New Features
    • Added admin endpoints to view and manage API errors: list all errors with pagination and optional filters, view individual error details, and update error status.
    • Integrated Sentry webhook to automatically track and log error occurrences from Sentry.

- POST /api/webhooks/sentry with HMAC-SHA256 signature verification using
  SENTRY_WEBHOOK_SECRET; payload is normalized via extractSentrySummary and
  upserted via apiErrorService.upsertFromSentrySummary.
- /api/admin/errors GET list (paginated + status/severity filters), GET :id,
  PATCH :id status (delegates to apiErrorService updateApiErrorStatus, maps
  ApiErrorStatusConflictError to 409, invalid transitions to 400).
- Mount new routes in app.ts and admin/index.ts; add SENTRY_WEBHOOK_SECRET
  to server/api/.env.example.
- Vitest coverage: signature OK / NG / missing / unconfigured secret,
  ignored payloads (no sentry_issue_id), invalid JSON, plus admin route
  auth/role and PATCH transition / 404 / 409 cases.

Refs Epic #616 (Phase 1).
@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@otomatty has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 35 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f7ffc055-6469-4aef-bd5e-77be3241d6cf

📥 Commits

Reviewing files that changed from the base of the PR and between 2f10a52 and 6e3e378.

📒 Files selected for processing (5)
  • server/api/src/__tests__/routes/admin/errors.test.ts
  • server/api/src/__tests__/routes/webhooks/sentry.test.ts
  • server/api/src/routes/admin/errors.ts
  • server/api/src/routes/webhooks/sentry.ts
  • server/api/src/services/apiErrorService.ts
📝 Walkthrough

Walkthrough

Implements Phase 1 of Epic #616 by adding Sentry webhook ingestion, signature verification, polymorphic payload extraction, and admin error management routes. New endpoint POST /api/webhooks/sentry validates HMAC-SHA256 signatures and upserts normalized error metadata into the api_errors table. Admin routes expose paginated error listing, detail retrieval, and status updates with comprehensive test coverage.

Changes

Sentry Webhook & Admin Error Management

Layer / File(s) Summary
Configuration & Environment
server/api/.env.example
Added SENTRY_WEBHOOK_SECRET environment variable with documentation describing HMAC-SHA256 signature validation for POST /api/webhooks/sentry.
Webhook Signature & Payload Extraction
server/api/src/routes/webhooks/sentry.ts
Implemented verifySentrySignature() using constant-time comparison, extractSentrySummary() to normalize polymorphic Sentry payloads (issue/event shapes) and extract sentryIssueId, title, fingerprint, route, and statusCode. Includes helpers to coerce and normalize tag formats.
Webhook Endpoint
server/api/src/routes/webhooks/sentry.ts
POST /api/webhooks/sentry handler validates signature (403 if missing/mismatched), parses JSON (400 if invalid), acknowledges non-extractable payloads as 200 ignored, and upserts extracted summaries to database; maps validation errors to 400 and conflicts to 409.
Admin Error Routes
server/api/src/routes/admin/errors.ts
Implemented GET /api/admin/errors (paginated list with status/severity filters, limit clamping to 200), GET /api/admin/errors/:id (detail with 404 fallback), and PATCH /api/admin/errors/:id (status update with conflict and transition validation). Includes query validation helpers.
Route Registration
server/api/src/app.ts, server/api/src/routes/admin/index.ts
Wired Sentry webhook at /api/webhooks/sentry and admin errors at /admin/errors into app routing.
Tests
server/api/src/__tests__/routes/webhooks/sentry.test.ts, server/api/src/__tests__/routes/admin/errors.test.ts
Comprehensive Vitest suites covering signature verification (valid/missing/mismatched/wrong-secret), payload extraction from multiple Sentry shapes (issue-created, event-based), ignored payloads, database upsert behavior, admin route access control (401/403), pagination, filtering, CRUD operations with conflict/transition validation, and error handling.

Sequence Diagram

sequenceDiagram
    participant Sentry as Sentry (SaaS)
    participant Webhook as POST /api/webhooks/sentry
    participant Verify as Signature Verification
    participant Extract as extractSentrySummary()
    participant DB as api_errors Table
    participant Admin as GET /admin/errors

    Sentry->>Webhook: POST with raw body + sentry-hook-signature header
    Webhook->>Verify: Verify HMAC-SHA256 signature
    alt Invalid Signature
        Verify-->>Sentry: 403 Forbidden
    else Valid Signature
        Webhook->>Webhook: Parse JSON payload
        alt Invalid JSON
            Webhook-->>Sentry: 400 Bad Request
        else Valid JSON
            Webhook->>Extract: extractSentrySummary(payload)
            alt Cannot extract sentryIssueId
                Extract-->>Webhook: null (payload ignored)
                Webhook-->>Sentry: 200 { received: true, ignored: true }
            else Successfully extracted
                Extract-->>Webhook: SentrySummaryExtraction
                Webhook->>DB: upsertFromSentrySummary(db, summary)
                DB-->>Webhook: { id, ... }
                Webhook-->>Sentry: 200 { received: true, id: ... }
            end
        end
    end

    Admin->>Webhook: (Later) GET /admin/errors?limit=10&offset=0
    Webhook->>DB: listApiErrors(filters, limit, offset)
    DB-->>Webhook: { errors[], total, limit, offset }
    Webhook-->>Admin: 200 JSON response
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly Related Issues

  • Epic #616: This PR implements Phase 1 of the full error capture and AI analysis pipeline, adding the Sentry webhook intake and admin error management foundation.

Possibly Related PRs

Poem

A Sentry whispers, "Error! Beware!" 🌙
Through HMAC's vault, the signature swears,
Tag-tuples and fingerprints, all rearranged,
Into admin's ledger—chaos estranged! ✨
Hop-hop, the rabbit decoded it all. 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: a Sentry webhook receiver and admin error management API are the primary deliverables of this pull request.
Linked Issues check ✅ Passed The PR implements Phase 1 of Epic #616 and resolves Issue #803, delivering the Sentry webhook receiver with signature verification, payload normalization, admin error API endpoints (list/detail/status update), comprehensive tests, and environment configuration as specified.
Out of Scope Changes check ✅ Passed All changes are scoped to Phase 1 objectives: webhook receiver, admin endpoints, tests, and configuration. No unrelated code or out-of-scope features are introduced beyond the documented requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-issue-803-CA6y7

Review rate limit: 0/5 reviews remaining, refill in 3 minutes and 35 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Sentry webhook receiver and administrative endpoints for managing API errors. Key additions include HMAC-SHA256 signature verification for Sentry webhooks, payload normalization, and a set of admin routes for listing, viewing, and updating error statuses. Feedback focuses on improving robustness by validating UUID formats for route parameters, handling potential null values in request bodies, and replacing fragile regex-based error handling with dedicated custom error classes.

Comment on lines +108 to +109
const id = c.req.param("id");
const db = c.get("db");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

id パラメータが UUID 形式であることを検証していません。不正な形式の文字列が渡されると、Postgres がエラーを投げ、結果として 500 Internal Server Error が返される可能性があります。事前に形式をチェックし、404 を返すのが適切です。

  const id = c.req.param("id");
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
    return c.json({ error: "Not found" }, 404);
  }
  const db = c.get("db");

Comment on lines +131 to +132
const id = c.req.param("id");
const db = c.get("db");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

PATCH エンドポイントでも同様に、id が UUID 形式であることを検証することを推奨します。不正な形式の場合に 500 エラーになるのを防ぐことができます。

  const id = c.req.param("id");
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
    return c.json({ error: "Not found" }, 404);
  }
  const db = c.get("db");

Comment thread server/api/src/routes/admin/errors.ts Outdated
return c.json({ error: "invalid JSON body" }, 400);
}

if (typeof body.status !== "string") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

c.req.json() が null を返す可能性があるため(リクエストボディが 'null' の場合など)、body.status の参照で TypeError が発生するリスクがあります。body が存在することを確認してからプロパティにアクセスするようにしてください。

Suggested change
if (typeof body.status !== "string") {
if (!body || typeof body.status !== "string") {

Comment thread server/api/src/routes/admin/errors.ts Outdated
Comment on lines +164 to +166
if (err instanceof Error && /invalid api_errors status transition/i.test(err.message)) {
return c.json({ error: err.message }, 400);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

サービス層からのエラーメッセージを正規表現で判定するのは保守性が低く脆弱です。メッセージ内容が変更されると判定が機能しなくなります。ApiErrorInvalidTransitionError などのカスタムエラークラスを定義し、instanceof で判定することを検討してください。

Comment on lines +336 to +338
/required|must be|less than or equal/i.test(err.message) &&
!/no rows/i.test(err.message)
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

この正規表現によるエラー判定は脆弱です。apiErrorService.ts 側のバリデーションロジックやメッセージが変更されると、意図せず 500 エラーとして扱われる可能性があります。専用のカスタムエラークラス(例: ApiErrorValidationError)を導入して判定することを推奨します。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f10a525aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +176 to +180
asString(issue?.id) ??
asString(event?.issue_id) ??
asString(eventIssue?.id) ??
asString(group?.id) ??
asString(data.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include data.error.id when extracting Sentry issue IDs

extractSentrySummary claims to support data.error payloads (and already reads error.title), but extractSentryIssueId never checks data.error.id. For webhook events where the identifier exists only under data.error, summary becomes null, the handler returns 200 {ignored:true}, and no api_errors row is created, so those errors are silently dropped from admin error tracking.

Useful? React with 👍 / 👎.

…ction (#803)

Address review feedback on PR #812:

- apiErrorService: introduce ApiErrorValidationError and
  ApiErrorInvalidTransitionError so route handlers can branch on
  `instanceof` instead of regex-matching error messages.
- routes/webhooks/sentry: replace regex-based 400 detection with
  `instanceof ApiErrorValidationError`, and extend extractSentryIssueId
  to also read `data.error.id` so payloads where the identifier only
  lives under `data.error` are no longer silently dropped (Codex P1).
- routes/admin/errors: validate `:id` matches the UUID pattern up front
  (return 404 instead of letting Postgres surface a 500), guard against
  `c.req.json()` returning literal `null`, and use the typed transition
  error for 400 mapping.
- Tests: cover invalid-UUID 404, JSON-null body 400, and the
  data.error.id extraction path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
server/api/src/routes/admin/errors.ts (2)

164-165: ⚡ Quick win

Replace message-regex error mapping with typed error handling.

Line 164-Line 165 depends on matching text in err.message; wording changes would unexpectedly convert a client 400 into a 500. Prefer a dedicated typed error from apiErrorService and instanceof here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/routes/admin/errors.ts` around lines 164 - 165, Replace the
fragile regex message check with a proper typed error from apiErrorService:
import the dedicated error class (e.g., InvalidStatusTransitionError or the
actual class exported by apiErrorService), and change the condition in the error
handler that currently tests err.message to use "err instanceof
<TypedErrorClass>" so you can return c.json({ error: err.message }, 400) only
for that type; leave other error branches unchanged so non-matching errors still
propagate as before.

28-28: ⚡ Quick win

Centralize ApiErrorStatus literals to avoid enum drift.

Line 28 duplicates workflow statuses already defined in service transition rules. If the source-of-truth changes, this parser can become stale and reject valid values.

♻️ Proposed refactor
 import {
+  ALLOWED_API_ERROR_STATUS_TRANSITIONS,
   ApiErrorStatusConflictError,
   API_ERROR_LIST_DEFAULT_LIMIT,
   API_ERROR_LIST_MAX_LIMIT,
   getApiErrorById,
   listApiErrors,
   updateApiErrorStatus,
 } from "../../services/apiErrorService.js";

-const VALID_STATUSES = ["open", "investigating", "resolved", "ignored"] as const;
+const VALID_STATUSES = Object.keys(
+  ALLOWED_API_ERROR_STATUS_TRANSITIONS,
+) as readonly ApiErrorStatus[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/routes/admin/errors.ts` at line 28, VALID_STATUSES duplicates
the canonical workflow statuses and risks drifting; replace the hardcoded list
with the single source-of-truth (the ApiErrorStatus definition used by the
service transition rules) by importing that symbol and deriving VALID_STATUSES
from it (e.g., Object.values(ApiErrorStatus) or the exported union type), then
update any parsing/validation logic that references VALID_STATUSES to use the
imported ApiErrorStatus so future changes remain centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/api/src/routes/webhooks/sentry.ts`:
- Around line 197-205: Reorder and sanitize route selection so we prefer non-PII
transaction names first: check tags.transaction, then issue.metadata.transaction
(via metadata.transaction), then issue.shortId; only if those are missing fall
back to event.request.url but strip origin, query string and fragment (use only
the pathname) before returning, and still prepend request method
(request.method) if present; update the logic around request/request.url,
tags.transaction, metadata.transaction, and issue.shortId to reflect this
preference and sanitize the URL fallback.

---

Nitpick comments:
In `@server/api/src/routes/admin/errors.ts`:
- Around line 164-165: Replace the fragile regex message check with a proper
typed error from apiErrorService: import the dedicated error class (e.g.,
InvalidStatusTransitionError or the actual class exported by apiErrorService),
and change the condition in the error handler that currently tests err.message
to use "err instanceof <TypedErrorClass>" so you can return c.json({ error:
err.message }, 400) only for that type; leave other error branches unchanged so
non-matching errors still propagate as before.
- Line 28: VALID_STATUSES duplicates the canonical workflow statuses and risks
drifting; replace the hardcoded list with the single source-of-truth (the
ApiErrorStatus definition used by the service transition rules) by importing
that symbol and deriving VALID_STATUSES from it (e.g.,
Object.values(ApiErrorStatus) or the exported union type), then update any
parsing/validation logic that references VALID_STATUSES to use the imported
ApiErrorStatus so future changes remain centralized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 86a66ecf-7852-4414-9e41-fbfaa90a6171

📥 Commits

Reviewing files that changed from the base of the PR and between c9a3696 and 2f10a52.

📒 Files selected for processing (7)
  • server/api/.env.example
  • server/api/src/__tests__/routes/admin/errors.test.ts
  • server/api/src/__tests__/routes/webhooks/sentry.test.ts
  • server/api/src/app.ts
  • server/api/src/routes/admin/errors.ts
  • server/api/src/routes/admin/index.ts
  • server/api/src/routes/webhooks/sentry.ts

Comment thread server/api/src/routes/webhooks/sentry.ts Outdated
…803)

CodeRabbit review feedback (PR #812):

- routes/webhooks/sentry: reorder extractRoute to prefer the scrubbed
  `transaction` tag, then issue.metadata.transaction, then issue.shortId,
  before falling back to request.url. URL fallback now reduces the value
  to its pathname (origin / query / fragment dropped) so capability
  tokens and absolute hosts never persist into api_errors.route. Adds
  tests for the new ordering and the URL-stripping behavior.
- routes/admin/errors: derive VALID_STATUSES from
  ALLOWED_API_ERROR_STATUS_TRANSITIONS so adding a new ApiErrorStatus
  automatically expands the accepted PATCH inputs without requiring a
  manual edit on this list.
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.

[Epic] APIエラーの自動検知 → AI解析 → GitHub Issue 起票パイプラインの構築

2 participants