前端监控 SDK
前端监控 SDK 的设计、实现与工程实践
基于「前端监控平台」整体架构与「前端监控 SDK」五大能力大纲整理,侧重原理与可落地的核心 API,适合作为面试复盘与工程实现参考。
一、前端监控平台整体架构
一个完整的监控体系并不只是「在浏览器里采集数据」,而是一条从 采集 → 加工 → 服务 → 展示 的完整链路。整体可分为四层:
| 层级 | 名称 | 运行环境 | 核心职责 |
|---|---|---|---|
| L1 | 应用接入层(SDK) | nodejs / 小程序 / 浏览器 | 在各端采集原始数据并上报 |
| L2 | 数据层 | 服务端管道 | 数据加工 / 清洗 / 聚合 |
| L3 | 监控平台服务层 | JAVA / NODE | 日志查询、监控通知、告警、巡检 |
| L4 | 监控平台应用层 | 前端 | 数据展示、可视化报表、异常 / 性能分析 |
1.1 应用接入层(SDK)
SDK(Software Development Kit)是整个平台的数据入口,需要做到一套核心 + 多端适配:
- 浏览器: 最主要的采集端,依赖
window、performance、PerformanceObserver等 Web API。 - nodejs: 服务端监控,采集进程异常、接口耗时等。
- 小程序: 在受限的运行时里采集,需用平台提供的 API(如
wx.onError)。
💡 提示: SDK 设计上通常把「核心逻辑」与「平台适配层」分离,core 负责调度与上报,platform 各自实现采集差异。
1.2 数据层
原始数据噪音大、格式不统一,需要在入库前处理:
- 数据加工: 把原始上报体补全字段(如解析 UA、定位地域、关联 sourcemap)。
- 数据清洗: 过滤脏数据 / 重复数据 / 无效错误(如已知的第三方脚本错误)。
- 数据聚合: 按错误指纹、页面、时间窗口聚合,把海量单条事件压缩成有意义的统计指标。
1.3 服务层(JAVA / NODE)
| 能力 | 说明 |
|---|---|
| 日志查询 | 按时间、用户、错误类型检索明细 |
| 监控通知 | 实时推送监控数据 / 状态 |
| 告警 | 指标超阈值时触发(邮件、IM、电话) |
| 巡检 | 定时主动探测页面 / 接口可用性 |
1.4 应用层(前端)
面向使用者的可视化界面:数据展示、可视化报表、异常分析、性能分析。
二、前端监控 SDK 五大能力
SDK 在浏览器端的采集能力可归纳为五类,下文逐一展开:
前端监控 SDK
├── 性能监控 (Performance)
├── 错误监控 (Error)
├── 行为监控 (Behaviour)
├── 异常监控 (Exception)
└── 数据上报 (Report)
三、性能监控(Performance)
性能监控的目标是回答两个问题:页面加载有多快?运行时资源 / 请求是否健康?
3.1 资源加载与请求监控
- resource: image、video、js、css 等静态资源的加载耗时,通过
PerformanceObserver监听resource类型获取。 - fetch / xhr: 通过重写(劫持)
fetch与XMLHttpRequest,记录接口耗时、状态码、成功率。
// 监听资源加载性能
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.initiatorType 标识资源类型:img / script / css / xmlhttprequest 等
report({
type: 'resource',
name: entry.name, // 资源 URL
duration: entry.duration, // 加载总耗时
transferSize: entry.transferSize, // 传输大小(含响应头)
})
}
})
// buffered: true 可拿到 observer 创建之前就已发生的条目
observer.observe({ type: 'resource', buffered: true })
// 劫持 fetch 以统计接口耗时与状态
const originalFetch = window.fetch
window.fetch = async function (...args) {
const start = performance.now()
try {
const res = await originalFetch.apply(this, args)
// 记录成功请求:耗时、状态码
report({ type: 'fetch', url: args[0], status: res.status, duration: performance.now() - start })
return res
} catch (err) {
// 记录失败请求(网络中断等)
report({ type: 'fetch', url: args[0], status: 0, duration: performance.now() - start, error: true })
throw err // 必须抛出,避免吞掉业务侧的错误
}
}
⚠️ 注意: 劫持原生 API 时务必保留对原函数的引用并正确转发参数 / 抛出异常,否则会破坏业务逻辑。
3.2 核心性能指标对比
这是面试高频考点,务必能区分「绘制类」与「交互类」指标:
| 指标 | 全称 | 含义 | 关注点 |
|---|---|---|---|
| FP | First Paint | 首次绘制,浏览器渲染任意像素的时间 | 白屏时间 |
| FCP | First Contentful Paint | 首次内容绘制,渲染第一个 DOM 内容(文本 / 图像 / svg)的时间 | 内容出现速度 |
| FMP | First Meaningful Paint | 首次有意义绘制,用户能看到有意义内容的时间点 | 主内容呈现 |
| LCP | Largest Contentful Paint | 最大内容绘制,视窗内最大元素的绘制时间 | 主体加载体验(核心指标) |
| LOAD | Load Event | 所有需立即加载的资源(图片 / css 等)完成的时间点 | 完整加载 |
| CLS | Cumulative Layout Shift | 累计布局偏移,页面意外位移的累计分数 | 视觉稳定性 |
| FID | First Input Delay | 首次输入延迟,用户首次交互(点击 / 触摸)到浏览器实际响应的间隔 | 交互响应(核心指标) |
| TTI | Time to Interactive | 可交互时间,页面完全可稳定交互的时间点 | 可用性 |
💡 提示: Google Core Web Vitals 三大核心指标是 LCP(加载)、FID / INP(交互)、CLS(稳定性)。FID 正逐步被 INP(Interaction to Next Paint)取代。
3.3 指标采集示例
// 采集 FCP(首次内容绘制)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
report({ type: 'FCP', value: entry.startTime }) // startTime 即 FCP 时间
}
}
}).observe({ type: 'paint', buffered: true })
// 采集 LCP(最大内容绘制):会多次回调,取最后一次为准
let lcpValue = 0
new PerformanceObserver((list) => {
const entries = list.getEntries()
// 最后一个 entry 才是最终的最大内容元素
lcpValue = entries[entries.length - 1].startTime
}).observe({ type: 'largest-contentful-paint', buffered: true })
// 采集 CLS(累计布局偏移):累加非用户输入引起的位移分数
let clsValue = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// hadRecentInput 为 true 表示是用户主动交互导致的位移,应忽略
if (!entry.hadRecentInput) clsValue += entry.value
}
}).observe({ type: 'layout-shift', buffered: true })
四、错误监控(Error)
目标:捕获并上报运行时的各类错误,并尽可能携带堆栈信息用于定位。
4.1 错误类型对比
| 错误类型 | 捕获方式 | 说明 |
|---|---|---|
| jsError | window.onerror / error 事件 |
JS 运行时错误(语法、引用等) |
| resourceError | addEventListener('error', cb, true) |
资源加载失败(img / script / css),需捕获阶段 |
| promiseError | unhandledrejection 事件 |
未被 .catch 的 Promise rejection |
| reactError | Error Boundary(componentDidCatch) |
React 组件渲染错误 |
| vueError | app.config.errorHandler |
Vue 组件错误 |
4.2 JS 错误与 Promise 错误
// 1. 捕获 JS 运行时错误
window.onerror = function (msg, url, line, column, error) {
report({
type: 'jsError',
message: msg,
filename: url,
position: `${line}:${column}`,
stack: error?.stack, // 堆栈信息,配合 sourcemap 还原源码位置
})
return true // 返回 true 阻止错误继续在控制台抛出(按需)
}
// 2. 捕获未处理的 Promise 异常
window.addEventListener('unhandledrejection', (e) => {
report({
type: 'promiseError',
reason: e.reason?.message || e.reason, // rejection 的原因
stack: e.reason?.stack,
})
})
4.3 资源加载错误
资源错误不会冒泡,必须在捕获阶段监听:
window.addEventListener(
'error',
(e) => {
const target = e.target
// 区分资源错误与 JS 错误:资源错误的 target 是具体的 DOM 元素
if (target && (target.src || target.href)) {
report({
type: 'resourceError',
tagName: target.tagName, // IMG / SCRIPT / LINK
url: target.src || target.href, // 加载失败的资源地址
})
}
},
true // 第三个参数 true 表示在捕获阶段监听,否则拿不到资源错误
)
🚨 警告: 资源加载错误只能在「捕获阶段」捕获(
useCapture = true),且无法被window.onerror捕获。
4.4 框架错误
// Vue 3 全局错误处理
app.config.errorHandler = (err, instance, info) => {
report({ type: 'vueError', message: err.message, stack: err.stack, info }) // info 描述错误来源钩子
}
// React 错误边界(Class 组件)
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
// errorInfo.componentStack 是组件层级堆栈
report({ type: 'reactError', message: error.message, stack: errorInfo.componentStack })
}
render() {
return this.props.children
}
}
💡 提示: 压缩后的 stack 难以阅读,需在数据层结合 sourcemap 反解为源码位置,这正是「数据加工」环节的典型工作。
五、行为监控(Behaviour)
目标:还原用户做了什么,为错误复现提供上下文。
- 自定义埋点: 业务主动调用 SDK 上报关键动作(如下单、点击按钮)。
- 用户行为栈(click): 维护一个有限长度的行为队列,记录点击等操作,错误发生时一并上报,形成「面包屑(breadcrumb)」。
- history / hash: 监听路由变化以采集 PV / UV 与页面停留。
- 错误回放(rrweb): 录制 DOM 变化,实现「像视频一样」回放用户操作现场。
5.1 路由监听(SPA 关键)
SPA 切换页面不会触发刷新,需手动监听两种路由模式:
// hash 模式:监听 hashchange
window.addEventListener('hashchange', (e) => {
report({ type: 'pv', from: e.oldURL, to: e.newURL })
})
// history 模式:history.pushState / replaceState 不触发任何事件,需重写
const rawPush = history.pushState
history.pushState = function (...args) {
const res = rawPush.apply(this, args)
// 重写后手动派发自定义事件,供监听方感知路由变化
window.dispatchEvent(new Event('pushstate'))
return res
}
window.addEventListener('pushstate', () => report({ type: 'pv', to: location.href }))
window.addEventListener('popstate', () => report({ type: 'pv', to: location.href })) // 前进 / 后退
⚠️ 注意:
popstate只在浏览器前进 / 后退时触发,pushState/replaceState不触发,所以必须重写这两个方法。
5.2 用户行为栈(面包屑)
const breadcrumbs = []
const MAX = 20 // 限制队列长度,避免内存膨胀
function pushBreadcrumb(item) {
if (breadcrumbs.length >= MAX) breadcrumbs.shift() // 满了就丢弃最旧的
breadcrumbs.push({ ...item, timestamp: Date.now() })
}
// 记录点击行为
document.addEventListener('click', (e) => {
pushBreadcrumb({ type: 'click', tag: e.target.tagName, text: e.target.innerText?.slice(0, 20) })
})
// 错误上报时携带 breadcrumbs,即可还原「出错前用户的操作路径」
六、异常监控(Exception)
区别于「错误监控」捕获显式报错,异常监控关注没有报错但页面已不可用的场景。
| 异常 | 现象 | 检测思路 |
|---|---|---|
| 页面白屏 | 页面空白、无内容 | 采样关键点的 elementFromPoint,判断是否命中容器 / 空白 |
| 页面崩溃 | 标签页崩溃、无响应 | 心跳机制 + load 标记,结合 Service Worker 检测 |
| 页面卡顿 | 交互迟钝、掉帧 | 监控 longtask 长任务、requestAnimationFrame 帧率 |
6.1 白屏检测(采样点法)
function isBlankScreen() {
const points = []
// 在屏幕横向 / 纵向取若干采样点
for (let i = 1; i <= 9; i++) {
const x = (window.innerWidth * i) / 10
const y = window.innerHeight / 2
const el = document.elementFromPoint(x, y) // 取该坐标最顶层元素
points.push(el?.tagName)
}
// 若采样点几乎都落在 body / html 这类「空容器」上,判定为白屏
const emptyCount = points.filter((t) => t === 'HTML' || t === 'BODY').length
return emptyCount >= 8
}
6.2 卡顿检测(长任务)
// longtask:执行超过 50ms 的任务会阻塞主线程,造成卡顿
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
report({ type: 'longtask', duration: entry.duration }) // duration > 50ms
}
}).observe({ entryTypes: ['longtask'] })
七、数据上报(Report)
采集到的数据最终要送回服务端。三种主流上报方式对比:
| 方式 | 原理 | 跨域 | 携带数据量 | 页面卸载时可靠性 | 典型场景 |
|---|---|---|---|---|---|
| img | new Image().src 发 GET 请求 |
天然支持 | 小(受 URL 长度限制) | 较好 | 简单埋点、像素打点 |
| ajax (xhr/fetch) | 异步 HTTP 请求 | 需 CORS | 大(支持 POST body) | 差(卸载时可能被取消) | 复杂结构化数据 |
| sendBeacon | 浏览器后台异步发送 | 需 CORS | 中 | 最好(不阻塞、不被卸载打断) | 页面卸载时上报(首选) |
7.1 三种方式示例
// 1. img 上报:兼容性最好,无需考虑跨域
function reportByImg(data) {
const img = new Image()
// 数据拼接到 query string,注意编码
img.src = `https://log.example.com/report?data=${encodeURIComponent(JSON.stringify(data))}`
}
// 2. sendBeacon 上报:页面卸载时最可靠,浏览器会保证发出
function reportByBeacon(data) {
// 返回 false 表示加入发送队列失败(如数据过大),需降级
return navigator.sendBeacon('https://log.example.com/report', JSON.stringify(data))
}
// 3. 统一上报入口:优先 sendBeacon,降级到 img
function report(data) {
if (navigator.sendBeacon) {
const ok = reportByBeacon(data)
if (ok) return
}
reportByImg(data) // 降级方案
}
💡 提示: 在
visibilitychange(页面隐藏)或pagehide时机用 sendBeacon 批量上报,比unload更可靠(移动端unload常不触发)。
7.2 上报优化策略
- 合并上报: 收集多条数据后批量发送,减少请求数。
- 空闲上报: 用
requestIdleCallback在浏览器空闲时上报,不抢占主线程。 - 抽样上报: 海量场景下按比例采样,降低服务端压力。
- 去重: 相同错误指纹在短时间内只报一次。
// 空闲时批量上报
let queue = []
function addToQueue(data) {
queue.push(data)
if (queue.length >= 10) flush() // 攒够一批就发
}
function flush() {
if (!queue.length) return
requestIdleCallback(() => {
navigator.sendBeacon('https://log.example.com/report', JSON.stringify(queue))
queue = [] // 清空队列
})
}
八、知识脉络总结
采集 (SDK) 加工 / 清洗 / 聚合 (数据层) 服务 (JAVA/NODE) 展示 (前端)
│ │ │ │
├─ 性能 Performance ──┐ ├─ 解析 UA / 地域 ├─ 日志查询 ├─ 数据展示
├─ 错误 Error ─┤ ├─ sourcemap 反解 ├─ 监控通知 ├─ 可视化报表
├─ 行为 Behaviour ─┼─ 上报 ──▶ ├─ 过滤脏数据 ──────────▶ ├─ 告警 (阈值) ──────▶ ├─ 异常分析
├─ 异常 Exception ─┤ Report └─ 按指纹聚合 └─ 巡检 └─ 性能分析
└─ (img/ajax/beacon)─┘
面试速记:
- 性能记住「绘制类(FP/FCP/FMP/LCP/LOAD)+ 交互稳定类(FID/TTI/CLS)」,核心是 LCP / FID / CLS。
- 错误四种捕获:
onerror(JS)、捕获阶段error(资源)、unhandledrejection(Promise)、框架errorHandler/ Error Boundary。 - 行为核心是「面包屑 + 路由劫持 + rrweb 回放」。
- 上报首选 sendBeacon,降级 img,记住三者跨域 / 容量 / 卸载可靠性差异。