← Frontend

前端监控 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)─┘

面试速记:

  1. 性能记住「绘制类(FP/FCP/FMP/LCP/LOAD)+ 交互稳定类(FID/TTI/CLS)」,核心是 LCP / FID / CLS。
  2. 错误四种捕获:onerror(JS)、捕获阶段 error(资源)、unhandledrejection(Promise)、框架 errorHandler / Error Boundary。
  3. 行为核心是「面包屑 + 路由劫持 + rrweb 回放」。
  4. 上报首选 sendBeacon,降级 img,记住三者跨域 / 容量 / 卸载可靠性差异。