Skip to content

chore(#207): 通知タップ切り分け用 診断ロガー [DO NOT MERGE] - #216

Draft
kuu13580 wants to merge 14 commits into
developfrom
chore/issue207_debug-logger
Draft

chore(#207): 通知タップ切り分け用 診断ロガー [DO NOT MERGE]#216
kuu13580 wants to merge 14 commits into
developfrom
chore/issue207_debug-logger

Conversation

@kuu13580

@kuu13580 kuu13580 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Related: #207, #209

目的

PR #209 の実装で「PWA background + 別 trip の通知タップで trip が切り替わらない」ケースが観測された。console が実機 (Android PWA / iOS PWA) から取りづらいため、IndexedDB 経由でログ収集して client の Copy ボタンで貼り付けできる診断ロガーをこのブランチに切って作業する。

このブランチはマージしない。切り分けが終わったら delete。

ベース

feature/issue207_sw-notificationclick-deeplink (#209) + 診断コミット 1 本 (`71cb265`)。

使い方

  1. Draft PR の preview URL を Android PWA に install
  2. `?debug=1` を付けて開く → 右下に Copy Logs / Clear ボタンが出る (localStorage 永続化)
  3. Clear で空にしてから再現手順を実行
  4. Copy Logs で貼り付け → 共有

解除: `?debug=0`

checkpoint

tag 意味
`SW notificationclick` 通知タップで SW handler 到達
`SW link extracted` payload から deep link 抽出
`SW origin mismatch` preview で staging リンクをタップした等
`SW intent stored` Cache Storage 書き込み完了
`SW matchAll` 生きている client 一覧 (url/focused/vis)
`SW target picked` 選んだ client。null なら openWindow へ
`SW postMessage sent` SPA navigate 指示送信
`SW focus ok/fail` PWA 前面化
`SW openWindow fallback` client 不在時
`CL listener mounted` useFcmNavigationListener setup
`CL sw message` SW → client の message 受信
`CL visibilitychange` PWA 前後面変化
`CL applyIntent` Cache 内 intent 消化
`CL navigate() called` React Router navigate 発火
`CL navigateOnce skip` dedupe
`FB effect` useFocusBlockOnMount 発火時状態
`FB wait done` data-block-id 発見 or timeout

期待パターン / 故障パターン

正常 (別 trip 通知タップ):
```
SW notificationclick → SW link extracted → SW intent stored
→ SW matchAll (client あり) → SW target picked → SW postMessage sent → SW focus ok
→ CL sw message → CL navigate() called (source: message)
```

postMessage 未着 (PWA freeze 疑い):
```
SW ... postMessage sent → SW focus ok
→ CL sw message 出ない
→ CL visibilitychange (visible)
→ CL applyIntent → CL navigate() called (source: intent) ← Cache safety net で救えるはず
```

両経路とも効かない場合:
```
SW ... postMessage sent
→ CL 側にログが一切出ない or navigate skip が出続ける
```

原因層が特定できたら、正しい fix を #209 に別コミットで反映してこの Draft PR は close。

通知タップ時の挙動を Firebase デフォルトから明示ハンドラに置き換え、既存
PWA/tab を単一 tab のまま該当 block まで scroll できるようにする。

- SW: notificationclick で clients.matchAll → focused > visible > 任意
  の優先度で client を選び、postMessage で SPA navigate。無ければ
  clients.openWindow。別 origin URL は payload 経路含め弾く
- client 側: useFcmNavigationListener が SW message を受け React Router
  で navigate。WindowClient.navigate() のフルリロードを避けて atom /
  SWR / scroll 位置を保持
- useFocusBlockOnMount: ?focusBlock={id} を useBlock で解決 → pageId
  切替 → data-block-id の DOM を rAF 待機して scrollIntoView (center)
  → replaceState で query 除去。404 / 非数値ゴミは黙って掃除
- useBlocks: list fetch 時に個別 key を populate し useBlock の重複
  fetch を回避 (楽観更新値がある場合は skip して巻き戻し防止)
- View block 2 種に data-block-id を付与 (edit モードは対象外)
- useFcmNavigationListener: FCM_NAVIGATE 受信で navigate 呼び出し / 別
  origin & 非 http scheme & 型不正 payload の無視 / unmount cleanup /
  serviceWorker 未定義環境
- useFocusBlockOnMount: block ロード成功で selectedPageId 切替 +
  scrollIntoView + query 除去 / 404 → query 除去のみ / 非数値 → API
  も叩かず query 除去 / focusBlock 無 → no-op
- docs/notifications.md: §5 Deep link を実装済み記述に更新 (SW ハンドラ
  → SPA navigate → useFocusBlockOnMount → scrollIntoView の経路と、
  処理後の replaceState 除去まで明記)
- docs/memo/notification_roadmap.md: P6b を完了へ
- focusBlock を正の safe integer のみ許可する parse に変更。regex を
  /^[1-9]\d*$/ + Number.isSafeInteger + >0 の三重チェックへ。0 が
  通ると useBlock(0) が SWR falsy key で fetch を止め ref も query
  も更新されずスタックしていた
- consumedKeyRef のセットを async の uncancelled 完了後に移動。
  StrictMode dev double-fire で最初の async が cancel された時に
  scroll が発火しなくなる問題を解消
- rawFocusBlock === null (query 掃除後) 時点で ref をリセット。同一
  block の再通知が同一マウント中に来ても再処理できるようにする
- 回帰テスト 2 種追加 (focusBlock=0 / 42 → clearParam → 42 再処理)
useBlock(id) 単発 fetch → selectedPageId 切替の後、対象 page の
useBlocks(pageId) list fetch が終わるまで ViewTripLayout は Skeleton を
返し、data-block-id DOM がまだ存在しない。Cloud Run コールドスタート等で
list fetch が 3 秒を超えると rAF ポーリングが空振りして scroll が
発火しないケースがあった。

- rAF ポーリングを MutationObserver に変更。Skeleton が Timeline に
  差し替わった瞬間に即発火するようになり無駄回転をなくす
- timeout を 3s → 8s に延長 (staging 実測でも 3s 超えるケースを確認)
- 要素発見後に 1 rAF 挟んでから scrollIntoView。Timeline 差し替え直後は
  layout がまだ確定していないタイミングで scroll してもズレることがあった
現状は SW → client の postMessage 1 本に依存しており、以下 2 パターンで
navigate が発火せず PWA が前面化したまま URL が変わらない事故になる:

- Android で PWA が freeze/kill され、matchAll は phantom client を
  返すが event loop で message が drain されない
- cold start で client 側 message listener 登録より前に SW が
  postMessage を送出

Cache Storage を safety net にして両方救う。

- SW: 通知タップの target URL を Cache に put してから postMessage/
  openWindow へ進む
- Client: useFcmNavigationListener を mount + visibilitychange で
  Cache 内 intent を消化する経路に拡張。fast path (message) と safety
  net (cache) の二重発火を lastNavigatedRef で dedupe
- 現在 URL と一致するときは navigate せず、履歴汚染を避ける
- Cache API 非対応環境 / QuotaExceeded は best effort として無視
Android PWA は console を remote inspect 経由でしか見られず、実機再現が
手間なので SW と client の checkpoint を IndexedDB に書き溜めて、URL に
?debug=1 で右下に出るボタンで一括 Copy できるようにする。運用機能では
なく本 issue の切り分け作業後に整理する前提。

- lib/debugLogger.ts: 500 件 ring buffer、readAll でフォーマット済み文字列
- firebase-messaging-sw.js: 同じ DB / store 名で inline 実装 (compat SDK
  で importScripts 経由なので TS 版を共有できない)
- components/DebugLogPanel.tsx: 右下 floating。?debug=1 で localStorage
  永続化、?debug=0 で解除。clipboard 拒否時は prompt にフォールバック
- SW の notificationclick 全経路 / client の message + intent 消化 /
  useFocusBlockOnMount の effect + wait 完了に checkpoint 挿入
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c12689d7-6b2a-43b8-95d5-3958575a403b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit 8260594):

https://tabi-share-8ef6b--pr216-chore-issue207-debug-o6lfwzfh.web.app

(expires Sat, 08 Aug 2026 12:29:48 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 9f2a87ede127df7673322845e34cf22c1372d720

PWA install 後は URL バーが隠れて ?debug=1 を後付けしにくいので、この
診断専用ブランチでは gate を撤廃し install 直後から Copy Logs / Clear
が使える状態にする。PR #216 は DO NOT MERGE 前提なので prod 影響なし。
SW 更新は非同期でユーザ操作依存 (skipWaiting していない) なので、実機で
古い SW が動いてるケースを共有ログから区別できるようにする。

- lib/debugLogger.ts (client): DEBUG_LOG_VERSION 定数を追加、LogEntry に
  version フィールドを追加、formatEntry で各行に埋め込み
- firebase-messaging-sw.js: 対の DEBUG_LOG_VERSION を持ち、SW script
  evaluate 時に 'sw script evaluated' ログ、各 entry に version 記録
- useFcmNavigationListener 'listener mounted' に SW controller の
  scriptURL と client version を含める → SW/client のバージョンずれ検知
- DebugLogPanel の Copy 時に client version / SW controller / URL /
  copied at のヘッダを先頭に付ける

診断修正のたびに 2 定数を手動 bump する運用 (現在 v02)。
実機ログで SW ハンドラが全く発火せず Firebase SDK デフォルト click 処理
だけが動く現象が発生していたが、原因は VitePWA が firebase-messaging-sw.js
を precache していたことにあった。register 時に Chrome が cache 経由で
古い版を fetch → 古い SW が active → いくら deploy しても新版に切り替
わらない。

- vite.config.ts の workbox に globIgnores + navigateFallbackDenylist で
  firebase-messaging-sw.js を除外
- firebase.json で /firebase-messaging-sw.js に Cache-Control: no-cache
  ヘッダを追加。Firebase Hosting のデフォルト max-age=3600 も抑える
- 診断: DEBUG_LOG_VERSION を v03 に bump、client 側で
  getRegistrations() を呼び出して全 SW の scope / active / waiting /
  installing の scriptURL をログ出力

feature branch (#209) に同等の修正を反映する前に、まず diagnostic
branch で実機挙動確認する。
実機ログで判明: Firebase Admin SDK で WebpushFCMOptions(link=...) を指定
しても、client 側の SW に届く payload では data.FCM_MSG.fcmOptions.link
ではなく data.FCM_MSG.notification.click_action に URL が入っていた
(Firebase JS SDK 12.16.0)。

現状は fcmOptions.link のみ見ていたので link が常に null になり、SW
handler が発火しても deep link 抽出で早期 return していた。実 payload
に合わせて click_action を primary、fcmOptions.link と data.link を
fallback にする。

DEBUG_LOG_VERSION を v04 に bump。
v04 のログで判明: staging に v04 の SW file が deploy 済みなのに実機は
v02 SW が active のまま。Chrome の SW default lifecycle だと新版は
waiting → 既存 client 閉じるまで activate 待ち、で反映されない。

- install イベントで self.skipWaiting()
- activate イベントで self.clients.claim() で既存 client も掌握
- 各イベントに diagnostic ログを付けて実機で切替タイミングを可視化
- DEBUG_LOG_VERSION を v05 に bump
v05 push 後も実機は v02 SW が active のまま。原因は FCM SW が root scope
に居ないため navigation で update check されず、register() も起動 1 回
のみで、24h の定期 check を待つ状態になっていた (実機で観測)。

- useFcmNavigationListener mount 時に FCM SW の registration に対して
  reg.update() を明示的に呼ぶ。Chrome に byte-diff check を促す
- updatefound / statechange イベントを listen して、新 SW が installed →
  waiting になった時に SKIP_WAITING message を送る (二重の保険)
- 既に waiting 状態の SW があれば即 SKIP_WAITING を送信
- firebase-messaging-sw.js に message listener を追加。
  { type: 'SKIP_WAITING' } を受けたら self.skipWaiting() を発火
- DEBUG_LOG_VERSION を v06 に bump

これで client mount 時に FCM SW の状態が可視化され、waiting なら強制
activate 経路が動く。v02 SW を追い出す最終手段。
v06 ログで判明: navigateOnce の lastNavigatedRef が unmount まで保持
されるため「一度 navigate した URL には二度と navigate できない」バグが
あった。前セッションで /trip/A?focusBlock=X に遷移後、別 trip に移動して
再度 focusBlock=X の通知タップ → 両経路 (message / intent) とも
'skip (== last)' で navigate 発火せず。

- ref の型を { url, at: performance.now() } に変更
- dedupe 判定を「同 URL かつ 1 秒以内」に限定 (NAV_DEDUPE_WINDOW_MS)
- 1 通知タップに対する message + intent の近接発火は dedupe できる
- それを超えた同 URL 再遷移は許可される
- DEBUG_LOG_VERSION を v07 に bump
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