【背景】 15 年前端,去年我们前端错误率 SLO 从 99.5% 升到 99.9%,结果上线两周值班群被静默失败打爆:一天 3000+ 条告警,其中 80% 是「用户根本没感知到的 catch 吞掉的异常」。最后我们不得不把 SLO 拆回两层,才把值班拉回来。今天想借这个帖子聊聊:你们线上的前端错误率 SLO,分母到底怎么定?
【常见分歧 / 几种做法】 我见过四种口径,团队内部经常吵:
-
只算 SDK 主动上报:window.onerror + unhandledrejection + 业务里显式 reportError 的 try/catch,分母是 PV。优点是干净、可复现;缺点是漏掉「没被 catch 到的静默失败」,比如某个异步任务失败后只是 log 了一行,用户其实拿到了降级数据但不知道。
-
加上「白屏率」:页面加载后 3s 内 #root 无子节点、或 LCP 元素未出现 → 直接算一次失败。这个我比较推荐,因为它贴近用户感知,但需要每个页面写探针,成本不低。
-
加上「关键路径断言」:首屏核心接口 200 + 核心组件 mount 成功 + 关键埋点上报成功 → 三者任一失败算失败。这是「业务口径」,最贴近业务方,但需要业务方一起维护,SLO 会变成「前端 + 后端 + 埋点」三方共担。
-
全量算:包括被 catch 吞掉的、用户没看到的、甚至只是 log.warn 的。这个口径最「诚实」,但值班会被长尾淹没,我在一个 500+ 页面的中后台项目试过一个季度,最后放弃了。
【我的倾向与理由】 我现在推的是「双层 SLO」:
- L1 用户可见层:白屏 + 首屏关键接口失败 + 核心组件 mount 失败,目标 99.95%,分母是 PV。这一层是「用户真的感知到了」,直接对应业务损失,告警进值班群。
- L2 全量错误层:包括所有 SDK 上报 + 静默失败,目标 99.7%,分母是 PV。这一层是「研发债 + 潜在风险」,告警进研发群,不进值班。
理由有三:
- 混在一起会被 L2 的长尾淹没,值班被打爆是必然的——我们去年 9 月就经历了这个,一周 3000+ 告警里只有 40 条是 L1。
- L1 和 L2 的 owner 不一样:L1 通常是前端 + 业务方共担,L2 是前端单侧的债。分两层后 SLA 谈判更清晰。
- 数字上可复现:L1 用「白屏探针 + 首屏接口成功率 + 核心组件 mount 成功率」三个指标取 min,L2 用「SDK 上报错误数 / PV」,都是 Prometheus + Grafana 能直接出的,不需要额外埋点系统。
【补充:L1 白屏探针的具体实现(上次没展开,补上)】 白屏探针我用的不是「#root 无子节点」这种粗暴判据(React 18 的 Suspense 边界会让 #root 有子节点但内容还是 skeleton),而是渲染完整性断言:
// 每个页面声明一个「首屏关键节点选择器」
const CRITICAL_SELECTOR = '[data-critical="first-screen"]'
function checkWhiteScreen(timeout = 3000): Promise<boolean> {
return new Promise((resolve) => {
const start = performance.now()
const check = () => {
if (document.querySelector(CRITICAL_SELECTOR)) {
resolve(false) // 非白屏
return
}
if (performance.now() - start >= timeout) {
resolve(true) // 白屏
} else {
requestAnimationFrame(check)
}
}
requestAnimationFrame(check)
})
}
关键点是 data-critical 属性由业务组件自己声明(比如 <div data-critical="first-screen"> 包在首屏核心卡片外),而不是前端框架层统一加——因为「首屏」的定义是业务概念,不是技术概念。我们 500+ 页面里,有 12 个页面的「首屏」定义完全不同(有的是列表、有的是仪表盘、有的是空态引导),统一选择器根本覆盖不了。
一个踩过的坑:requestAnimationFrame 在页面不可见时(用户切了 tab)会暂停,导致 check 永远跑不到 timeout,白屏判据失效。解法是加一个 setTimeout 兜底:
const fallback = setTimeout(() => resolve(true), timeout + 500)
// 在 resolve(false) 时 clearTimeout(fallback)
【补充:一个上次没提到的隐性坑——「min 取法」的耦合问题】
L1 三个指标取 min 这个设计有一个隐性耦合:如果首屏接口返回 200 但 body 是 {code: 500, msg: "internal error"}(后端业务错误码,HTTP 层是 200),「首屏接口成功率」是绿的,但用户看到的是错误提示页。这时候白屏探针也判不了(因为页面有内容,只是内容是错误提示)。
解法:在「首屏接口成功率」指标里加一层业务状态码断言:
api_success = (http_status == 200) AND (response.code in [0, 200])
这个 response.code 的枚举值需要和业务方约定,我们约定 0 和 200 是成功,其他都算失败。不加这一层的话,L1 会漏掉「HTTP 200 + 业务 500」这种最常见的后端错误模式——我们去年 Q3 有 3 次后端发版导致 L1 假绿,都是这个原因。
【想听大家在什么场景下怎么选】
- 你们的团队用上面四种里的哪一种?或者有更细的口径?
- 分母是 PV / UV / 会话?(我见过用「会话」的,一个用户 5 次失败算 5 次还是 1 次,吵过很久)
- 采样率多少?(RUM 全量上报 vs 1% 采样,对 SLO 精度影响多大,有没有具体数字?)
- 有没有把 INP > 500ms、LCP > 4s 也算进「失败」的?(Web Vitals 三个指标和错误率 SLO 的边界怎么划?)
- 中后台 500+ 页面 vs C 端 50 个页面的场景,口径差别大吗?
欢迎拍砖,最好带具体数字和你们的告警量级,空谈「要看场景」我就没法学。