-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add api_errors table and service for Sentry error aggregation #811
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
809d82a
feat(api,db): add api_errors table and apiErrorService (Epic #616 Pha…
claude 22f42b1
fix(api): harden api_errors upsert/update and add DB enum checks
claude 0aec113
fix(api): validate statusCode/timestamps on api_errors upsert
claude ff83b9d
fix(api): coerce listApiErrors pagination to integers and reattach TSDoc
claude 5c54cad
fix(api): enforce firstSeenAt <= lastSeenAt monotonicity on upsert
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| -- Add `api_errors` table — aggregated summary of API errors detected by Sentry. | ||
| -- Sentry が検知した API エラーの集約サマリ用テーブルを追加する。 | ||
| -- | ||
| -- 生のスタックトレース・パラメータは Sentry 側に保持し、本テーブルでは | ||
| -- `sentry_issue_id` をユニークキーとした「issue 単位の状態」のみ持つ。 | ||
| -- Webhook ハンドラは `INSERT ... ON CONFLICT (sentry_issue_id) DO UPDATE` で | ||
| -- `occurrences` を加算し `last_seen_at` を前進させる(`first_seen_at` は保持)。 | ||
| -- | ||
| -- Raw stack traces / payloads stay in Sentry; this table only stores the | ||
| -- per-issue aggregation (occurrence count, severity, status, AI analysis, | ||
| -- GitHub issue mapping) keyed on `sentry_issue_id`. The webhook upserts via | ||
| -- `ON CONFLICT (sentry_issue_id) DO UPDATE` to bump `occurrences` and advance | ||
| -- `last_seen_at` while preserving `first_seen_at`. | ||
| -- | ||
| -- See parent epic otomatty/zedi#616, sub-issue #802. | ||
|
|
||
| CREATE TABLE IF NOT EXISTS "api_errors" ( | ||
| "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, | ||
| "sentry_issue_id" text NOT NULL, | ||
| "fingerprint" text, | ||
| "title" text NOT NULL, | ||
| "route" text, | ||
| "status_code" integer, | ||
| "occurrences" integer DEFAULT 1 NOT NULL, | ||
| "first_seen_at" timestamp with time zone DEFAULT now() NOT NULL, | ||
| "last_seen_at" timestamp with time zone DEFAULT now() NOT NULL, | ||
| "severity" text DEFAULT 'unknown' NOT NULL, | ||
| "status" text DEFAULT 'open' NOT NULL, | ||
| "ai_summary" text, | ||
| "ai_suspected_files" jsonb, | ||
| "ai_root_cause" text, | ||
| "ai_suggested_fix" text, | ||
| "github_issue_number" integer, | ||
| "created_at" timestamp with time zone DEFAULT now() NOT NULL, | ||
| "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | ||
| CONSTRAINT "api_errors_sentry_issue_id_unique" UNIQUE ("sentry_issue_id") | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE INDEX IF NOT EXISTS "idx_api_errors_status_last_seen" | ||
| ON "api_errors" ("status", "last_seen_at" DESC); | ||
| --> statement-breakpoint | ||
| CREATE INDEX IF NOT EXISTS "idx_api_errors_severity_status" | ||
| ON "api_errors" ("severity", "status"); | ||
| --> statement-breakpoint | ||
| CREATE INDEX IF NOT EXISTS "idx_api_errors_last_seen" | ||
| ON "api_errors" ("last_seen_at" DESC); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| /** | ||
| * `api_errors` テーブル — Sentry が検知した API エラーの集約サマリ。 | ||
| * 生のスタックトレース・パラメータは Sentry 側に保持し、本テーブルでは | ||
| * 重複排除済みの「issue 単位の状態」を保持する。Epic #616 / Sub-issue #802 に準拠。 | ||
| * | ||
| * `api_errors` table — aggregated summary of API errors detected by Sentry. | ||
| * Raw stack traces and request payloads stay in Sentry; this table only stores | ||
| * the deduplicated "per-issue state" (occurrence count, severity, status, | ||
| * AI analysis output, GitHub issue mapping). See Epic #616 / sub-issue #802. | ||
| * | ||
| * @see https://github.com/otomatty/zedi/issues/616 | ||
| * @see https://github.com/otomatty/zedi/issues/802 | ||
| */ | ||
| import { pgTable, uuid, text, integer, timestamp, jsonb, index } from "drizzle-orm/pg-core"; | ||
|
|
||
| /** | ||
| * AI が判定する重大度。Issue 自動起票は `high` / `medium` のみが対象になる | ||
| * (`low` は集約のみ、`unknown` は AI 解析未完了の暫定値)。 | ||
| * | ||
| * AI-assigned severity. Issue auto-creation only triggers for `high` / `medium`. | ||
| * `low` is aggregated but never escalated; `unknown` is the default before AI | ||
| * analysis finishes. | ||
| */ | ||
| export type ApiErrorSeverity = "high" | "medium" | "low" | "unknown"; | ||
|
|
||
| /** | ||
| * 管理者が更新するエラーのワークフロー状態。 | ||
| * Workflow status updated by an admin via the management UI. | ||
| * | ||
| * - `open`: 新規検出・未対応 / Newly detected, untriaged | ||
| * - `investigating`: 調査中 / Currently being investigated | ||
| * - `resolved`: 解決済み(再発時は `open` に戻す) / Fixed; reopened on regression | ||
| * - `ignored`: 既知だが対応不要と判断 / Known and intentionally ignored | ||
| */ | ||
| export type ApiErrorStatus = "open" | "investigating" | "resolved" | "ignored"; | ||
|
|
||
| /** | ||
| * AI が推定した「関連しそうなファイル」のエントリ。 | ||
| * Suspected file entry produced by the AI analysis step. | ||
| */ | ||
| export interface ApiErrorSuspectedFile { | ||
| /** リポジトリ相対パス / Repository-relative path */ | ||
| path: string; | ||
| /** 関連と推定する根拠(任意) / Optional rationale */ | ||
| reason?: string; | ||
| /** 行番号(任意) / Optional line number */ | ||
| line?: number; | ||
| } | ||
|
|
||
| /** | ||
| * `api_errors` テーブル定義。`sentry_issue_id` をユニークキーにして upsert する。 | ||
| * | ||
| * Drizzle definition for the `api_errors` table. `sentry_issue_id` is the | ||
| * idempotency key used by `apiErrorService.upsertFromSentrySummary` so the | ||
| * same Sentry issue cannot create duplicate rows when alerts fire repeatedly. | ||
| */ | ||
| export const apiErrors = pgTable( | ||
| "api_errors", | ||
| { | ||
| id: uuid("id").primaryKey().defaultRandom(), | ||
| /** Sentry の issue ID(`group.id`)。upsert キー / Sentry issue id; upsert key */ | ||
| sentryIssueId: text("sentry_issue_id").notNull().unique(), | ||
| /** Sentry の fingerprint または自前計算したグルーピングキー / Grouping fingerprint */ | ||
| fingerprint: text("fingerprint"), | ||
| /** エラー要約タイトル / Short error title */ | ||
| title: text("title").notNull(), | ||
| /** 発生したルート(例: `POST /api/ingest`) / Route where the error fired */ | ||
| route: text("route"), | ||
| /** HTTP ステータスコード / HTTP status code */ | ||
| statusCode: integer("status_code"), | ||
| /** 集約済み発生回数(alert ごとに加算)/ Total occurrences across alerts */ | ||
| occurrences: integer("occurrences").notNull().default(1), | ||
| /** 初回観測時刻(upsert で更新しない)/ First-seen timestamp; preserved on upsert */ | ||
| firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).defaultNow().notNull(), | ||
| /** 最終観測時刻(upsert で前進する) / Last-seen timestamp; advances on upsert */ | ||
| lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).defaultNow().notNull(), | ||
| /** AI 解析後の重大度(既定 `unknown`) / Severity after AI analysis */ | ||
| severity: text("severity").$type<ApiErrorSeverity>().notNull().default("unknown"), | ||
| /** 管理画面で人が更新するワークフロー状態 / Admin-updated workflow status */ | ||
| status: text("status").$type<ApiErrorStatus>().notNull().default("open"), | ||
| /** AI が生成した要約 / AI-generated summary */ | ||
| aiSummary: text("ai_summary"), | ||
| /** AI が推定した関連ファイル一覧 / AI-suspected related files */ | ||
| aiSuspectedFiles: jsonb("ai_suspected_files").$type<ApiErrorSuspectedFile[]>(), | ||
| /** AI が推定した原因仮説 / AI-suspected root cause */ | ||
| aiRootCause: text("ai_root_cause"), | ||
| /** AI が推奨する修正方針 / AI-suggested fix direction */ | ||
| aiSuggestedFix: text("ai_suggested_fix"), | ||
| /** 自動起票した GitHub Issue 番号(low / 起票前は null) / Linked GitHub issue number */ | ||
| githubIssueNumber: integer("github_issue_number"), | ||
| createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), | ||
| updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), | ||
| }, | ||
| (table) => [ | ||
| // Admin UI の主クエリは「未対応を新着順で見る」なのでこれを先頭インデックスに置く。 | ||
| // The admin UI's primary query lists open issues newest-first, so this | ||
| // composite index covers the hot path. | ||
| index("idx_api_errors_status_last_seen").on(table.status, table.lastSeenAt.desc()), | ||
| // severity フィルタ + status の絞り込み(severity:high & status:open 等)。 | ||
| // For severity-filtered admin queries (e.g. severity=high & status=open). | ||
| index("idx_api_errors_severity_status").on(table.severity, table.status), | ||
| // last_seen_at だけでの新着順表示用 / For straight newest-first listings. | ||
| index("idx_api_errors_last_seen").on(table.lastSeenAt.desc()), | ||
| ], | ||
| ); | ||
|
|
||
| /** SELECT 行型 / Row type for `api_errors` SELECT results. */ | ||
| export type ApiError = typeof apiErrors.$inferSelect; | ||
|
|
||
| /** INSERT 値型 / Insert payload type for `api_errors`. */ | ||
| export type NewApiError = typeof apiErrors.$inferInsert; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.