Skip to content

feat: クライアントエラーのGA計測とWorkersエラー観測性を追加 - #83

Merged
kuu13580 merged 2 commits into
mainfrom
feature/error-observability
Jul 6, 2026
Merged

feat: クライアントエラーのGA計測とWorkersエラー観測性を追加#83
kuu13580 merged 2 commits into
mainfrom
feature/error-observability

Conversation

@kuu13580

@kuu13580 kuu13580 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

API (Workers) ログには現れないクライアント側エラーと、サーバ側の未捕捉エラーを観測できるようにする。既存の GA4 (gtag.js) と Workers Logs を使い、追加の外部依存なしで実装。

フロント — GA4 client_error イベント (frontend/src/lib/analytics.ts 新規)

  • 画像レンダリング失敗: R2 presigned GET を表示する <img> 6箇所に onError(ギャラリー/詳細/ダッシュボード/送信完了/アバター)
  • 画像処理失敗: 送信時の変換パイプライン、送信前プレビュー生成の確定失敗
  • アップロード失敗: presigned PUT(R2 直 PUT、ステータス・ネット断/CORS を分類)
  • グローバル未捕捉: window.onerror / unhandledrejection / 動的 import チャンクロード失敗
  • 本番のみ送信・開発は debugLog のみ・署名付き URL / handle 等をサニタイズ・同一エラーはデデュープ

Workers — Workers Logs で観測 (workers/src/lib/logger.ts 新規)

  • app.onError で未捕捉例外を {error:{code:"INTERNAL"}} の 500 に統一し構造化ログ記録(従来はプレーンテキスト・ログ無し)
  • Cron を全ステップ個別 try/catch + per-item 分離、失敗を集約 throw(1 ステップ/1 件の失敗で全体が止まらない)
  • アカウント削除の背景 R2 削除の失敗をログ化(従来は完全サイレント)

Test plan

  • 本番相当ビルドで画像読み込みを失敗させ(ネット遮断 / 無効 URL)、GA4 DebugView に client_errorerror_kind=image_render)が届く
  • 開発環境ではエラーを誘発しても GA へ送信されず、?debug=truedebugLog に出るのみ
  • アップロード中のネット遮断で upload_put、HEIC 等の変換失敗で image_processing が届く
  • Workers で意図的に例外を起こし、500 が {error:{code:"INTERNAL"}} で返り、Workers Logs に構造化ログが出る

補足(運用)

GA4 レポートで error_kind / context 等を分解表示するには、GA4 管理画面「カスタム定義」でイベントスコープのカスタムディメンション登録が必要(DebugView / BigQuery には登録なしでも生値が届く)。

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 新機能
    • 画像の読み込み失敗やクライアント側エラー(未捕捉例外・未処理reject・チャンク/アップロード関連)をGA4で計測できるようにし、把握しやすくしました。
    • サムネイル/詳細/アップロード中の画像エラー時に計測とハンドリングを適用しました。
  • バグ修正
    • 未捕捉例外やcron失敗時のログ/応答を整理し、定期処理やアカウント削除、クリーンアップは失敗しても継続して記録されます。

API では補足できないクライアント側エラーと、サーバ側の未捕捉エラーを
観測できるようにする。

フロント (GA4 client_error イベント):
- 画像レンダリング失敗 (<img> の R2 GET 失敗)、画像処理/プレビュー生成失敗、
  presigned PUT 失敗、未捕捉例外/未処理reject/チャンクロード失敗を計測
- 本番のみ送信・開発は debugLog・署名URL/handle等をサニタイズ・デデュープ
  (lib/analytics.ts)

Workers (Workers Logs):
- app.onError で未捕捉例外を {error:{code:INTERNAL}} 500 に統一しログ記録
- Cron を全ステップ個別 try/catch + per-item 分離、失敗を集約 throw
- アカウント削除の背景 R2 削除の失敗をログ化 (lib/logger.ts)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

フロントエンドにクライアントエラー計測を追加し、画像読み込み失敗、アップロード失敗、起動時のグローバル例外をGA4へ送信するようにした。Workers側では構造化ロギング、未捕捉例外応答、cron継続処理、アカウント削除失敗記録、クォータ減算の分離を追加した。

Changes

フロントエンドのクライアントエラー計測

