一個你幾乎看不見的 typo
先看這兩段字串,它們只差中間一個字元:
...-eIlo-...(原本正確的:e、大寫 I、小寫 l、o)
...-eIIo-...(被改錯的:e、大寫 I、大寫 I、o)
在多數等寬字型裡,小寫 l 和大寫 I 幾乎無法分辨。
錯誤發生在第 66 個字元——不是開頭、不是結尾,正好埋在最難用肉眼掃描的中段。
更糟的是,這次改動還是頂著「P0 修復」的名義進去的:當時把一段「截圖誤抄的反例」
當成了正確值的記憶,於是把對的改成了錯的。一行、一個字元,功能就無聲死亡。
為什麼健康檢查騙過了所有人
我有一套上線後的健康檢查,四條 curl,當時全綠:設定檔有部署、版本號正確、
關鍵 HTML 區塊存在、設定條目數量符合。看起來無懈可擊。
問題在於 curl 加 grep 只能驗證一件事:
某個字串存在於 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。防護是給每一條「正確值」配上來源證據連結—— 沒有當天部署驗證佐證的值,就不允許被單獨引用。
怎麼把真相逼出來:排除法 + 對照實驗
鎖定根因的流程,本質是一連串排除:
- 排除環境:用一個已知會成功的設定值動態載入同一個 SDK,成功——證明瀏覽器、網路、跨域都沒問題,問題在那個值本身。
- 排除部署延遲:用帶時間戳的查詢字串強制繞過快取去抓檔案,確認線上跑的確實是新部署的內容,不是還沒生效。
- 對照實驗:在同一個頁面、同一個來源下,把「錯的值」和「對的值」各自動態載入一次 SDK,一個
onerror、一個成功掛上物件。到這裡真相再無懸念。
兩版本並排對照是最有說服力的一步——它把「我覺得是這個值的問題」變成 「同樣的環境、只換這一個變數,結果相反」。能設計出只改一個變數的對照, 就不用再爭論。
關鍵教訓
字串存在 ≠ 功能可用:curl 加 grep 只驗 HTTP 層字串,驗不到 runtime 能不能跑、第三方會不會接受。關鍵路徑要有真實瀏覽器的冒煙測試,才能宣告 healthy。
三層快取分開清:CDN、瀏覽器 HTTP 快取、Service Worker 快取各自獨立、key 不同。前端變更至少更新 SW 快取名稱;影響檔案內容時還要 bump 該檔的查詢字串版本。
指紋擋不了中段錯字:末 N 碼加長度只能擋頭尾與長度異常。中段單字元替換要靠全文雜湊或真實載入測試。
反例段落是筆記的陷阱:「曾被誤抄成 B」幾週後很容易被讀成「正確是 B」。每個正確值都該綁來源證據,否則不准單獨引用。
只改一個變數的對照實驗,勝過一切推測:同環境、同來源,只換可疑的那一個值,結果相反,爭論就結束了。