一個你幾乎看不見的 typo

先看這兩段字串,它們只差中間一個字元:

...-eIlo-...(原本正確的:e、大寫 I、小寫 l、o)
...-eIIo-...(被改錯的:e、大寫 I、大寫 I、o)

在多數等寬字型裡,小寫 l 和大寫 I 幾乎無法分辨。 錯誤發生在第 66 個字元——不是開頭、不是結尾,正好埋在最難用肉眼掃描的中段。 更糟的是,這次改動還是頂著「P0 修復」的名義進去的:當時把一段「截圖誤抄的反例」 當成了正確值的記憶,於是把對的改成了錯的。一行、一個字元,功能就無聲死亡。

為什麼健康檢查騙過了所有人

我有一套上線後的健康檢查,四條 curl,當時全綠:設定檔有部署、版本號正確、 關鍵 HTML 區塊存在、設定條目數量符合。看起來無懈可擊。

問題在於 curlgrep 只能驗證一件事: 某個字串存在於 HTTP 回應裡。它驗不到的東西太多了—— 那段 JavaScript 在瀏覽器裡能不能真正執行、第三方 SDK 會不會接受這個識別字串、 window 上的物件有沒有真的掛上去。字串「在那裡」和功能「能用」之間, 隔著整個 runtime。

這就是脆弱訊號(fragile signal)的典型:一個指標看起來在量你關心的事, 其實量的是它的影子。修法是給關鍵路徑補一層真實的冒煙測試—— 用無頭瀏覽器真的把頁面打開、真的去抓 runtime 物件存不存在。 字串檢查可以當第一道快速門檻,但不能當作可以宣告「healthy」的最後一道。

三層快取,三套清除規則

找到錯字、改成正確值、推上線——以為結束了,回訪的使用者卻還是壞的。 因為一個前端字串的修正,要穿過三層各自獨立的快取,每一層的失效觸發條件都不同。

快取層它的 key 是什麼怎樣才會失效
CDN 邊緣節點 完整 URL(含查詢字串) max-age 自然到期(可能數分鐘)
瀏覽器 HTTP 快取 完整 URL(含查詢字串) URL 改變——改查詢字串或改路徑
Service Worker 快取 快取名稱 + URL 改快取名稱,SW 啟用時清掉舊的

實測過程能直接看到這三層的獨立性。把 Service Worker 註銷、清掉它的快取, 重新載入後舊的 SW 還是接管、跑的還是錯誤版本——證明只改檔案內容不夠, 必須同時更動快取名稱,逼 SW 在啟用時丟掉舊快取。

但即使 SW 升級了、SW 快取清乾淨了,瀏覽器自己的 HTTP 快取仍然鎖著 檔名?v=舊版本 這個 URL key,回的還是舊內容。用 fetch(url, { cache: 'no-store' }) 拿到的是新版,用帶舊版本號的 URL 拿到的卻是舊版—— 兩個 fetch 同一支檔案、結果不同,HTTP 快取層的存在與獨立性就這樣被直接證明了。

所以規則是:任何前端變更,至少要更新 Service Worker 的快取名稱(擋住「SW 餵舊檔」); 如果變更影響到某支檔案的內容,還要同時更新它在 HTML 裡 <script src="檔名?v=版本"> 的查詢字串(擋住「HTTP 快取搶在 SW 前面餵舊檔」)。 兩層都 bump,回訪流量才真的拿到新版。

指紋驗證的中段盲區

我原本有一套防呆:對公開識別字串記錄「末 6 碼 + 總長度」當指紋,啟動時比對。 理論上字串被改動就會被抓到。但這次它徹底失靈:

  • 錯字在第 66 個字元,屬於中段
  • 末 6 碼兩個版本完全相同
  • 總長度兩個版本都是 82,完全相同

指紋過了、長度過了、SDK 還是載不起來。教訓很清楚: 「末 N 碼 + 長度」只能擋頭尾截斷或長度異常,對中段的單字元替換完全無感。 要真的防住,得用全文雜湊(例如後端回傳 SHA-256 讓前端啟動時比對), 或者乾脆把終極防線交給「SDK 真的載入成功」這個冒煙測試。 指紋是門檻,不是防線。

記憶不是真相,原始紀錄才是

這次能破案,靠的是翻出當初首次上線那天的部署驗證紀錄——白紙黑字寫著正確值、 而且寫著「SDK 載入成功」。對照之下,後來那次「修復」引用的, 其實是同一份筆記裡「某某值曾被誤抄」的反例段落。

這是筆記最危險的地方:反例段落最容易被後人讀成正確值。 一份寫著「正確是 A,曾經有人誤抄成 B」的筆記,幾週後再讀, 很可能只記得 B 而把它當成 A。防護是給每一條「正確值」配上來源證據連結—— 沒有當天部署驗證佐證的值,就不允許被單獨引用。

怎麼把真相逼出來:排除法 + 對照實驗

鎖定根因的流程,本質是一連串排除:

  1. 排除環境:用一個已知會成功的設定值動態載入同一個 SDK,成功——證明瀏覽器、網路、跨域都沒問題,問題在那個值本身。
  2. 排除部署延遲:用帶時間戳的查詢字串強制繞過快取去抓檔案,確認線上跑的確實是新部署的內容,不是還沒生效。
  3. 對照實驗:在同一個頁面、同一個來源下,把「錯的值」和「對的值」各自動態載入一次 SDK,一個 onerror、一個成功掛上物件。到這裡真相再無懸念。

兩版本並排對照是最有說服力的一步——它把「我覺得是這個值的問題」變成 「同樣的環境、只換這一個變數,結果相反」。能設計出只改一個變數的對照, 就不用再爭論。

關鍵教訓

字串存在 ≠ 功能可用:curl 加 grep 只驗 HTTP 層字串,驗不到 runtime 能不能跑、第三方會不會接受。關鍵路徑要有真實瀏覽器的冒煙測試,才能宣告 healthy。

三層快取分開清:CDN、瀏覽器 HTTP 快取、Service Worker 快取各自獨立、key 不同。前端變更至少更新 SW 快取名稱;影響檔案內容時還要 bump 該檔的查詢字串版本。

指紋擋不了中段錯字:末 N 碼加長度只能擋頭尾與長度異常。中段單字元替換要靠全文雜湊或真實載入測試。

反例段落是筆記的陷阱:「曾被誤抄成 B」幾週後很容易被讀成「正確是 B」。每個正確值都該綁來源證據,否則不准單獨引用。

只改一個變數的對照實驗,勝過一切推測:同環境、同來源,只換可疑的那一個值,結果相反,爭論就結束了。

來源:個人開發日誌 2026-06-14 · 一次 typo 引發的 11 天靜默失效調查 · 三層快取與 ground truth 三角驗證方法論