Layer / File(s) Summary
analytics.ts の計測基盤実装
frontend/src/lib/analytics.ts
client_error 計測の型、サニタイズ、抽出、重複抑止、画像ハンドラ、グローバル購読を追加。
アプリ起動時の初期化
frontend/src/main.tsx
React描画前にinitAnalytics()を実行するようにした。
画像onErrorハンドラの各ページ組み込み
frontend/src/pages/DashboardPage.tsx, GalleryPage.tsx, PhotoDetailPage.tsx, send/DonePage.tsx, send/LandingPage.tsx
各ページの画像にonImageErrorを配線した。
アップロードパイプラインへの計測組み込み
frontend/src/pages/send/UploadPage.tsx, send/UploadingPage.tsx
画像処理失敗、最終スイープ失敗、R2 PUT失敗をtrackClientErrorへ送るようにした。
GA初期化スクリプトの設定更新
frontend/index.html
page_location を追加してGA初期化を更新した。

Estimated code review effort: 3 (Moderate) | ~25 minutes

Workers側エラーハンドリングとロギング

Layer / File(s) Summary
構造化ロガーの新規実装
workers/src/lib/logger.ts
logError関数を追加し、Error/非ErrorをJSONで出力するようにした。
app.onErrorとscheduledのエラーハンドリング
workers/src/index.ts, docs/architecture.md
app.onErrorで未捕捉例外をINTERNAL(500)へ変換し、scheduledrunCleanup失敗をログ後に再throwするようにした。
cronクリーンアップの失敗集約と継続処理
workers/src/cron/cleanup.ts
runCleanupをステップ実行化し、各写真処理の失敗を記録して継続するようにした。
アカウント削除時のR2失敗ログ
workers/src/routes/auth.ts
R2バックグラウンド削除の失敗をlogErrorで記録するようにした。

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Window
  participant initAnalytics
  participant trackClientError
  participant gtag as window.gtag

  Window->>initAnalytics: error / unhandledrejection
  initAnalytics->>trackClientError: error_kind, context="global"
  trackClientError->>trackClientError: サニタイズ・重複抑止
  trackClientError->>gtag: client_error イベント送信(本番のみ)
Loading
sequenceDiagram
  participant Client
  participant Hono as OpenAPIHono
  participant onError as app.onError
  participant logger as logError

  Client->>Hono: リクエスト
  Hono--xonError: 未捕捉例外
  onError->>logger: logError("unhandled", err)
  onError-->>Client: JSON {code: INTERNAL} status 500
Loading

Poem

ぴょんと跳ねて 失敗をひろい
うさぎの耳で ログをきき
gtagへそっと 計測を届け
R2のつまずき ちゃんと記録
みんな安心、ぴょんぴょん運用 ✨

🚥 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 クライアントエラーのGA計測とWorkers側のエラー観測性追加という変更内容を正しく要約しています。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/error-observability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 3

🧹 Nitpick comments (3)
workers/src/cron/cleanup.ts (1)

36-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

汎用Errorではなく構造化エラーの使用を検討。

runCleanupの集約エラーは通常のErrorとして投げられています。このエラーはHTTPレスポンスには変換されずscheduled内でログ・再throwされるのみなので影響は限定的ですが、リポジトリ全体の一貫性の観点からは構造化エラーが望ましいです。

As per coding guidelines, workers/**/*.{ts,tsx,js,jsx}: "Throw structured errors with status codes instead of generic errors."

🤖 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 `@workers/src/cron/cleanup.ts` around lines 36 - 38, `runCleanup` currently
throws a generic Error when `failedSteps > 0`; update this to use the repo’s
structured error pattern with a status code instead of a plain Error. Keep the
same failure condition and message context, but replace the throw in `cleanup`
so the scheduled job can propagate a structured error consistently with the
`workers/**/*` guideline and existing error-handling conventions.

Source: Coding guidelines

frontend/src/pages/PhotoDetailPage.tsx (1)

175-196: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

任意: 画像読み込み失敗時にフォールバックUIへ切り替える仕組みがない

onError はGA4計測のみで、失敗後もUIは壊れた <img> のまま(view_url/thumb_url が存在する限り、200-204行目の「画像を読み込めません」表示には遷移しない)。計測自体は妥当だが、必要であれば onError 内で state を更新しフォールバック表示に切り替えることも検討の余地あり。

🤖 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 `@frontend/src/pages/PhotoDetailPage.tsx` around lines 175 - 196,
`PhotoDetailPage` の画像 `onError` は計測のみで、読み込み失敗後も `photo.thumb_url` /
`photo.view_url` がある限り壊れた `<img>` が表示され続けます。`onImageError` か `PhotoDetailPage`
内の表示ロジックでエラー状態を state 管理し、`thumb`/`original`
の失敗時に「画像を読み込めません」系のフォールバックUIへ切り替えるようにしてください。`onImageError`、`viewLoaded`、画像表示の条件分岐を目印に修正してください。
frontend/src/pages/send/UploadingPage.tsx (1)

473-478: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

任意: R2直PUTにタイムアウト設定がない

