研發度量會說謊
程式有 bug 會 crash、會報紅字,你馬上知道。度量有 bug 不會 crash——它只會默默產出一個「看起來很合理」的錯數字,然後這個數字被畫成漂亮的圖表、被拿去排 sprint、被拿去評績效。等有人發現不對,錯誤判斷已經做完了。
這篇是踩過一整輪 DORA / DevEx 度量坑之後沉澱的七個語意陷阱。它們的共同點是:不會噴錯、只會騙人,而且騙的方向通常是系統性偏移(全體高估、某類人整批消失),不是隨機噪音——所以特別難察覺、也特別傷。
核心:度量的錯不在算術,在語意
flowchart LR
A[原始事件<br>狀態變更 / 時間戳] --> B[指標定義<br>起點? 終點? 算誰?]
B --> C[彙總數字]
C --> D[圖表 / 報告]
D --> E[人事 / 排程判斷]
B -.語意錯在這.-> X["'合理但錯'的數字<br>不會 crash"]
X --> C
style B fill:#f59e0b,color:#fff
style X fill:#ef4444,color:#fff
style E fill:#7c3aed,color:#fff
算術很少出錯,出錯的是「這個數字到底代表什麼」。下面七個都是在這一層翻車的。
陷阱 1:別用「票建立日」當工時起點
最常見、也最貴的一個。
一張票從建立到真正開工,中間可能排隊等規格、等排期、等人力——這段等待可以長達 10 天以上。如果你把「票建立日」當成工時起點,就等於把排隊時間算進了開發成本。
真實案例:一張實際只花了 30 個工作天的票,用「建立日 → 完成日」的日曆天算法,變成了 94 天。三倍灌水,而且是系統性地灌——每一張排過隊的票都被高估。
正解:起點用「首次進入 in-progress / dev 狀態」的時間戳。要拿的是「開工到完成」,不是「進系統到完成」。
flowchart LR
C[建立票] -->|排隊 10+ 天<br>等規格/等排期| P[首次 in-progress]
P -->|真正開發| D[完成]
C -.❌ 錯起點.-> D
P -.✅ 對起點.-> D
style C fill:#94a3b8,color:#fff
style P fill:#10b981,color:#fff
陷阱 2:狀態語意必須逐個對齊
工作流裡兩個看起來很像的狀態,語意可能天差地遠。
- 「分配給 QA」= 票被指派到 QA 名下,但還沒開始測
- 「QA 測試中」= QA 真的在跑測試
如果你做「開測後 RD 又改 code」的風險偵測,把「分配給 QA」也算成「已開測」,就會大量誤報——一堆根本還沒進測試的票被標成「危險」。
正解:每個狀態的語意要逐個確認,不能靠名字猜。畫一張狀態語意對照表,把「這個狀態代表工作真的發生了嗎」逐格填清楚。
陷阱 3:工作流有例外路徑,別讓人「從報告消失」
你以為的標準流程:todo → in-progress → qa → done。
現實:某些團隊會跳關。例如 PR 一開就自動把狀態從 todo 直接切到 qaing,完全跳過 in-progress。如果你的指標只認「首次進 in-progress」當起點、又沒設 fallback,這些人的票會查不到起點 → 直接從報告上消失。
結果就是:那個團隊的產出「憑空蒸發」,管理層看到的是一張少了一整組人的報告,卻不知道是度量把他們吃掉了。
正解:起點/終點都要有降級 fallback。例如「首次 in-progress,查不到就退而取『首個非 todo 狀態』」;終點「首個 release-candidate,查不到就退 done」。每條路徑都要能落到一個合理值。
陷阱 4:工作天 ≠ 日曆天
卡關、停留、週期這類「天數」指標,一定要扣掉週末與假日。
一張票週五進 QA、週一有人看,用日曆天算是 3 天,實際只過了 1 個工作天。跨越連假時誤差更誇張。所有拿去跟「SLA / 目標天數」比較的停留指標,都要用工作天口徑,否則週末多的月份會無端「變慢」。
正解:內建工作天計算(扣週末 + 國定假日)。這是所有時間類指標的預設,不是加值功能。
陷阱 5:「全是 0」不一定是資料壞了
看到一整排 0,第一反應是「資料管線壞了」。先別急著這樣報。
真實案例:reject(打回)數整排都是 0。查下去發現資料完全健康,是指標定義把它濾光的——這個指標「只算已完成的票」,但票一旦完成就會漂移到另一個分類,於是「已完成 ∩ 曾被 reject」這個集合被兩層濾網結構性地殺成空集合。
底層每一筆 reject 事件都好好在資料庫裡,是定義讓它顯示為 0。
正解:報「資料壞掉」之前,先問「是不是定義把它濾光了」。把濾網條件一層層攤開,看是不是某兩個條件的交集天生為空。
陷阱 6:「欄位是 NULL」不代表它「被洗掉」
這是陷阱 5 的鏡像。看到欄位一片 NULL,別急著報「資料受損」。
NULL 可能是「從來就沒填過」,也可能是「本來有值後來被覆蓋掉」——這兩件事的嚴重程度完全不同。前者是正常,後者才是事故。
真實案例:一個外部登入整合,某些欄位(部門 / 職級)只在 userinfo 端點回傳、不在 token 裡。當 userinfo 端點 5xx 時,如果無腦「整包覆蓋」就會把既有的部門職級洗成 NULL。正確做法是偵測到「這次只拿到 token、沒拿到 userinfo」就跳過覆蓋,區分「對方刪了欄位」與「我這次抓失敗」。
正解:要下「資料受損」的結論,需要「曾經有值」的正面證據(歷史快照、稽核紀錄),不能光憑現在是 NULL 就斷言。訓練自己不做無證據的斷言。
陷阱 7:狀態瞬間連跳 ≠ 沒投入
有時你會看到一張票的狀態在同一秒內連跳好幾格:todo → in-progress → qa → done,時間戳幾乎一樣。
直覺會判成「造假」或「沒真的做」。通常都不是——這是事後補登:人做完了才回來一次把狀態點過去。工作是真的發生了,只是狀態機被一次補完。
正解:連跳當成「補登訊號」處理,不要當「造假」或「漏資料」報。真要抓造假,得看別的證據(有沒有對應的 commit / MR / 測試產出),不能只看狀態時間戳。
加碼:兩個「join 錯資料」的真實戰記
上面七個是語意層,這兩個是實作層踩的坑,同樣是「數字看起來對、其實錯」:
「卡了 38 天」其實只卡了 22 小時。 一張跨組的票有 mirror(鏡像)row,狀態歷史表只存 base id。用了帶後綴的 task_id 去查 → 查不到 → fallback 回「票建立日」→ 於是把「從開票到現在」整段算成「卡在現狀」。修法:切掉後綴對到 base id,並 fallback 到真實的 time-in-status,而不是建立日。
移票灌出的 100% 完成率。 Sprint 完成率如果用 live count(現在還在這個 sprint 的票),那些做到一半被移出 sprint 的票會消失,分母縮小、比率被灌到接近 100%。正解:用「最後一張每日 burndown 快照」的 done/total,而不是即時數字;live count 另存供對照就好。
上線前的語意審查清單
把度量當成「會默默出錯的生產程式」來對待:
mindmap
root((度量上線前<br>語意審查))
時間口徑
起點用開工不用建立日
工作天扣週末假日
終點有 fallback
狀態語意
逐格確認代表什麼
例外路徑不讓人消失
連跳當補登不當造假
極端值
全 0 先查是不是被濾光
NULL 要正面證據才報受損
爆高/歸零主動查
可驗證
改邏輯前先寫 parametric test
每個數字能 drill 回原始清單
drill 用同一段 SQL 對得上
三條鐵則收尾:
- 改指標邏輯前,先寫 parametric test 把現有行為釘死。 度量漂移是靜默的,沒有測試守著,下次重構就悄悄變了數字沒人知道。
- 每個彙總數字都要能 drill 回原始清單,而且用同一段 SQL。 否則彈窗總數會跟卡片對不上——那種「加起來對不上」的信任崩壞,一次就毀掉整個儀表板的公信力。
- 報告不要誇大。 「AI 輔助度量」= 加速人工整理 + 人把關,不能寫成「全自動、不耗人力」。管理層最在意的是失真,一次浮誇會賠掉所有數字的可信度。
一句話
度量不會 crash,只會騙你。而它騙的方向,通常剛好是你最想聽的那個方向——所以每個「漂亮的數字」,上線前都值得被當成嫌疑犯審一遍。