三個實戰案例 + Debug 日誌模板

一句話總結:Debug 的經驗沒辦法從教科書學,只能從真實案例中累積。每次 Debug 完留下日誌,三個月後遇到類似問題你會感謝自己。

結論先講:Status code 200 不代表資料結構跟你預期的一樣。只在 production 出現的問題通常跟延遲、並發、資料量有關。Memory leak 需要長時間觀察,Heap Snapshot 的 Comparison view 是最有效的工具。

案例一:API 回 200 但前端空白

症狀:使用者回報「訂單列表空白」,但 Network tab 看到 API 回傳 200。

Debug 過程

  1. 重現:登入測試帳號 → 訂單頁面 → 列表空白。可重現。
  2. 定位:Network tab 看到 API 回 200 有資料。Console 有 TypeError: orders.map is not a function
  3. 假設:API 回傳格式變了
  4. 驗證:前端預期 { "orders": [...] },API 實際回傳 { "data": { "orders": [...] } }。後端加了一層 wrapper,前端還在用舊路徑。
// Before
const orders = response.orders;
// After
const orders = response.data.orders;
  1. 預防:加上 API response 的 TypeScript interface、加上 API contract test 確保前後端對格式有共識。

教訓:Status code 200 不代表資料結構跟你預期的一樣。永遠要看 response body。而且後端改 API 格式不通知前端的,應該被罰站。

案例二:只在 Production 出現的 Race Condition

症狀:使用者偶爾回報「付款成功但訂單狀態還是未付款」。Staging 完全無法重現。

Debug 過程

  1. 重現:Staging 怎麼測都正常。加入更詳細的 log(帶 trace ID 和 timestamp),等 production 再次出現。

  2. 分析 log

10:30:45.100 [order-service]   info  Order #123 created (pending)
10:30:45.200 [payment-service] info  Payment #123 started
10:30:45.800 [payment-service] info  Payment #123 succeeded
10:30:45.810 [payment-service] info  Updating order #123 to paid
10:30:45.150 [order-service]   info  Cache refreshed (pending)  ← 注意時間!

最後一行的時間是 45.150,比付款成功的 45.800 早!order-service 在付款完成之前就刷新了快取,拿到的是舊的 pending 狀態。

  1. 根因:前端建立訂單後立刻 polling 狀態,但付款回調是非同步的。Production 有多台機器 + CDN + 分散式快取,延遲比 staging 大得多。

  2. 修復:付款成功用 WebSocket 推播通知不靠 polling、快取加 invalidation、訂單查詢加 read-after-write consistency。

  3. 為什麼 staging 重現不了:staging 一台機器,延遲極低,快取幾乎即時更新。

教訓:只在 production 出現的問題,通常跟延遲、並發、資料量有關。加更多 log(帶時間戳)是唯一辦法。

案例三:Memory Leak 追蹤

症狀:Node.js 服務運行 2-3 天後記憶體從 200MB 慢慢爬到 1.5GB,然後 OOM 被 kill。

Debug 過程

  1. 確認:監控顯示記憶體緩慢上升沒有回落(GC 沒回收)。重啟恢復,2-3 天後又爬上去。

  2. 抓 Heap Snapshot

// 加一個 endpoint 來抓 heap snapshot
app.get('/debug/heap', (req, res) => {
  const v8 = require('v8');
  const snapshot = v8.writeHeapSnapshot();
  res.json({ file: snapshot });
});
// 啟動後 1 小時和 24 小時各抓一次
  1. 分析:Chrome DevTools Memory tab → Load snapshot → Comparison view → 發現大量 EventListener 沒被移除。

  2. 定位:WebSocket handler 每次連線 .on('data', handler),斷線時沒有 .off('data', handler)

// Bug:listener 越積越多
socket.on('connection', (conn) => {
  eventBus.on('update', (data) => conn.send(data));
  // 斷線後 eventBus 上的 listener 還在!
});
 
// 修復:斷線時清除
socket.on('connection', (conn) => {
  const handler = (data) => conn.send(data);
  eventBus.on('update', handler);
  conn.on('close', () => eventBus.off('update', handler));
});
  1. 預防:加 EventEmitter.listenerCount 監控、設 maxListeners 警告、定期檢查記憶體趨勢。

教訓:Memory leak 不會立刻出現,需要長時間觀察。Heap Snapshot 的 Comparison view 是最有效的工具。

Debug 日誌模板

每次 Debug 完留下紀錄。不是為別人,是為未來的你。

# Debug 日誌
 
## 基本資訊
- 日期:YYYY-MM-DD
- 報告者:PM / 使用者 / 監控告警
- 嚴重程度:P0 / P1 / P2 / P3
- 影響範圍:全部使用者 / 特定使用者 / 特定功能
 
## 問題描述
(一句話,包含預期行為和實際行為)
 
## 重現步驟
1. ...
2. ...
 
## 環境資訊
- 瀏覽器 / OS:
- 前後端版本(commit hash):
- 環境:dev / staging / production
 
## 嘗試過的方法
| # | 假設 | 驗證方式 | 結果 |
|---|------|----------|------|
| 1 | API 格式錯 | 檢查 Network tab | 格式正確 |
| 2 | 快取問題 | 清快取 | 問題依舊 |
| 3 | Race condition | 加 loading state | 問題消失 |
 
## 根因分析
(詳細說明根本原因)
 
## 修復方式
(怎麼修的,PR 連結)
 
## 預防措施
- [ ] 加自動化測試
- [ ] 更新監控告警
- [ ] 更新 Runbook
- [ ] 分享給團隊
 
## 花費時間
- 定位:__ 小時 / 修復:__ 小時
 
## 學到的事
(一句話,未來遇到類似問題可以更快)

這個模板跟 事件管理方法論 的 Post-mortem 互補——Debug 日誌記錄技術細節,Post-mortem 記錄流程和組織改進。

系列總結

Debug 的核心不是工具,不是技巧,是紀律

  • 不要跳過重現步驟
  • 不要同時改三個變數
  • 不要只靠直覺
  • 不要忘記記錄

下次遇到 bug,先深呼吸,走一遍流程。大部分 bug 都沒你想像的那麼難——只要你不是在碰運氣。

系列文章:

延伸閱讀:

「Debug 完不寫日誌,就像考試完不看錯的題目——你會一直犯同樣的錯。」