presigned URLへの直PUTに signal が指定されておらず、失敗しない限りブラウザ既定のタイムアウト(Chromeで約300秒など)までハングし得ます。モバイル回線での長時間停止を避けるため、AbortSignal.timeout() の付与を検討してください。ただし画像サイズ(最大20MB)を考慮し、極端に短い値は誤検知の原因になるため妥当な猶予(例: 60〜120秒)を設定するのが望ましいです。

♻️ 提案: フェッチにタイムアウトを追加
   try {
     res = await fetch(url, {
       method: "PUT",
       body: blob,
       headers: { "Content-Type": "image/jpeg" },
+      signal: AbortSignal.timeout(120_000),
     });
🤖 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 `@frontend/src/pages/send/UploadingPage.tsx` around lines 473 - 478, The direct
R2 PUT in UploadingPage’s fetch call does not set a timeout, so the upload can
hang until the browser default kicks in. Update the upload logic around the
existing fetch(url, { method: "PUT", body: blob, headers: { "Content-Type":
"image/jpeg" } }) call to pass a reasonable AbortSignal.timeout via signal,
using a conservative duration that fits up to 20MB images (for example, 60–120
seconds). Keep the timeout handling localized to the upload path so failures
abort cleanly and can be surfaced through the existing res handling.
🤖 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 `@frontend/src/lib/analytics.ts`:
- Around line 130-152: The GA4 config is currently sending the full default page
URL, including query parameters, so update the gtag initialization to override
page_location with a value built from window.location.origin plus
window.location.pathname and exclude the query string. Apply this in the
gtag('config', 'G-BE16TKNVZ5') setup used for client analytics, keeping the
change focused on the page_location/page_path handling so signed or access-key
URLs are not reported.

In `@frontend/src/pages/send/UploadingPage.tsx`:
- Around line 470-495: `putBlob` の `trackClientError` 呼び出しで、`upload_put` の重複抑止が
`original` と `thumb` を同一扱いにしてしまっています。`trackClientError` の dedupe key に
`target`(必要なら `http_status` も)を含めるよう、`putBlob` 内の fetch 失敗時と `!res.ok`
時のイベント送信を修正してください。

In `@workers/src/cron/cleanup.ts`:
- Around line 99-114: The cleanup flow in cleanupExpiredPhotos is making two
separate DB writes after the R2 deletes, which can leave storage usage and photo
rows out of sync on partial failure. Update the subtractStorageUsage and DELETE
FROM photos steps to run together through env.DB.batch() so they are applied
atomically, while keeping the existing error handling and logging around the
cleanup loop.

---

Nitpick comments:
In `@frontend/src/pages/PhotoDetailPage.tsx`:
- Around line 175-196: `PhotoDetailPage` の画像 `onError` は計測のみで、読み込み失敗後も
`photo.thumb_url` / `photo.view_url` がある限り壊れた `<img>` が表示され続けます。`onImageError` か
`PhotoDetailPage` 内の表示ロジックでエラー状態を state 管理し、`thumb`/`original`
の失敗時に「画像を読み込めません」系のフォールバックUIへ切り替えるようにしてください。`onImageError`、`viewLoaded`、画像表示の条件分岐を目印に修正してください。

In `@frontend/src/pages/send/UploadingPage.tsx`:
- Around line 473-478: The direct R2 PUT in UploadingPage’s fetch call does not
set a timeout, so the upload can hang until the browser default kicks in. Update
the upload logic around the existing fetch(url, { method: "PUT", body: blob,
headers: { "Content-Type": "image/jpeg" } }) call to pass a reasonable
AbortSignal.timeout via signal, using a conservative duration that fits up to
20MB images (for example, 60–120 seconds). Keep the timeout handling localized
to the upload path so failures abort cleanly and can be surfaced through the
existing res handling.

In `@workers/src/cron/cleanup.ts`:
- Around line 36-38: `runCleanup` currently throws a generic Error when
`failedSteps > 0`; update this to use the repo’s structured error pattern with a
status code instead of a plain Error. Keep the same failure condition and
message context, but replace the throw in `cleanup` so the scheduled job can
propagate a structured error consistently with the `workers/**/*` guideline and
existing error-handling conventions.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f382dfe9-4ed7-4eb5-ab48-612881218add

📥 Commits

Reviewing files that changed from the base of the PR and between 0ecc3a3 and 1203e0a.

📒 Files selected for processing (15)
  • docs/architecture.md
  • frontend/index.html
  • frontend/src/lib/analytics.ts
  • frontend/src/main.tsx
  • frontend/src/pages/DashboardPage.tsx
  • frontend/src/pages/GalleryPage.tsx
  • frontend/src/pages/PhotoDetailPage.tsx
  • frontend/src/pages/send/DonePage.tsx
  • frontend/src/pages/send/LandingPage.tsx
  • frontend/src/pages/send/UploadPage.tsx
  • frontend/src/pages/send/UploadingPage.tsx
  • workers/src/cron/cleanup.ts
  • workers/src/index.ts
  • workers/src/lib/logger.ts
  • workers/src/routes/auth.ts

Comment thread frontend/src/lib/analytics.ts
Comment thread frontend/src/pages/send/UploadingPage.tsx
Comment thread workers/src/cron/cleanup.ts
- index.html: gtag config に page_location=origin+pathname を設定し
  /send/:handle?k=KEY のアクセスキーが GA4 page_view に載るのを防止
- analytics.ts: client_error の dedupe key に target/http_status を追加し
  upload_put の original/thumb を別カウント
- cleanup.ts + quota.ts: cleanupExpiredPhotos の storage_used 減算と
  photos 削除を DB.batch() で原子化 (subtractStorageUsageStmt を追加)
- UploadingPage.tsx: R2 直 PUT に AbortSignal.timeout(120s) を付与

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kuu13580

kuu13580 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai レビュー本文の nitpick 3件に対応しました(commit 5fb26d5):

  • R2 直 PUT のタイムアウト: putBlob の fetch に AbortSignal.timeout(120_000) を追加しました。
  • 💭 runCleanup の集約エラーを structured error に: 見送りました。この throw は scheduled 内でログ出力+再 throw されるのみで HTTP レスポンスに変換されないため、status code を持つ構造化エラーは文脈に合わないと判断しました。
  • 💭 PhotoDetail の onError で fallback UI へ切替: 見送りました。本 PR は「エラー計測の追加」がスコープであり、UI 挙動変更は別トピックのため最小変更に留めました。

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@kuu13580 対応ありがとうございます。確認しました。

  • R2 直 PUT のタイムアウト: AbortSignal.timeout(120_000) の追加、問題ありません。無限に待ち続けるリクエストを防げるようになりましたね。
  • 👍 runCleanup の集約エラー: 納得しました。scheduled 内でログ出力+再 throw されるだけで HTTP レスポンスに変換されない以上、status code を持たせる意味はないですね。現状の実装(プレーンな Error 集約)で妥当だと思います。
  • 👍 PhotoDetail の fallback UI: こちらも同意します。本 PR のスコープは計測の追加であり、UI 挙動の変更(フォールバック画像など)は別 PR で扱う方が変更範囲が明確になりますね。

いずれも妥当な判断だと思います。ありがとうございました!

🐰✨

@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.

🧹 Nitpick comments (1)
frontend/src/lib/analytics.ts (1)

130-152: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

dedupeKey の拡張は妥当。

target/http_status を追加したことで、original/thumbnail の upload_put エラーが正しく別カウントされるようになっています。PR の意図通りです。

一点補足として、sentCounts はページのライフタイム全体で保持される Map で、error_message がキーに含まれるため、異なるスタックトレース等でメッセージが変化すると distinct キーが増え続けます。DEDUP_LIMIT は単一キーの送信回数のみを制限しており、Map 自体のエントリ数上限は無いため、長時間開いたままのタブでは緩やかにメモリが増加します。実害は小さいですが、必要であれば Map のサイズ上限や定期リセットを検討してください。

🤖 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 `@frontend/src/lib/analytics.ts` around lines 130 - 152, The dedupeKey
expansion in trackClientError is correct, but sentCounts can grow without bound
because error_message makes each distinct message create a new Map entry. Update
the trackClientError flow to cap or recycle sentCounts entries in a bounded way,
such as adding a maximum size or periodic reset/eviction policy around the
sentCounts Map, while keeping the existing dedupe behavior for target and
http_status.
🤖 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.

Nitpick comments:
In `@frontend/src/lib/analytics.ts`:
- Around line 130-152: The dedupeKey expansion in trackClientError is correct,
but sentCounts can grow without bound because error_message makes each distinct
message create a new Map entry. Update the trackClientError flow to cap or
recycle sentCounts entries in a bounded way, such as adding a maximum size or
periodic reset/eviction policy around the sentCounts Map, while keeping the
existing dedupe behavior for target and http_status.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7373f328-71fe-4531-b50d-9468bd7a8958

📥 Commits

Reviewing files that changed from the base of the PR and between 1203e0a and 5fb26d5.

📒 Files selected for processing (5)
  • frontend/index.html
  • frontend/src/lib/analytics.ts
  • frontend/src/pages/send/UploadingPage.tsx
  • workers/src/cron/cleanup.ts
  • workers/src/lib/quota.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • workers/src/cron/cleanup.ts
  • frontend/src/pages/send/UploadingPage.tsx

@kuu13580
kuu13580 merged commit 62f492b into main Jul 6, 2026
6 checks passed
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