一、引言
「后端挂了我们会知道,前端卡了没人察觉」——这是很多团队的真实状态。前端监控(RUM,Real User Monitoring)解决的是真实用户在真实设备上的体验与故障:页面 4 秒才显示、某个老机型用户白屏、生产环境报了一堆 minified 错误却无法定位。这些问题实验室永远测不出来,只有把埋点铺到真实用户身上才能看见。
本文从零构建一套可落地的前端监控体系:RUM 指标采集(PerformanceObserver + sendBeacon)、JS 错误上报与 Sourcemap 堆栈还原、性能采样的统计设计、Session Replay 的实现思路,以及从告警到根因的闭环流程。指标的语义基础可参考 Core Web Vitals 深入,接入层也可用 Vercel Analytics(见 Vercel Analytics & Speed Insights)快速起步,但本文讲的是自己做、可掌控的方案。
二、RUM 采集原理与实现
2.1 采集什么
RUM 采集分四类:
| 类型 | 数据点 | 来源 API |
|---|---|---|
| 性能指标 | LCP / INP / CLS / TTFB / FCP | PerformanceObserver + navigation entries |
| 资源加载 | img/script/fetch 的耗时与失败 | PerformanceObserver('resource') |
| 错误 | JS 异常、未捕获 Promise、资源加载失败 | window.onerror / unhandledrejection |
| 行为 | 路由切换、点击、页面生命周期 | 手动埋点 + pagehide |
2.2 核心采集代码
一个最小 RUM SDK(TypeScript):
// rum.ts — 最小可运行 RUM 采集器
const ENDPOINT = 'https://monitor.example.com/collect'
type Metrics = {
lcp?: number
cls?: number
inp?: number
ttfb?: number
fp?: number
url: string
ua: string
ts: number
vitals?: Record<string, string>
}
function report(payload: Record<string, unknown>) {
// 页面卸载/切后台时也要发出去 → sendBeacon 不被中断
if (navigator.sendBeacon) {
navigator.sendBeacon(
ENDPOINT,
new Blob([JSON.stringify(payload)], { type: 'application/json' }),
)
} else {
fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
// keepalive:允许在页面卸载时继续发送
keepalive: true,
})
}
}
// 1) 收集 Web Vitals
let cls = 0
const metrics: Metrics = {
url: location.href,
ua: navigator.userAgent,
ts: Date.now(),
}
new PerformanceObserver((list) => {
const entries = list.getEntries()
metrics.lcp = entries[entries.length - 1]?.startTime
report({ type: 'lcp', ...metrics })
}).observe({ type: 'largest-contentful-paint', buffered: true })
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
if (!e.hadRecentInput) cls += e.value
}
metrics.cls = cls
}).observe({ type: 'layout-shift', buffered: true })
// INP 需要等会话结束或兜底上报
window.addEventListener('pagehide', () => {
report({ type: 'page', ...metrics })
}, { once: true })
// 2) TTFB:navigation entry
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
if (nav) {
metrics.ttfb = nav.responseStart
report({ type: 'ttfb', ...metrics })
}
// 3) 错误采集
window.addEventListener('error', (ev) => {
report({ type: 'error', message: ev.message, stack: ev.error?.stack })
}, true) // 捕获阶段捕获资源加载错误
window.addEventListener('unhandledrejection', (ev) => {
report({ type: 'error', message: String(ev.reason) })
})
2.3 上报时机与体积控制
- 时机:LCP/CLS 在
observer回调里即可报;INP 建议页面pagehide或 10s 兜底统一报;TTFB 立即报。 - 合并:把同页面的多条事件合并成一个批次,
sendBeacon一次发完,降低请求量。 - 体积:每条 ≤ 10KB,压缩 UA、裁剪堆栈(保留前 20 帧)。
- 去重:同 URL + 同错误签名去重,避免循环报错打爆上报管道。
三、错误上报与 Sourcemap 堆栈还原
3.1 生产环境是 minified 的
生产代码经压缩混淆后,error.stack 长这样:
TypeError: Cannot read properties of undefined (reading 'map')
at o.ev (https://cdn.example.com/_next/static/chunks/main-abc123.js:1:23456)
at s.sj (https://cdn.example.com/_next/static/chunks/app.js:1:12345)
没法直接定位源码。要还原,需要 Sourcemap + 后端符号化(symbolication)。
3.2 构建期上传 Sourcemap
关键决策:Sourcemap 只上传给监控平台,绝不下发到浏览器(否则源码裸奔)。Next.js 配置:
// next.config.js
module.exports = {
productionBrowserSourceMaps: false, // 不给浏览器
experimental: {
// 构建后把 map 文件交给脚本上传(如 Sentry/自建符号化服务)
},
webpack(config, { isServer, dev }) {
if (!dev && !isServer) {
config.devtool = 'hidden-source-map'
}
return config
},
}
# 构建后上传 sourcemap(示例脚本)
npx sentry-cli sourcemaps upload --org=... --project=... ./out/_next/static/chunks
3.3 后端符号化
监控后端拿到堆栈与 sourceMappingURL 后,查 Sourcemap 还原出源码位置 + 原始代码行:
// symbolicate.ts — 用 source-map 库还原堆栈
import { SourceMapConsumer } from 'source-map'
export async function symbolicate(stack: string, sourcemaps: Map<string, Buffer>) {
const consumer = await new SourceMapConsumer(await sourcemaps.get(url).json())
return stack.split('\n')
.map((line) => {
const m = line.match(/at .* \(.*?:(\d+):(\d+)\)/)
if (!m) return line
const pos = consumer.originalPositionFor({
line: +m[1], column: +m[2],
})
if (pos.source && pos.line) {
const source = pos.source.split('/').pop()
return ` at ${pos.name ?? '<anonymous>'} (${source}:${pos.line}:${pos.column})`
}
return line
})
.join('\n')
}
还原后:
TypeError: Cannot read properties of undefined (reading 'map')
at processList (src/components/List.tsx:42:18)
at render (src/pages/index.tsx:18:9)
3.4 错误聚合与分级
按「错误签名」聚合,分级处理:
| 等级 | 示例 | 动作 |
|---|---|---|
| Critical | 白屏、主流程抛错、崩溃 | 立即告警 + 追查发布 |
| Warning | 次级功能异常、资源加载失败 | 日会复盘 |
| Info | 用户操作路径异常、兼容性错误 | 汇总趋势 |
// 错误签名:message + 首帧源码文件:行号 → 决定聚合桶
function signature(err: { message: string; stack?: string }) {
const file = err.stack?.match(/\(?([^:)]+\.(tsx?|jsx?|js)):(\d+):\d+\)?/)
return `${err.message}|${file?.[1] ?? 'unknown'}:${file?.[3] ?? '0'}`
}
四、性能采样与分位数聚合
4.1 为什么要采样
100% 采集在流量大时是灾难:每秒上万条 beacon 打爆存储与费用。RUM 通常按流量比例采样,但采样必须保证统计代表性。
4.2 采样策略设计
| 策略 | 适用 | 缺点 |
|---|---|---|
| 固定比例(如 10%) | 大流量、指标类 | 小流量页面样本不足 |
| 自适应采样 | 页面流量不均时 | 实现复杂 |
| 错误全采 + 性能采样 | 错误必采、性能抽采 | 需要两套通道 |
| 关键页面必采 + 其他抽采 | 转化页、核心漏斗 | 需要页面白名单 |
// 分布式采样:按 user id / session id 哈希,保证同一用户一致性
const SAMPLE_RATE = 0.1 // 10%
function shouldSample(seed: string) {
// 简单一致性哈希,同一 seed 始终同结果
let hash = 0
for (let i = 0; i < seed.length; i++) {
hash = (hash * 31 + seed.charCodeAt(i)) | 0
}
return (hash & 0xffff) / 0xffff <= SAMPLE_RATE
}
⚠️ 用「随机数」采样会导致同一会话的多个事件被随机切断;用「用户/会话 id 哈希」采样,整个会话一致采或不采,Session Replay 才能完整。
4.3 分位数聚合,不看平均数
平均数被长尾用户拉高,掩盖真实分布。看 P50 / P75 / P95 / P99:
-- ClickHouse 或 PostgreSQL 聚合示例(P75)
SELECT
url,
count() AS n,
quantile(0.75)(lcp) AS lcp_p75,
quantile(0.90)(inp) AS inp_p90,
quantile(0.75)(cls) AS cls_p75
FROM rum_events
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY url
ORDER BY lcp_p75 DESC
| 分位 | 代表 |
|---|---|
| P50 | 中位用户体验(正常) |
| P75 | 谷歌 CWV 官方口径,代表多数用户 |
| P95/P99 | 长尾、弱网、低端设备——最容易出问题的用户 |
优化目标按 P75 对齐 CWV 阈值,同时用 P95 盯长尾。
五、Session Replay:回放用户真实操作
5.1 原理:事件流重放
Session Replay 不是录屏视频,而是记录 DOM 变异 + 用户输入 + 网络请求的时间流,后端重放:
[记录] [DOM 快照] [click] [input "abc"] [网络 res] [DOM diff] [scroll] ...
↓ 重放引擎按时间线逐帧应用
// replay.ts — 最小 DOM 变异采集
import { patch } from 'rrweb' // 业界常用库
patch((event) => {
// 批量缓存,按时间片打包发送
buffer.push(event)
if (buffer.length >= 50) flush()
})
5.2 隐私与成本控制
- 掩码:输入框、密码、信用卡号用
maskText脱敏;可用data-rs-ignore标记敏感区。 - 降采样:只对
shouldSample命中的会话录制,replay 数据量大(一个会话可达 MB 级)。 - 保留策略:只保留有错误/异常指标的会话(如报错会话、白屏会话),正常会话 24h 后清理。
// 只在出问题时才保留完整 replay
if (errorSignature) {
persistReplay(sessionId, replayStream) // 标记保留
} else {
discardReplay(sessionId)
}
Session Replay 的价值在于把「指标差」变成「看得见的过程」:LCP 差在回放里是图片加载、布局抖动还是字体闪烁,一目了然,直接指导优化。
六、告警闭环与根因分析
6.1 告警设计:别把「事件」当「告警」
监控不是「有错就告警」——生产环境每分钟都有噪声。告警应建立在聚合后的异常上:
| 告警类型 | 触发条件 | 示例 |
|---|---|---|
| 崩溃率突增 | 错误数 / 会话数 比值突增(环比) | JS Error 率 > 2%,较昨日翻倍 |
| 性能回归 | 核心页面 P75 指标越过阈值 | LCP P75 > 2.5s 持续 10 分钟 |
| 白屏/首屏失败 | FCP 缺失 或 FP 超阈值 | FP P95 > 8s |
| 地域/版本异常 | 某版本或地区指标突变 | Chrome 129 用户 INP 突增 |
// 自建告警:滑动窗口 + 环比
const ratio = errorsInWindow / sessionsInWindow
if (ratio > 0.02 && ratio > baseline * 2) {
await notify({ channel: 'oncall', msg: `JS Error 率 ${ratio.toFixed(3)}` })
}
6.2 根因定位流程(闭环)
1. 告警触发 → 看指标 → 是否某个 URL/版本/地区突增
2. 拉该会话 Session Replay → 定位复现路径
3. 错误列表 → Sourcemap 符号化 → 定位源码文件:行
4. 关联发布记录 → 找出对应部署时间戳
5. 回滚 / 修复 → 验证指标回落 → 更新告警基线
# 关联发布:给部署打 tag,监控里按 version 过滤
git tag release-2026-09-27 && git push origin release-2026-09-27
# 前端 SDK 带上版本
navigator.sendBeacon(ENDPOINT, new Blob([JSON.stringify({
version: __BUILD_VERSION__, // 构建注入
})]))
6.3 观测数据分层
| 层 | 工具 | 用途 |
|---|---|---|
| 指标层 | RUM SDK + 分位数 | 趋势、告警 |
| 事件层 | 错误流 + 资源失败 | 问题定位 |
| 会话层 | Session Replay | 用户视角复现 |
| 追踪层 | 前端 trace + 后端 trace 关联 | 端到端耗时(用 traceparent 头透传) |
// 把 trace id 透传给后端,打通前后端链路
const traceId = crypto.randomUUID()
fetch(url, {
headers: { 'x-trace-id': traceId },
})
// 后端日志带同一 trace-id,前端报错也带 → 一次请求全链路可查
七、落地清单与最佳实践
| 实践 | 理由 |
|---|---|
| 错误全采、性能按会话哈希采样 | 成本可控且统计代表 |
| Sourcemap 只传监控平台,不下发浏览器 | 还原堆栈且不泄露源码 |
| 指标用 P75/P95,不用平均值 | 长尾可见,对齐 CWV 口径 |
用 sendBeacon + keepalive 上报 | 页面卸载时不丢数据 |
| 会话级采样 + 错误会话保留 replay | 存储省、问题可回放 |
| 告警建立在聚合与环比上 | 过滤噪声,避免告警疲劳 |
| 前后端共享 trace id | 一次故障全链路可查 |
| 持续度量,指标不进则退 | RUM 是系统工程,不是上线一次 |
八、总结
前端监控体系的本质是把「看不见的用户体验」变成「可度量、可回放、可告警」的工程资产。四件事缺一不可:RUM 指标采集(PerformanceObserver + sendBeacon)告诉你「慢在哪」,错误上报 + Sourcemap 告诉你「坏在哪」,采样与分位数聚合告诉你「多严重」,Session Replay 与告警闭环告诉你「为什么」。配合性能优化的优化动作,形成「度量 → 优化 → 再度量」的正循环——这正是可观测性要解决的问题。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。