从零实现一个前端监控系统:性能、错误与用户行为全方位监控
从零实现一个前端监控系统:性能、错误与用户行为全方位监控
深入探索前端监控 SDK 的实现原理,从性能指标采集、错误捕获到用户行为追踪,手把手教你打造一个企业级的前端监控方案。
前言
在现代 Web 应用中,前端监控是保障产品质量和用户体验的重要基石。一个完善的前端监控系统应该具备以下能力:
- 性能监控:采集页面加载性能、接口请求耗时等关键指标
- 错误监控:捕获 JS 错误、资源加载失败、Promise 异常等问题
- 行为监控:追踪用户点击、页面跳转、PV/UV 等行为数据
- 数据上报:高效、可靠地将数据发送到服务端
本文将基于 webEyeSDK 项目,详细讲解如何从零实现一个前端监控 SDK。
架构设计
整体架构
webEyeSDK
├── src/
│ ├── webEyeSDK.js # SDK 入口文件
│ ├── config.js # 配置管理
│ ├── report.js # 数据上报
│ ├── cache.js # 数据缓存
│ ├── utils.js # 工具函数
│ ├── performance/ # 性能监控
│ │ ├── index.js
│ │ ├── observeLCP.js
│ │ ├── observerFCP.js
│ │ ├── observerLoad.js
│ │ ├── observerPaint.js
│ │ ├── observerEntries.js
│ │ ├── fetch.js
│ │ └── xhr.js
│ ├── error/ # 错误监控
│ │ └── index.js
│ └── behavior/ # 行为监控
│ ├── index.js
│ ├── pv.js
│ ├── onClick.js
│ └── pageChange.js
模块职责
| 模块 | 职责 |
|---|---|
| Performance | 采集页面性能指标(FCP、LCP、Load 等) |
| Error | 捕获 JS 错误、资源错误、Promise 错误 |
| Behavior | 追踪用户行为(点击、页面跳转、PV) |
| Report | 数据上报(支持 sendBeacon、XHR、Image) |
| Cache | 数据缓存与批量上报 |
核心功能实现
一、性能监控
性能监控是前端监控的核心模块,主要通过 Performance API 和 PerformanceObserver 来采集关键指标。
1. FCP(首次内容绘制)
FCP(First Contentful Paint)测量页面首次渲染任何文本、图像等内容的时间。
import { lazyReportBatch } from '../report';
export default function observerFCP() {
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
observer.disconnect();
const json = entry.toJSON();
const reportData = {
...json,
type: 'performance',
subType: entry.name,
pageUrl: window.location.href,
};
lazyReportBatch(reportData);
}
}
};
// 统计和计算 FCP 的时间
const observer = new PerformanceObserver(entryHandler);
// buffered: true 确保观察到所有 paint 事件
observer.observe({ type: 'paint', buffered: true });
}
核心要点:
- 使用
PerformanceObserver监听paint类型的性能条目 -
buffered: true确保能观察到页面加载过程中已发生的性能事件 - 找到
first-contentful-paint后立即断开监听,避免重复上报
2. LCP(最大内容绘制)
LCP(Largest Contentful Paint)测量视口内最大内容元素渲染的时间,是 Core Web Vitals 的重要指标。
import { lazyReportBatch } from '../report';
export default function observerLCP() {
const entryHandler = (list) => {
if (observer) {
observer.disconnect();
}
for (const entry of list.getEntries()) {
const json = entry.toJSON();
const reportData = {
...json,
type: 'performance',
subType: entry.name,
pageUrl: window.location.href,
};
lazyReportBatch(reportData);
}
};
// 统计和计算 LCP 的时间
const observer = new PerformanceObserver(entryHandler);
observer.observe({ type: 'largest-contentful-paint', buffered: true });
}
LCP 特点:
- LCP 可能会多次触发(如最大内容元素改变),需要持续监听
- 通常在用户交互或页面加载完成后才上报最终值
- Google 建议 LCP 应在 2.5 秒以内
3. XHR/Fetch 请求监控
通过重写 XMLHttpRequest 原型方法,实现接口请求的性能监控。
import { lazyReportBatch } from '../report';
export const originalProto = XMLHttpRequest.prototype;
export const originalSend = originalProto.send;
export const originalOpen = originalProto.open;
function overwriteOpenAndSend() {
originalProto.open = function newOpen(...args) {
this.url = args[1];
this.method = args[0];
originalOpen.apply(this, args);
};
originalProto.send = function newSend(...args) {
this.startTime = Date.now();
const onLoaded = () => {
this.endTime = Date.now();
this.duration = this.endTime - this.startTime;
const { url, method, startTime, endTime, duration, status } = this;
const reportData = {
status,
duration,
startTime,
endTime,
url,
method: method.toUpperCase(),
type: 'performance',
success: status >= 200 && status < 300,
subType: 'xhr'
};
lazyReportBatch(reportData);
this.removeEventListener('loadend', onLoaded, true);
};
this.addEventListener('loadend', onLoaded, true);
originalSend.apply(this, args);
};
}
export default function xhr() {
overwriteOpenAndSend();
}
实现原理:
- 重写
XMLHttpRequest.prototype.open方法,记录请求 URL 和方法 - 重写
XMLHttpRequest.prototype.send方法,记录请求开始时间 - 监听
loadend事件,计算请求耗时并上报
4. 性能监控入口
import fetch from "./fetch";
import observerEntries from "./observerEntries";
import observerLCP from "./observeLCP";
import observerFCP from "./observerFCP";
import observerLoad from "./observerLoad";
import observerPaint from "./observerPaint";
import xhr from "./xhr";
export default function performance() {
fetch();
observerEntries();
observerLCP();
observerFCP();
observerLoad();
observerPaint();
xhr();
}
二、错误监控
错误监控帮助开发者及时发现和定位线上问题,是保障应用稳定性的关键。
1. JS 运行时错误
通过 window.onerror 捕获 JavaScript 运行时错误。
window.onerror = function (msg, url, lineNo, columnNo, error) {
const reportData = {
type: 'error',
subType: 'js',
msg,
url,
lineNo,
columnNo,
stack: error.stack,
pageUrl: window.location.href,
startTime: performance.now(),
};
lazyReportBatch(reportData);
};
参数说明:
-
msg:错误消息 -
url:发生错误的脚本 URL -
lineNo:错误行号 -
columnNo:错误列号 -
error:Error 对象,包含堆栈信息
2. 资源加载错误
资源加载错误(如图片、CSS、JS 文件加载失败)需要通过事件捕获来监听。
window.addEventListener(
'error',
function (e) {
const target = e.target;
if (target.src || target.href) {
const url = target.src || target.href;
const reportData = {
type: 'error',
subType: 'resource',
url,
html: target.outerHTML,
pageUrl: window.location.href,
paths: e.path,
};
lazyReportBatch(reportData);
}
},
true // 使用捕获阶段
);
关键点:
- 必须在捕获阶段监听(第三个参数为
true),因为资源加载错误不会冒泡 - 通过
e.target.src或e.target.href判断是否为资源错误
3. Promise 错误
Promise 中未被捕获的错误需要监听 unhandledrejection 事件。
window.addEventListener(
'unhandledrejection',
function (e) {
const reportData = {
type: 'error',
subType: 'promise',
reason: e.reason?.stack,
pageUrl: window.location.href,
startTime: e.timeStamp,
};
lazyReportBatch(reportData);
},
true
);
4. 框架错误捕获
Vue 错误捕获:
export function install(Vue, options) {
if (__webEyeSDK__.vue) return;
__webEyeSDK__.vue = true;
setConfig(options);
const handler = Vue.config.errorHandler;
Vue.config.errorHandler = function (err, vm, info) {
const reportData = {
info,
error: err.stack,
subType: 'vue',
type: 'error',
startTime: window.performance.now(),
pageURL: window.location.href,
};
lazyReportBatch(reportData);
if (handler) {
handler.call(this, err, vm, info);
}
};
}
React 错误捕获:
export function errorBoundary(err, info) {
if (__webEyeSDK__.react) return;
__webEyeSDK__.react = true;
const reportData = {
error: err?.stack,
info,
subType: 'react',
type: 'error',
startTime: window.performance.now(),
pageURL: window.location.href,
};
lazyReportBatch(reportData);
}
使用方式:
// Vue 项目
import webEyeSDK from './webEyeSDK';
Vue.use(webEyeSDK, { appId: 'xxx' });
// React 项目
import webEyeSDK from './webEyeSDK';
class ErrorBoundary extends React.Component {
componentDidCatch(error, info) {
webEyeSDK.errorBoundary(error, info);
}
render() {
return this.props.children;
}
}
5. 错误监控入口
import { lazyReportBatch } from '../report';
export default function error() {
// 捕获资源加载失败的错误
window.addEventListener('error', function (e) {
const target = e.target;
if (target.src || target.href) {
const url = target.src || target.href;
const reportData = {
type: 'error',
subType: 'resource',
url,
html: target.outerHTML,
pageUrl: window.location.href,
paths: e.path,
};
lazyReportBatch(reportData);
}
}, true);
// 捕获 JS 错误
window.onerror = function (msg, url, lineNo, columnNo, error) {
const reportData = {
type: 'error',
subType: 'js',
msg,
url,
lineNo,
columnNo,
stack: error.stack,
pageUrl: window.location.href,
startTime: performance.now(),
};
lazyReportBatch(reportData);
};
// 捕获 Promise 错误
window.addEventListener('unhandledrejection', function (e) {
const reportData = {
type: 'error',
subType: 'promise',
reason: e.reason?.stack,
pageUrl: window.location.href,
startTime: e.timeStamp,
};
lazyReportBatch(reportData);
}, true);
}
三、行为监控
用户行为监控帮助我们理解用户如何使用应用,为产品优化提供数据支持。
1. PV(页面浏览量)
import { lazyReportBatch } from '../report';
import { generateUniqueId } from '../utils';
export default function pv() {
const reportData = {
type: 'behavior',
subType: 'pv',
startTime: performance.now(),
pageUrl: window.location.href,
referrer: document.referrer,
uuid: generateUniqueId(),
};
lazyReportBatch(reportData);
}
PV 数据包含:
- 当前页面 URL
- 来源页面(
document.referrer) - 访问时间
- 唯一标识(用于关联用户行为链路)
2. 点击行为
import { lazyReportBatch } from '../report';
export default function onClick() {
['mousedown', 'touchstart'].forEach((eventType) => {
window.addEventListener(eventType, (e) => {
const target = e.target;
if (target.tagName) {
const reportData = {
type: 'behavior',
subType: 'click',
target: target.tagName,
startTime: e.timeStamp,
innerHtml: target.innerHTML,
outerHtml: target.outerHTML,
width: target.offsetWidth,
height: target.offsetHeight,
eventType,
path: e.path,
};
lazyReportBatch(reportData);
}
});
});
}
点击数据用途:
- 分析用户交互热点
- 绘制热力图
- 检测异常点击行为
3. 页面跳转
import { lazyReportBatch } from '../report';
import { generateUniqueId } from '../utils';
export default function pageChange() {
let oldUrl = '';
// Hash 路由
window.addEventListener('hashchange', function (event) {
const newUrl = event.newURL;
const reportData = {
from: oldUrl,
to: newUrl,
type: 'behavior',
subType: 'hashchange',
startTime: performance.now(),
uuid: generateUniqueId(),
};
lazyReportBatch(reportData);
oldUrl = newUrl;
}, true);
let from = '';
// History 路由
window.addEventListener('popstate', function (event) {
const to = window.location.href;
const reportData = {
from: from,
to: to,
type: 'behavior',
subType: 'popstate',
startTime: performance.now(),
uuid: generateUniqueId(),
};
lazyReportBatch(reportData);
from = to;
}, true);
}
路由监听:
- 支持 Hash 路由(
hashchange事件) - 支持 History 路由(
popstate事件) - 记录跳转前后 URL,用于分析用户路径
4. 行为监控入口
import onClick from './onClick';
import pageChange from './pageChange';
import pv from './pv';
export default function behavior() {
onClick();
pageChange();
pv();
}
四、数据上报
数据上报是前端监控的最后一步,需要保证数据可靠、高效地发送到服务端。
1. 上报策略
const config = {
url: '',
projectName: 'eyesdk',
appId: '123456',
userId: '123456',
isImageUpload: false,
batchSize: 5,
};
配置项说明:
-
url:上报接口地址 -
appId:应用唯一标识 -
userId:用户标识 -
isImageUpload:是否使用图片方式上报 -
batchSize:批量上报阈值
2. 批量上报
import { addCache, getCache, clearCache } from './cache';
export function lazyReportBatch(data) {
addCache(data);
const dataCache = getCache();
if (dataCache.length && dataCache.length > config.batchSize) {
report(dataCache);
clearCache();
}
}
批量上报优势:
- 减少网络请求次数
- 降低服务端压力
- 提升性能
3. 多种上报方式
export function report(data) {
if (!config.url) {
console.error('请设置上传 url 地址');
}
const reportData = JSON.stringify({
id: generateUniqueId(),
data,
});
// 使用图片方式上报
if (config.isImageUpload) {
imgRequest(reportData);
} else {
// 优先使用 sendBeacon
if (window.navigator.sendBeacon) {
return beaconRequest(reportData);
} else {
xhrRequest(reportData);
}
}
}
上报方式对比:
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| sendBeacon | 异步、不阻塞页面卸载、可靠 | 浏览器兼容性 | 页面关闭时上报 |
| Image | 简单、跨域友好 | 数据大小限制 | 简单数据上报 |
| XHR | 兼容性好、支持 POST | 可能被阻塞 | 常规上报 |
4. sendBeacon 上报
export function beaconRequest(data) {
if (window.requestIdleCallback) {
window.requestIdleCallback(
() => {
window.navigator.sendBeacon(config.url, data);
},
{ timeout: 3000 }
);
} else {
setTimeout(() => {
window.navigator.sendBeacon(config.url, data);
});
}
}
sendBeacon 特点:
- 浏览器在页面卸载时也能可靠发送
- 异步执行,不阻塞页面关闭
- 适合用于页面关闭前的数据上报
5. XHR 上报
export function xhrRequest(data) {
if (window.requestIdleCallback) {
window.requestIdleCallback(
() => {
const xhr = new XMLHttpRequest();
originalOpen.call(xhr, 'post', config.url);
originalSend.call(xhr, JSON.stringify(data));
},
{ timeout: 3000 }
);
} else {
setTimeout(() => {
const xhr = new XMLHttpRequest();
originalOpen.call(xhr, 'post', url);
originalSend.call(xhr, JSON.stringify(data));
});
}
}
requestIdleCallback:
- 在浏览器空闲时执行上报
- 避免阻塞关键渲染任务
- 设置
timeout: 3000确保最迟 3 秒后执行
6. 图片上报
export function imgRequest(data) {
const img = new Image();
img.src = `${config.url}?data=${encodeURIComponent(JSON.stringify(data))}`;
}
Image 上报优势:
- 实现简单
- 天然支持跨域
- 无需担心阻塞
五、数据缓存
import { deepCopy } from './utils.js';
const cache = [];
export function getCache() {
return deepCopy(cache);
}
export function addCache(data) {
cache.push(data);
}
export function clearCache() {
cache.length = 0;
}
缓存机制:
- 使用数组缓存待上报数据
- 达到阈值后批量上报
- 上报后清空缓存
六、工具函数
// 深拷贝
export function deepCopy(target) {
if (typeof target === 'object') {
const result = Array.isArray(target) ? [] : {};
for (const key in target) {
if (typeof target[key] == 'object') {
result[key] = deepCopy(target[key]);
} else {
result[key] = target[key];
}
}
return result;
}
return target;
}
// 生成唯一 ID
export function generateUniqueId() {
return 'id-' + Date.now() + '-' + Math.random().toString(36).substring(2, 9);
}
七、SDK 入口
import performance from './performance/index';
import error from './error/index';
import behavior from './behavior/index';
import { setConfig } from './config';
import { lazyReportBatch } from './report';
window.__webEyeSDK__ = {
version: '0.0.1',
};
// 针对 Vue 项目的错误捕获
export function install(Vue, options) {
if (__webEyeSDK__.vue) return;
__webEyeSDK__.vue = true;
setConfig(options);
const handler = Vue.config.errorHandler;
Vue.config.errorHandler = function (err, vm, info) {
const reportData = {
info,
error: err.stack,
subType: 'vue',
type: 'error',
startTime: window.performance.now(),
pageURL: window.location.href,
};
lazyReportBatch(reportData);
if (handler) {
handler.call(this, err, vm, info);
}
};
}
// 针对 React 项目的错误捕获
export function errorBoundary(err, info) {
if (__webEyeSDK__.react) return;
__webEyeSDK__.react = true;
const reportData = {
error: err?.stack,
info,
subType: 'react',
type: 'error',
startTime: window.performance.now(),
pageURL: window.location.href,
};
lazyReportBatch(reportData);
}
export function init(options) {
setConfig(options);
performance();
error();
behavior();
}
export default {
install,
errorBoundary,
performance,
error,
behavior,
init,
}
使用指南
安装
import webEyeSDK from './webEyeSDK';
// 初始化
webEyeSDK.init({
url: 'http://your-server.com/report',
appId: 'your-app-id',
userId: 'user-123',
batchSize: 10,
});
Vue 项目集成
import Vue from 'vue';
import webEyeSDK from './webEyeSDK';
Vue.use(webEyeSDK, {
url: 'http://your-server.com/report',
appId: 'your-app-id',
});
React 项目集成
import React from 'react';
import webEyeSDK from './webEyeSDK';
class ErrorBoundary extends React.Component {
componentDidCatch(error, info) {
webEyeSDK.errorBoundary(error, info);
}
render() {
return this.props.children;
}
}
// 初始化
webEyeSDK.init({
url: 'http://your-server.com/report',
appId: 'your-app-id',
});
核心特性总结
| 功能模块 | 监控内容 | 实现方式 |
|---|---|---|
| 性能监控 | FCP、LCP、Load、XHR/Fetch | PerformanceObserver、重写原型 |
| 错误监控 | JS 错误、资源错误、Promise 错误 | window.onerror、事件监听 |
| 行为监控 | PV、点击、页面跳转 | 事件监听 |
| 数据上报 | 批量上报、多种方式 | sendBeacon、XHR、Image |
性能优化建议
- 使用 requestIdleCallback:在浏览器空闲时执行数据上报,避免阻塞关键渲染
- 批量上报:减少网络请求次数,降低服务端压力
- sendBeacon:页面关闭时使用 sendBeacon 保证数据可靠性
- 数据压缩:上报前压缩数据,减少传输体积
- 采样上报:对高频事件(如点击)进行采样,减少数据量
扩展方向
- SourceMap 解析:还原压缩后的错误堆栈
- 录屏回放:使用 rrweb 记录用户操作
- 白屏检测:检测页面白屏问题
- 性能评分:基于 Core Web Vitals 计算性能评分
- 告警系统:实时告警通知
参考资料
总结
本文从零实现了一个前端监控 SDK,涵盖了性能监控、错误监控、行为监控三大核心模块。通过 Performance API、事件监听、原型重写等技术,实现了全方位的前端监控能力。
掌握前端监控的实现原理,不仅能帮助你在工作中构建完善的监控系统,还能加深对浏览器性能、错误处理等底层机制的理解。
如果觉得本文有帮助,欢迎点赞收藏,有问题欢迎在评论区讨论!