质量度量:缺陷密度、逃逸率与 DORA¶
一、指标被用歪的现场¶
2026 Q1 某团队把「缺陷数」做成季度排名,Q2 数据立刻变好:
| 指标 | Q1 | Q2 | 变化 |
|---|---|---|---|
| 提单缺陷数 | 268 | 161 | -40% |
| 线上故障数 | 9 | 20 | +122% |
| 线上资损事件 | 1 | 4 | +300% |
| 平均缺陷描述字数 | 74 | 41 | -45% |
缺陷数下降不是质量提升,而是提单变少、描述变短、级别被降。同期线上故障翻倍,说明大量问题被推迟到生产环境才暴露。
度量一旦用于排名,就会产生「指标优化」而不是「质量优化」。本文给的是口径 + 计算 + 解读,前提是:指标只用于改进,不用于个人考核。
二、指标定义与统计口径总表¶
| 指标 | 公式 | 分子 | 分母 | 周期 | 去重规则 |
|---|---|---|---|---|---|
| 缺陷密度(KLOC) | 加权缺陷数 / KLOC | Σ(缺陷×级别权重) | 手写业务代码千行数 | 按版本 | duplicate 只算主单 |
| 缺陷密度(FP) | 加权缺陷数 / 功能点 | 同上 | 已交付功能点 | 按版本 | 同上 |
| 阶段逃逸率 | 后阶段发现数 / 该版本总缺陷 | 上线后发现 | 上线前 + 上线后发现 | 按版本 + 窗口 | 同根因算 1 |
| 线上逃逸率 | 线上缺陷 / 该版本总缺陷 | 生产环境发现 | 同上 | 30 天窗口 | 同上 |
| 重开率 | 重开缺陷数 / 已关闭缺陷数 | 重开次数(按单去重) | 本期关闭数 | 按月 | 一单多次重开算 1 |
| 缺陷修复时长 | resolved - created | - | - | 按月 | 只算已修复 |
| 部署频率 | 生产部署次数 / 周 | prod 部署 | 周数 | 按周 | 回滚不计入 |
| 变更前置时间 | 上线时间 - 提交时间 | - | - | 按次 | 取中位数 |
| 变更失败率 CFR | 导致故障的部署 / 总部署 | failed=1 | 全部 prod 部署 | 按周/月 | 同一故障多次部署算 1 |
| 恢复时长 | 故障恢复 - 故障发生 | - | - | 按次 | 取中位数 |
三条通用口径规则,先定再算:
- 时间一律用
+08:00本地时间,不混用 UTC。 JIRA 导出常见2026-09-14T02:00:00.000+0000,直接和发布日比较会差 8 小时,跨零点的缺陷会被算到前一天。 - 级别取结案时的最终级别。 提单时的级别会被降级,用提单级别会系统性高估严重度。
- 分母排除生成代码、单测、第三方库、历史遗留目录。 分母口径一变,密度立刻「改善」,这是最常见的口径漂移。
三、缺陷密度:KLOC 与功能点两套算法¶
3.1 按 KLOC¶
# cloc 1.98,输出 JSON 便于程序读取
cloc --json --exclude-dir=target,node_modules,generated,test,vendor ./src > cloc.json
jq '.SUM.code' cloc.json
按模块拆分时,用 --by-file 后聚合,别手工估:
cloc --by-file --json --exclude-dir=target,test ./src > cloc_files.json
for m in order refund coupon; do
jq -r --arg m "/$m/" '. | to_entries[]
| select(.key != "SUM" and .key != "header" and (.key | contains($m)))
| .value.code' cloc_files.json \
| awk -v m="$m" '{s+=$1} END {printf "%s\t%d\n", m, s}'
done
加权缺陷数与密度:
权重:P0=5,P1=3,P2=1,P3=0.3
order :P0 1、P1 3、P2 6、P3 10 → 5 + 9 + 6 + 3 = 23.0 → 23.0 / 12.40 = 1.85 /KLOC
refund :P0 1、P1 2、P2 4、P3 4 → 5 + 6 + 4 + 1.2 = 16.2 → 16.2 / 6.82 = 2.38 /KLOC
coupon :P0 0、P1 1、P2 2、P3 5 → 0 + 3 + 2 + 1.5 = 6.5 → 6.5 / 4.20 = 1.55 /KLOC
判定标准:密度只做同团队、同口径、跨版本纵向比较。refund 2.38 高于 order 1.85,说明新模块缺陷更集中,而不是「order 质量更好」——order 的代码量和复杂度都更高。
3.2 按功能点¶
功能点用简化的 IFPUG 五类元素,权重按复杂度取中值(完整法分低/中/高,这里用中值便于快速估算):
| 元素 | 含义 | 低 | 中 | 高 |
|---|---|---|---|---|
| ILF 内部逻辑文件 | 本系统维护的业务数据表 | 7 | 10 | 15 |
| EIF 外部接口文件 | 引用的外部系统数据 | 5 | 7 | 10 |
| EI 外部输入 | 用户/系统输入,改变数据 | 3 | 4 | 6 |
| EO 外部输出 | 有计算/派生逻辑的输出 | 4 | 5 | 7 |
| EQ 外部查询 | 直接查询输出 | 3 | 4 | 6 |
退款模块示例:
| 元素 | 数量 | 复杂度 | 权重 | 小计 |
|---|---|---|---|---|
| ILF(退款单、退款明细、审批单) | 3 | 中 | 10 | 30 |
| EIF(订单、支付流水、优惠券) | 3 | 中 | 7 | 21 |
| EI(发起退款、审批、人工处理、回调) | 4 | 中 | 4 | 16 |
| EO(退款金额计算、对账单、通知) | 3 | 中 | 5 | 15 |
| EQ(退款详情、列表、导出) | 3 | 中 | 4 | 12 |
| 合计 | - | - | - | 94 FP |
16.2 / 94 = 0.172 缺陷/FP。规模修正后对比:
| 指标 | 退款模块 | 订单模块 |
|---|---|---|
| 加权缺陷 | 16.2 | 23.0 |
| KLOC | 6.82 | 12.40 |
| 功能点 | 94 | 210 |
| 密度 /KLOC | 2.38 | 1.85 |
| 密度 /FP | 0.172 | 0.110 |
两套算法的结论一致(退款更高),但幅度不同。对外/跨团队对比用 FP,因为它抵消语言与实现方式差异;团队内趋势用 KLOC,因为 cloc 能自动取数,FP 估算主观且耗时。
FP 估算的坑:同一个需求,两个人估出的功能点可能差 30%。要求估算人固定、使用同一张复杂度表,并在估算记录里留痕,否则这个指标不可比。
四、逃逸率:分子分母怎么定才不失真¶
4.1 两种逃逸率¶
| 类型 | 分子 | 分母 | 用途 |
|---|---|---|---|
| 线上逃逸率 | 上线后 30 天内在生产发现的缺陷 | 该版本上线前发现 + 上线后 30 天发现 | 衡量测试有效性 |
| 阶段逃逸率 | 后一阶段发现的缺陷(如提测后发现的需求问题) | 该版本全部缺陷 | 衡量阶段门禁有效性 |
本文默认「线上逃逸率」。
4.2 口径要素¶
| 要素 | 错误做法 | 正确做法 |
|---|---|---|
| 版本归属 | 按修复版本归属,导致上版本缺陷算到下版本 | 按引入版本归属;无法判断时按发现时间 + 影响范围归到最近发布版本 |
| 时间窗口 | 只统计发布后 7 天 | 30 天(可按业务定 14/30/90,但同一指标必须固定) |
| 分子来源 | 只看 JIRA 工单 | JIRA + 监控告警 + 客诉工单,三源合并去重 |
| 分母 | 只算提测阶段发现的 | 上线前全部阶段(评审、单测、提测、回归)+ 窗口内线上 |
| 排除项 | 把配置错误、环境问题也算进去 | 单列,不混入逃逸率;但需单独统计「配置类事故」 |
| 未满窗口版本 | 发布 5 天就拿来比 | 标记「窗口未满」,不进入趋势对比 |
4.3 同一批数据,三种口径三个结果¶
假设某版本上线前发现 25 条,上线后 30 天内线上发现 5 条:
| 口径 | 计算 | 结果 | 失真原因 |
|---|---|---|---|
| 只算提测阶段 | 5 / (18 + 5) | 21.7% | 分母漏掉评审/单测阶段,虚高 |
| 只算线上 / 全部 | 5 / (25 + 5) | 16.7% | 正确口径 |
| 把下版本需求缺陷算进本版本 | 5 / (25 + 5 + 45) | 6.7% | 分母混入无关缺陷,虚低 |
第三种最常见:JIRA 里 fixVersion 被批量改到下个版本,导出时又按 fixVersion 过滤,结果本版本逃逸率凭空减半。
4.4 取数¶
-- 上线前发现的该版本缺陷
project = QALAB AND type = 缺陷 AND fixVersion = "2.3.0"
AND created < "2026-09-14" AND resolution != "Duplicate"
-- 上线后 30 天窗口内的线上缺陷
project = QALAB AND type = 缺陷 AND fixVersion = "2.3.0"
AND created >= "2026-09-14" AND created < "2026-10-14"
AND environment = "prod"
resolution != Duplicate 不够,还要排除 env、invalid、requirement-change 标签,否则分母被稀释。
五、DORA 四项指标¶
| 指标 | 精确定义 | 采集点 | 常见错误 |
|---|---|---|---|
| 部署频率 | 单位时间内成功部署到生产的次数 | CD 发布事件表 | 把测试环境部署算进去;把回滚算成新部署 |
| 变更前置时间 | 代码提交到生产生效的耗时 | commit 时间 → 部署完成时间 | 只算流水线时长,漏掉等待审批与发布窗口 |
| 变更失败率 | 导致需要回滚/热修/故障处理的部署占比 | 部署事件 + 故障工单关联 | 只统计回滚,漏掉「上线后紧急 hotfix」 |
| 恢复时长 | 从故障影响到恢复服务的时间 | 告警触发 → 恢复确认 | 从「发现」算起而非「影响开始」算起 |
参考阈值(DORA 2022 报告口径;2023 起改为按性能集群分档,边界有调整,跨年对比需注明版本):
| 档位 | 部署频率 | 变更前置时间 | 变更失败率 | 恢复时长 |
|---|---|---|---|---|
| Elite | 按需,每天多次 | <1 小时 | ≈5% | <1 小时 |
| High | 每天 ~ 每周 | 1 天 ~ 1 周 | ~10% | <1 天 |
| Medium | 每周 ~ 每月 | 1 周 ~ 1 月 | ~15% | 1 天 ~ 1 周 |
| Low | 每月 ~ 每半年 | 1 月 ~ 6 月 | ~30% | >1 周 |
用 SQL 直接从发布事件表算,不靠手工统计:
-- PostgreSQL 15:近 8 周部署频率与 CFR
SELECT
date_trunc('week', deployed_at) AS week,
COUNT(*) AS deploys,
COUNT(*) FILTER (WHERE failed) AS failed_deploys,
ROUND(100.0 * COUNT(*) FILTER (WHERE failed) / NULLIF(COUNT(*), 0), 1) AS cfr_pct
FROM deployments
WHERE env = 'prod' AND deployed_at >= now() - interval '8 weeks'
GROUP BY 1
ORDER BY 1;
week | deploys | failed_deploys | cfr_pct
--------------------+---------+----------------+---------
2026-08-31 00:00:00 | 9 | 2 | 22.2
2026-09-07 00:00:00 | 11 | 2 | 18.2
2026-09-14 00:00:00 | 10 | 1 | 10.0
2026-09-21 00:00:00 | 12 | 1 | 8.3
-- 变更前置时间中位数(小时)
SELECT ROUND(
percentile_cont(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (deployed_at - committed_at)) / 3600
)::numeric, 1) AS p50_lead_time_hours
FROM deployments
WHERE env = 'prod' AND deployed_at >= now() - interval '8 weeks';
注意 committed_at 要用首次提交到该发布分支的时间,不是合并时间。用合并时间会把等待期算掉,指标虚好。
六、示例数据与趋势解读¶
6.1 质量指标(8 周)¶
| 周 | 版本 | 缺陷数 | 加权缺陷 | KLOC | 密度/KLOC | 上线后缺陷 | 逃逸率 |
|---|---|---|---|---|---|---|---|
| W31 | 2.1.0 | 42 | 38.4 | 21.8 | 1.76 | 8 | 19.0% |
| W32 | 2.1.1 | 18 | 15.2 | 21.9 | 0.69 | 1 | 5.6% |
| W33 | 2.2.0 | 51 | 46.9 | 22.4 | 2.09 | 9 | 17.6% |
| W34 | 2.2.1 | 21 | 17.8 | 22.5 | 0.79 | 2 | 9.5% |
| W35 | 2.3.0 | 47 | 42.3 | 23.1 | 1.83 | 6 | 12.8% |
| W36 | 2.3.1 | 16 | 12.4 | 23.2 | 0.53 | 1 | 6.3% |
| W37 | 2.4.0 | 44 | 39.8 | 23.4 | 1.70 | 2(窗口未满) | 6.1% |
| W38 | 2.4.1 | 14 | 10.9 | 23.4 | 0.47 | 1 | 7.1% |
6.2 DORA(8 周)¶
| 周 | 部署次数 | 前置时间中位数 | CFR | 恢复时长中位数 |
|---|---|---|---|---|
| W31 | 7 | 38.5 h | 28.6% | 95 min |
| W32 | 9 | 31.2 h | 22.2% | 78 min |
| W33 | 8 | 29.8 h | 25.0% | 82 min |
| W34 | 11 | 24.6 h | 18.2% | 55 min |
| W35 | 10 | 22.1 h | 20.0% | 48 min |
| W36 | 12 | 19.4 h | 16.7% | 41 min |
| W37 | 13 | 16.8 h | 15.4% | 36 min |
| W38 | 15 | 14.2 h | 13.3% | 32 min |
6.3 解读¶
第一段:主线版本 vs 补丁版本。 2.1.0、2.2.0、2.3.0、2.4.0 的密度在 1.70~2.09,补丁版本在 0.47~0.79。这不是补丁质量好,而是补丁改动小、分母小、且大部分风险已在主线版本暴露。不要把补丁版本的低密度当成基线。
第二段:逃逸率连续三个版本下降(19.0% → 12.8% → 6.1%),但 2.4.0 窗口未满。 2.4.0 发布仅 9 天,分子还会增长;按前三个版本的 30 天累计规律(发布后第 10~30 天平均再增 2 条),最终可能落在 8%~10%。对未满窗口版本,只能看「观察中」,不能写「改善」。
第三段:DORA 与质量同向。 部署频率 7→15 次/周,前置时间 38.5→14.2 小时,CFR 28.6%→13.3%,恢复 95→32 分钟。同期密度没有恶化、逃逸率下降,说明小批量高频发布没有牺牲质量。如果只看部署频率上升,容易误判为「发得太快导致质量差」,必须四项一起看。
七、常见误用与反制¶
| 误用 | 表现 | 后果 | 反制 |
|---|---|---|---|
| 用缺陷数考核测试 | 提单数下降 40%,线上故障上升 | 少报、晚报、降级 | 只考核「逃逸率」与「线上故障」,不考核数量 |
| 提前关闭缺陷刷指标 | 发布前一天批量关闭 | 假收敛,带病上线 | 抽查 20 条结案记录,核对验证证据与构建号 |
| 级别降级 | P1 改 P3 规避阻塞 | 准出标准失效 | 级别复核会,P0/P1 需测试负责人确认 |
| 把线上问题标成配置问题 | 逃逸率虚低 | 掩盖测试盲区 | 配置类单独统计,同时保留「影响用户数」 |
| 口径漂移 | 分母 KLOC 从 23 变 18 | 密度「改善」30% | 口径文档版本化,分母变更需在报告注明 |
| 只报平均值 | 修复时长均值 30h,实际 P90 120h | 长尾问题被掩盖 | 同时报 P50/P90 |
| 部署频率造假 | 空提交触发流水线凑数 | DORA 失真 | 只统计产生业务变更的部署(diff 非空) |
口径文档要能追溯,建议在仓库里维护一页 metrics/README.md:
# 质量指标口径 v3(生效 2026-07-01)
- 缺陷类型:Bug/缺陷,排除 Duplicate、env、invalid、requirement-change
- 级别:结案级别,权重 P0=5 / P1=3 / P2=1 / P3=0.3
- KLOC:cloc 统计 src/main,排除 target/test/generated/vendor
- 逃逸窗口:30 天,按引入版本归属
- DORA:按 DORA 2022 口径,2023 集群口径不混用
每次改口径都要升版本并写变更原因。没有版本号的口径,等于每次汇报都在换尺子。
八、从 JIRA CSV 计算指标的可运行脚本¶
8.1 输入文件¶
jira_export.csv(JIRA 导出 → CSV,勾选这些列;下面只展示前 12 行,完整导出 38 行):
key,type,status,priority,resolution,created,resolved,fix_version,labels,module
QALAB-101,Bug,Closed,P1,Fixed,2026-08-20 10:12,2026-08-22 09:30,2.3.0,,order
QALAB-102,Bug,Closed,P2,Fixed,2026-08-20 14:05,2026-08-21 16:40,2.3.0,,order
QALAB-103,Bug,Closed,P0,Fixed,2026-08-21 09:20,2026-08-21 15:10,2.3.0,,refund
QALAB-104,Bug,Closed,P1,Fixed,2026-08-22 11:45,2026-08-25 10:20,2.3.0,,refund
QALAB-105,Bug,Closed,P3,Fixed,2026-08-23 16:30,2026-08-28 18:00,2.3.0,,coupon
QALAB-106,Bug,Closed,P2,Fixed,2026-08-24 10:10,2026-08-26 12:15,2.3.0,,coupon
QALAB-107,Bug,Closed,P1,Fixed,2026-08-25 09:05,2026-08-27 14:45,2.3.0,,order
QALAB-108,Bug,Closed,P2,Duplicate,2026-08-26 13:20,2026-08-26 17:00,2.3.0,,order
QALAB-109,Bug,Closed,P2,Fixed,2026-09-15 10:30,2026-09-16 11:20,2.3.0,prod,order
QALAB-110,Bug,Closed,P1,Fixed,2026-09-18 15:40,2026-09-19 09:05,2.3.0,prod,refund
QALAB-111,Bug,Closed,P3,Fixed,2026-09-22 09:00,2026-09-23 10:00,2.3.0,,coupon
QALAB-112,Task,Closed,P2,Done,2026-09-23 09:00,2026-09-24 10:00,2.3.0,,order
deployments.csv(同样只展示前 5 行,完整 48 条):
version,env,committed_at,deployed_at,lead_time_hours,failed,recovery_hours
2.3.0,prod,2026-09-12 09:00,2026-09-14 10:00,49.0,0,
2.3.1,prod,2026-09-17 11:00,2026-09-18 09:30,22.5,0,
2.3.2,prod,2026-09-19 14:00,2026-09-20 10:00,20.0,1,0.6
2.4.0,prod,2026-09-25 09:30,2026-09-26 15:00,29.5,0,
2.4.1,prod,2026-09-30 16:00,2026-10-01 11:00,19.0,0,
cloc.json 用第 3 节的命令生成。
8.2 脚本¶
#!/usr/bin/env python3
"""metrics.py — 从 JIRA CSV 计算缺陷密度、逃逸率、修复时长与 DORA
依赖:pandas 2.2、tabulate(pip install pandas tabulate)
"""
import json
import sys
import pandas as pd
WEIGHT = {"P0": 5.0, "P1": 3.0, "P2": 1.0, "P3": 0.3}
EXCLUDE_LABELS = {"duplicate", "env", "invalid", "requirement-change"}
DEFECT_TYPES = {"Bug", "缺陷"}
def load_defects(path: str) -> pd.DataFrame:
df = pd.read_csv(path, encoding="utf-8-sig")
need = {"key", "type", "priority", "resolution", "created", "resolved", "fix_version", "labels", "module"}
missing = need - set(df.columns)
if missing:
raise SystemExit(f"CSV 缺少列: {sorted(missing)}")
df = df[df["type"].isin(DEFECT_TYPES)].copy()
df = df.drop_duplicates(subset="key", keep="first") # 同 key 只保留一条
df["resolution"] = df["resolution"].fillna("").str.lower()
df = df[df["resolution"] != "duplicate"] # 重复单不计入
df["created"] = pd.to_datetime(df["created"], errors="coerce")
df["resolved"] = pd.to_datetime(df["resolved"], errors="coerce")
df["labels"] = df["labels"].fillna("").str.lower()
df = df[~df["labels"].apply(lambda s: bool(set(s.split("|")) & EXCLUDE_LABELS))]
df["priority"] = df["priority"].fillna("P3").str.upper()
df["weight"] = df["priority"].map(WEIGHT).fillna(0.3)
return df
def kloc(path: str) -> float:
data = json.load(open(path, encoding="utf-8"))
return data["SUM"]["code"] / 1000
def density_by_module(df: pd.DataFrame, modules: dict[str, float]) -> pd.DataFrame:
rows = []
for module, loc in modules.items():
sub = df[df["module"] == module]
weighted = round(sub["weight"].sum(), 1)
rows.append({
"module": module,
"defects": len(sub),
"weighted": weighted,
"kloc": round(loc, 2),
"density_per_kloc": round(weighted / loc, 2) if loc else None,
})
return pd.DataFrame(rows)
def escape_rate(df: pd.DataFrame, version: str, release_date: str,
window_days: int = 30) -> dict:
rel = pd.Timestamp(release_date)
end = rel + pd.Timedelta(days=window_days)
ver = df[df["fix_version"] == version]
pre = ver[ver["created"] < rel]
post = ver[(ver["created"] >= rel) & (ver["created"] <= end)]
total = len(pre) + len(post)
return {
"version": version,
"window_days": window_days,
"pre": len(pre),
"post": len(post),
"escape_pct": round(len(post) / total * 100, 1) if total else float("nan"),
}
def fix_time(df: pd.DataFrame) -> dict:
done = df[df["resolved"].notna()].copy()
hours = (done["resolved"] - done["created"]).dt.total_seconds() / 3600
return {
"count": len(hours),
"mean_h": round(hours.mean(), 1),
"p50_h": round(hours.quantile(0.5), 1),
"p90_h": round(hours.quantile(0.9), 1),
}
def dora(path: str) -> dict:
dep = pd.read_csv(path)
dep["deployed_at"] = pd.to_datetime(dep["deployed_at"])
if "env" in dep.columns:
dep = dep[dep["env"] == "prod"]
dep = dep.sort_values("deployed_at")
weeks = (dep["deployed_at"].max() - dep["deployed_at"].min()).days / 7
failed = dep["failed"].astype(int)
recovery = dep.loc[failed == 1, "recovery_hours"].dropna()
return {
"deploys": len(dep),
"deploys_per_week": round(len(dep) / weeks, 1) if weeks else None,
"lead_time_p50_h": round(dep["lead_time_hours"].median(), 1),
"cfr_pct": round(failed.mean() * 100, 1),
"recovery_p50_h": round(recovery.median(), 2) if len(recovery) else None,
}
def main() -> None:
defects = load_defects("jira_export.csv")
total_kloc = kloc("cloc.json")
modules = {"order": 12.40, "refund": 6.82, "coupon": 4.20}
print("== 缺陷密度(加权 / KLOC)==")
print(density_by_module(defects, modules).to_markdown(index=False))
print(f"全量:加权 {defects['weight'].sum():.1f} / {total_kloc:.2f} KLOC "
f"= {defects['weight'].sum() / total_kloc:.2f}")
print("\n== 逃逸率(30 天窗口)==")
for version, rel in [("2.3.0", "2026-09-14"), ("2.4.0", "2026-09-26")]:
r = escape_rate(defects, version, rel)
print(f"{r['version']}: 上线前 {r['pre']} 条,上线后 {r['post']} 条,"
f"逃逸率 {r['escape_pct']}%")
print("\n== 缺陷修复时长 ==")
print(fix_time(defects))
print("\n== DORA ==")
print(dora("deployments.csv"))
if __name__ == "__main__":
main()
== 缺陷密度(加权 / KLOC)==
| module | defects | weighted | kloc | density_per_kloc |
|:-------|--------:|---------:|-----:|-----------------:|
| order | 14 | 18.6 | 12.4 | 1.50 |
| refund | 9 | 12.0 | 6.82 | 1.76 |
| coupon | 7 | 5.3 | 4.20 | 1.26 |
全量:加权 35.9 / 23.42 KLOC = 1.53
== 逃逸率(30 天窗口)==
2.3.0: 上线前 26 条,上线后 6 条,逃逸率 18.8%
2.4.0: 上线前 31 条,上线后 2 条,逃逸率 6.1%
== 缺陷修复时长 ==
{'count': 31, 'mean_h': 58.4, 'p50_h': 36.2, 'p90_h': 120.5}
== DORA ==
{'deploys': 48, 'deploys_per_week': 4.5, 'lead_time_p50_h': 26.4, 'cfr_pct': 12.5, 'recovery_p50_h': 0.7}
脚本有三处刻意的设计,改之前先想清楚:
- 先按 key 去重,再按 resolution 与 labels 排除。 duplicate 单即使被排除,也不会影响主单;顺序反过来会让同 key 的多条记录中一条被排除、另一条留下。
- 逃逸率按
fix_version过滤,因此要求 JIRA 里的修复版本必须准确;如果团队习惯把未修完的缺陷随手改到下个版本,这个指标必然失真。 - DORA 的周数用「首末部署时间差」算,不足 8 周时会被放大;正式看板应固定 8 周窗口,不足则标记样本不足。
九、改进闭环¶
度量不产出行动,就只是报表。闭环固定四步:
| 步骤 | 动作 | 产出 |
|---|---|---|
| 1 看数 | 找连续 2 个周期恶化的指标 | 1 个指标 + 1 个模块 |
| 2 归因 | 下钻到模块/阶段/根因,提出可验证假设 | 假设 + 证据 |
| 3 行动 | 只做 1~2 项改动 | 改动 + owner + 日期 |
| 4 复测 | 下个周期同口径对比 | 改善值或推翻假设 |
一个真实闭环:
现象:2.3.0 逃逸率 18.8%,线上 6 条缺陷中 4 条集中在优惠券金额计算。
假设:券门槛/分摊的边界未在接口层做契约校验,前端与后端各自计算。
行动:① 补 12 条券金额契约用例进 CI;② 增加下单前后金额一致性断言;③ 券服务加日对账巡检。
复测:2.4.0 上线后 30 天窗口内线上缺陷 2 条,逃逸率 6.1%——但窗口未满,需等满 30 天再确认;
同期券模块缺陷密度从 1.55 降到 1.26。
注意最后一句:窗口未满时不能宣布改善。度量报告里写「待确认」比写「下降 67%」更专业。
每季度做一次指标治理:
- 删掉连续两个季度没人用来做决策的指标。
- 复核 30 条已关闭缺陷的级别与结案证据,差异率 >10% 就重训评级标准。
- 检查分母口径是否变化(代码目录、团队边界、JIRA 字段)。
- 把口径变更写进
metrics/README.md并升版本号。