feat: Add API errors admin page with real-time polling and Sentry integration - #813
Conversation
…/errors UI (#804) Wires up `@sentry/react` for the frontend (`src/`) and admin (`admin/`) SPAs with `Error Boundary` to forward unhandled exceptions, both gated on the `VITE_SENTRY_DSN_WEB` / `VITE_ADMIN_SENTRY_DSN` env vars (no-op when blank). Adds an admin `/errors` page (list + detail + status update) backed by the existing `/api/admin/errors` endpoints, plus a sidebar badge that polls for the count of active (`open` + `investigating`) errors. Phase 1 uses simple polling at a 30s interval; realtime sync is deferred to a later phase.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
✅ Files skipped from review due to trivial changes (4)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughAdds Sentry initialization and ErrorBoundary to web and admin frontends; implements an admin "API errors" feature including client APIs, polling hooks, list/detail UI with status updates, i18n, tests, Playwright E2E, layout wiring, and related config and dependency updates. ChangesError Tracking & Admin API Errors Feature
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin SPA
participant API as Backend API
participant Sentry as Sentry SDK
Admin->>Admin: initSentry() (reads VITE_ADMIN_SENTRY_DSN)
alt DSN set
Admin->>Sentry: Sentry.init(...)
end
Admin->>API: GET /api/admin/errors (useApiErrors)
API-->>Admin: { errors, total }
Admin->>Admin: render ErrorsContent (badges, table)
Admin->>API: PATCH /api/admin/errors/{id} (patchApiErrorStatus)
API-->>Admin: { error: updatedRow }
alt Uncaught render error
Admin->>Sentry: captureException(error, { extra: { componentStack } })
Admin-->>Admin: ErrorBoundary shows fallback UI
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements Sentry integration across the application and introduces a new API error management dashboard in the admin SPA. Key changes include the addition of a global ErrorBoundary component, Sentry initialization logic, and a dedicated Errors page that allows administrators to view detailed error reports and update their workflow status. The admin sidebar has also been updated with a dynamic badge to track active errors. Feedback was provided regarding a potential race condition in the useApiErrorActiveCount hook, where overlapping asynchronous requests might lead to inconsistent state updates if not properly tracked.
| const isMountedRef = useRef(true); | ||
|
|
||
| useEffect(() => { | ||
| isMountedRef.current = true; | ||
| let cancelled = false; | ||
|
|
||
| const fetchCount = async () => { | ||
| try { | ||
| const results = await Promise.all( | ||
| ACTIVE_API_ERROR_STATUSES.map((status) => getApiErrors({ status, limit: 1 })), | ||
| ); | ||
| if (cancelled || !isMountedRef.current) return; | ||
| const total = results.reduce((sum, r) => sum + r.total, 0); | ||
| setCount(total); | ||
| } catch { | ||
| // バッジ取得失敗時は表示を更新しない(古い値を維持して誤った 0 表示を避ける)。 | ||
| // Swallow errors to keep stale-but-correct count rather than flashing 0. | ||
| } | ||
| }; |
There was a problem hiding this comment.
fetchCount 関数において、複数の非同期リクエストが並行して走った際のレースコンディションへの考慮が不足しています。例えば、ポーリングによる実行と visibilitychange による実行が重なった場合、古いリクエストの結果が新しい結果を上書きしてしまう可能性があります。useApiErrors.ts で実装されているように、useRef を用いて最新のリクエスト ID を追跡し、古いリクエストの結果を破棄する仕組みを導入することを推奨します。
const isMountedRef = useRef(true);
const latestRequestRef = useRef(0);
useEffect(() => {
isMountedRef.current = true;
let cancelled = false;
const fetchCount = async () => {
const requestId = ++latestRequestRef.current;
try {
const results = await Promise.all(
ACTIVE_API_ERROR_STATUSES.map((status) => getApiErrors({ status, limit: 1 })),
);
if (cancelled || !isMountedRef.current || requestId !== latestRequestRef.current) return;
const total = results.reduce((sum, r) => sum + r.total, 0);
setCount(total);
} catch {
// バッジ取得失敗時は表示を更新しない(古い値を維持して誤った 0 表示を避ける)。
// Swallow errors to keep stale-but-correct count rather than flashing 0.
}
};Polling ticks and visibilitychange callbacks can race. Track the latest request id (mirroring `useApiErrors`) and discard stale responses so a slow earlier fetch can't overwrite a fresher count. Addresses gemini-code-assist review on PR #813.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
admin/src/pages/errors/useApiErrorActiveCount.ts (1)
25-32: ⚡ Quick winPrevent older polls from overwriting newer counts.
fetchCount()can run concurrently from the interval and thevisibilitychangerefresh. A slower older request can resolve last and replace a fresher count.Suggested sequencing guard
export function useApiErrorActiveCount(): number { const [count, setCount] = useState(0); const isMountedRef = useRef(true); + const requestIdRef = useRef(0); useEffect(() => { isMountedRef.current = true; let cancelled = false; const fetchCount = async () => { + const requestId = ++requestIdRef.current; try { const results = await Promise.all( ACTIVE_API_ERROR_STATUSES.map((status) => getApiErrors({ status, limit: 1 })), ); - if (cancelled || !isMountedRef.current) return; + if (cancelled || !isMountedRef.current || requestId !== requestIdRef.current) return; const total = results.reduce((sum, r) => sum + r.total, 0); setCount(total); } catch {Also applies to: 39-48
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@admin/src/pages/errors/useApiErrorActiveCount.ts` around lines 25 - 32, fetchCount can complete out-of-order and overwrite newer counts; add a sequencing guard: maintain a fetch sequence counter ref (e.g., lastFetchIdRef) and increment it at the start of fetchCount, capture the current id in a local variable, and after awaiting the API calls check that the captured id === lastFetchIdRef.current (and that cancelled/isMountedRef checks still pass) before computing total and calling setCount; apply the same pattern to the other refresh path (the visibilitychange refresh) so only the latest fetch result updates state.src/components/ErrorBoundary.tsx (2)
1-35: LGTM!Implementation is correct and mirrors the admin version. Both components are well-typed with comprehensive bilingual documentation.
Optional future consideration: These two
ErrorBoundaryimplementations are nearly identical. Consider extracting to a shared UI package to reduce duplication.Also applies to: 52-75
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ErrorBoundary.tsx` around lines 1 - 35, The ErrorBoundary class is duplicated across admin and this component; refactor by extracting the ErrorBoundary (the exported class ErrorBoundary with its props and state interfaces and methods like static getDerivedStateFromError and any lifecycle error handling) into a shared UI package/module and replace both local implementations with imports from that shared module, keeping the same public API (ErrorBoundaryProps, ErrorBoundaryState, fallback prop signature) so callers need no changes.
38-50: ⚡ Quick winSame recommendation: forward
componentStackto Sentry.As noted in the admin counterpart, passing
info.componentStackto Sentry would improve error debugging.♻️ Proposed enhancement
componentDidCatch(error: Error, info: ErrorInfo): void { try { - captureException(error); + captureException(error, { extra: { componentStack: info.componentStack } }); } catch { // 失敗時もフォールバック描画は継続する。 // Continue rendering the fallback even if reporting fails. }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ErrorBoundary.tsx` around lines 38 - 50, The ErrorBoundary.componentDidCatch currently calls captureException(error) without passing the component stack; update componentDidCatch to forward info.componentStack to the Sentry helper (captureException) so the component stack is recorded—modify the call in componentDidCatch (and the captureException helper signature if needed) to accept and forward the componentStack (info.componentStack) to Sentry/SDK as event context or as the second argument.admin/src/components/ErrorBoundary.tsx (1)
38-50: ⚡ Quick winConsider forwarding
componentStackto Sentry for richer debugging context.The
info.componentStackis available but not passed tocaptureException. Including it would help diagnose which component tree path led to the error.♻️ Proposed enhancement to include component stack
componentDidCatch(error: Error, info: ErrorInfo): void { try { - captureException(error); + captureException(error, { extra: { componentStack: info.componentStack } }); } catch { // 失敗時もフォールバック描画は継続する。 // Continue rendering the fallback even if reporting fails. }Note: This requires updating
captureExceptionin@/lib/sentryto accept and forward options to the Sentry SDK.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@admin/src/components/ErrorBoundary.tsx` around lines 38 - 50, Forward the component stack from ErrorBoundary to Sentry by updating componentDidCatch to pass info.componentStack into captureException (in ErrorBoundary.componentDidCatch) and then update the captureException helper in "@/lib/sentry" to accept an options/metadata parameter and forward that to the Sentry SDK (e.g., as extra or contexts) so the componentStack is sent; ensure the try/catch behavior remains and include the componentStack only when present to avoid breaking uninitialized/no-op behavior.admin/src/pages/errors/ErrorDetailDialog.tsx (1)
21-21: 💤 Low valueConsider extracting
STATUS_VALUESto a shared location.This array is duplicated in both
ErrorsContent.tsxandErrorDetailDialog.tsx. Extracting to a shared constants file (e.g.,@/api/admin.tsalongside the types) would ensure consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@admin/src/pages/errors/ErrorDetailDialog.tsx` at line 21, STATUS_VALUES is duplicated between ErrorDetailDialog.tsx and ErrorsContent.tsx; extract this constant into a shared location (e.g., alongside ApiErrorStatus in `@/api/admin.ts`) and import it from both components. Update ErrorDetailDialog.tsx and ErrorsContent.tsx to remove the local const and import STATUS_VALUES from the shared module, keeping the type ApiErrorStatus for the array to preserve type safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@admin/src/i18n/locales/en/errors.json`:
- Line 37: The "occurrencesShort" translation always returns "{{count}}
occurrences" which yields "1 occurrences"; update the value to handle
pluralization for the singular case (e.g. use ICU plural syntax or the project's
i18n plural keys). Replace the value of the occurrencesShort key with a
plural-aware string such as "{{count, plural, one {# occurrence} other {#
occurrences}}}" or, if your system uses separate plural keys, add a singular key
(occurrencesShort_one: "{{count}} occurrence") and a plural key
(occurrencesShort_other: "{{count}} occurrences") and ensure the lookup uses
those keys.
In `@admin/src/i18n/locales/en/nav.json`:
- Line 5: Update the unreadBadgeAriaLabel value to be self-describing by
including what is unresolved (e.g. change the value of the
"unreadBadgeAriaLabel" key from "{{count}} unresolved" to something like
"{{count}} unresolved API errors" or another specific descriptor used in the UI)
so screen readers announce the item type along with the count.
---
Nitpick comments:
In `@admin/src/components/ErrorBoundary.tsx`:
- Around line 38-50: Forward the component stack from ErrorBoundary to Sentry by
updating componentDidCatch to pass info.componentStack into captureException (in
ErrorBoundary.componentDidCatch) and then update the captureException helper in
"@/lib/sentry" to accept an options/metadata parameter and forward that to the
Sentry SDK (e.g., as extra or contexts) so the componentStack is sent; ensure
the try/catch behavior remains and include the componentStack only when present
to avoid breaking uninitialized/no-op behavior.
In `@admin/src/pages/errors/ErrorDetailDialog.tsx`:
- Line 21: STATUS_VALUES is duplicated between ErrorDetailDialog.tsx and
ErrorsContent.tsx; extract this constant into a shared location (e.g., alongside
ApiErrorStatus in `@/api/admin.ts`) and import it from both components. Update
ErrorDetailDialog.tsx and ErrorsContent.tsx to remove the local const and import
STATUS_VALUES from the shared module, keeping the type ApiErrorStatus for the
array to preserve type safety.
In `@admin/src/pages/errors/useApiErrorActiveCount.ts`:
- Around line 25-32: fetchCount can complete out-of-order and overwrite newer
counts; add a sequencing guard: maintain a fetch sequence counter ref (e.g.,
lastFetchIdRef) and increment it at the start of fetchCount, capture the current
id in a local variable, and after awaiting the API calls check that the captured
id === lastFetchIdRef.current (and that cancelled/isMountedRef checks still
pass) before computing total and calling setCount; apply the same pattern to the
other refresh path (the visibilitychange refresh) so only the latest fetch
result updates state.
In `@src/components/ErrorBoundary.tsx`:
- Around line 1-35: The ErrorBoundary class is duplicated across admin and this
component; refactor by extracting the ErrorBoundary (the exported class
ErrorBoundary with its props and state interfaces and methods like static
getDerivedStateFromError and any lifecycle error handling) into a shared UI
package/module and replace both local implementations with imports from that
shared module, keeping the same public API (ErrorBoundaryProps,
ErrorBoundaryState, fallback prop signature) so callers need no changes.
- Around line 38-50: The ErrorBoundary.componentDidCatch currently calls
captureException(error) without passing the component stack; update
componentDidCatch to forward info.componentStack to the Sentry helper
(captureException) so the component stack is recorded—modify the call in
componentDidCatch (and the captureException helper signature if needed) to
accept and forward the componentStack (info.componentStack) to Sentry/SDK as
event context or as the second argument.
🪄 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: 8e949e21-a6f5-435b-a1f2-14716483e224
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (28)
.env.exampleadmin/.env.exampleadmin/e2e/errors-page.spec.tsadmin/package.jsonadmin/playwright.config.tsadmin/src/App.tsxadmin/src/api/admin.test.tsadmin/src/api/admin.tsadmin/src/components/ErrorBoundary.test.tsxadmin/src/components/ErrorBoundary.tsxadmin/src/i18n/index.tsadmin/src/i18n/locales/en/errors.jsonadmin/src/i18n/locales/en/nav.jsonadmin/src/i18n/locales/ja/errors.jsonadmin/src/i18n/locales/ja/nav.jsonadmin/src/lib/sentry.tsadmin/src/main.tsxadmin/src/pages/Layout.tsxadmin/src/pages/errors/ErrorDetailDialog.tsxadmin/src/pages/errors/ErrorsContent.test.tsxadmin/src/pages/errors/ErrorsContent.tsxadmin/src/pages/errors/index.tsxadmin/src/pages/errors/useApiErrorActiveCount.tsadmin/src/pages/errors/useApiErrors.tspackage.jsonsrc/components/ErrorBoundary.tsxsrc/lib/sentry.tssrc/main.tsx
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@admin/src/pages/errors/useApiErrorActiveCount.ts`:
- Around line 15-16: The JSDoc for the hook useApiErrorActiveCount is incorrect
about failure behavior; instead of always falling back to 0 on fetch/auth errors
the implementation preserves the previous successful count (see the logic around
the block that retains previous count). Update the comment to state that the
hook returns the last known successful count on fetch/auth errors (and falls
back to 0 only if there was never a successful value), or alternatively change
the implementation to match the current doc—whichever contract you want to keep;
reference the hook name useApiErrorActiveCount and the code path that retains
previous count when editing.
- Around line 47-51: The initial unconditional call to fetchCount causes a fetch
even when the tab is hidden; modify the bootstrap logic in
useApiErrorActiveCount so that you only call fetchCount() immediately if
document is defined and document.hidden is false (i.e., guard the initial void
fetchCount() with the same "typeof document !== 'undefined' && !document.hidden"
check used inside the setInterval), leaving the interval and
API_ERRORS_POLL_INTERVAL_MS behavior unchanged.
🪄 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: 05961ace-b8ab-4648-b90a-f7cdf06a6630
📒 Files selected for processing (1)
admin/src/pages/errors/useApiErrorActiveCount.ts
- i18n: add `_one` / `_other` plural keys for `errors.detail.occurrencesShort`
(was rendering "1 occurrences"). Pass `count` as a number so i18next plural
resolution actually fires.
- i18n: make `nav.unreadBadgeAriaLabel` self-describing for screen readers
("{{count}} unresolved API errors").
- sentry: extend `captureException` with an optional `{ extra }` context and
forward `componentStack` from both `ErrorBoundary` components so Sentry events
capture the failing subtree.
- admin/src/api/admin.ts: expose `API_ERROR_STATUS_VALUES` and
`API_ERROR_SEVERITY_VALUES` as the single source of truth; drop the local
copies in `ErrorsContent` and `ErrorDetailDialog`.
- useApiErrorActiveCount: fix JSDoc to match actual behavior (preserves last
successful count on errors), and guard the bootstrap fetch with the same
visibility check as the polling tick so a hidden tab doesn't pay for the
initial fan-out.
Skipped: extracting `ErrorBoundary` into a shared package — out of scope here.
概要
管理画面に API エラー一覧ページを追加し、Sentry から取得したエラーを可視化・管理できるようにしました。エラーの詳細表示、AI 解析結果の確認、ワークフロー状態(open/investigating/resolved/ignored)の更新が可能です。また、フロントエンド全体に Sentry エラーバウンダリーを導入し、未捕捉例外の自動報告を実装しました。
変更点
管理画面エラー一覧ページ (
admin/src/pages/errors/)ErrorsContent.tsx: ステータス・重大度フィルター、テーブル表示、バッジ色分けErrorDetailDialog.tsx: AI 解析結果(要約・原因・修正方針・関連ファイル)の詳細表示とステータス更新useApiErrors.ts:GET /api/admin/errorsの定期ポーリング(30秒間隔、タブ非表示時スキップ)useApiErrorActiveCount.ts: サイドバーバッジ用の軽量件数取得(active ステータスのみ)index.tsx: コンテナコンポーネント(状態管理・API 呼び出し)API クライアント (
admin/src/api/admin.ts)getApiErrors(): フィルター・ページネーション対応の一覧取得getApiErrorById(): 単一エラー詳細取得patchApiErrorStatus(): ワークフロー状態更新ApiErrorStatus,ApiErrorSeverity,ApiErrorRow,ApiErrorSuspectedFileSentry 統合
src/lib/sentry.ts/admin/src/lib/sentry.ts: SDK 初期化(DSN 未設定時は no-op)ErrorBoundaryコンポーネント(src//admin/src/): 未捕捉例外をキャッチして Sentry に報告main.tsx両方: ErrorBoundary でアプリをラップUI・ナビゲーション
admin/src/pages/Layout.tsx: サイドバーに「API エラー」メニュー追加、未対応件数バッジ表示admin/src/App.tsx:/errorsルート追加多言語対応
admin/src/i18n/locales/{ja,en}/errors.json: エラー一覧・詳細画面の全文言admin/src/i18n/locales/{ja,en}/nav.json: ナビゲーション文言追加テスト・E2E
admin/src/pages/errors/ErrorsContent.test.tsx: UI レンダリング・フィルター動作admin/src/components/ErrorBoundary.test.tsx: 例外キャッチ・Sentry 報告admin/src/api/admin.test.ts: API クライアント関数admin/e2e/errors-page.spec.ts: ページ全体の最小 E2E(モック API)admin/playwright.config.ts: 管理画面専用 Playwright 設定環境変数・パッケージ
.env.example/admin/.env.example: `https://claude.ai/code/session_01XDYFEoEyfjbSQK5KXXA7yK
Summary by CodeRabbit
New Features
Monitoring
Tests
Documentation