前端监控体系建设:从异常捕获到性能分析
大家好,我是蔓蔓。在大厂工作时,我负责搭建了团队的前端监控体系。一个好的监控系统能够帮助我们及时发现问题、定位问题、解决问题。今天我来和大家分享如何从零开始构建一套完整的前端监控体系。
监控体系概述
为什么需要监控
监控分类
| 错误监控 | JS错误、资源加载失败、接口错误 | Sentry、Bugsnag |
| 性能监控 | 页面加载、首屏时间、FPS | Lighthouse、Performance API |
| 用户行为 | 点击、页面浏览、表单提交 | Google Analytics、Segment |
| 业务指标 | 转化率、留存率、DAU | 自定义上报 |
错误监控
捕获JS错误
// 全局错误捕获
window.addEventListener('error', (event) => {
const errorInfo = {
type: 'javascript_error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
};
// 上报错误
reportError(errorInfo);
});
// Promise 未捕获异常
window.addEventListener('unhandledrejection', (event) => {
const errorInfo = {
type: 'promise_rejection',
reason: event.reason?.message || String(event.reason),
timestamp: Date.now(),
url: window.location.href
};
reportError(errorInfo);
});
捕获资源加载错误
// 监听资源加载失败
document.addEventListener('error', (event) => {
const target = event.target;
if (target.tagName === 'SCRIPT' ||
target.tagName === 'LINK' ||
target.tagName === 'IMG') {
const errorInfo = {
type: 'resource_error',
resourceType: target.tagName.toLowerCase(),
src: target.src || target.href,
timestamp: Date.now(),
url: window.location.href
};
reportError(errorInfo);
}
}, true);
捕获接口错误
// 重写 fetch
const originalFetch = window.fetch;
window.fetch = async function(…args) {
const [url, options] = args;
const startTime = Date.now();
try {
const response = await originalFetch(…args);
// 检查 HTTP 状态码
if (!response.ok) {
const errorInfo = {
type: 'api_error',
url,
method: options?.method || 'GET',
status: response.status,
duration: Date.now() – startTime,
timestamp: Date.now()
};
reportError(errorInfo);
}
return response;
} catch (error) {
const errorInfo = {
type: 'api_error',
url,
method: options?.method || 'GET',
error: error.message,
duration: Date.now() – startTime,
timestamp: Date.now()
};
reportError(errorInfo);
throw error;
}
};
性能监控
使用 Performance API
// 获取页面加载性能数据
function getPerformanceData() {
const performance = window.performance;
const timing = performance.timing;
return {
// DNS 查询时间
dnsTime: timing.domainLookupEnd – timing.domainLookupStart,
// TCP 连接时间
tcpTime: timing.connectEnd – timing.connectStart,
// 请求响应时间
requestTime: timing.responseEnd – timing.requestStart,
// DOM 解析时间
domParseTime: timing.domComplete – timing.domInteractive,
// 首字节时间
ttfb: timing.responseStart – timing.navigationStart,
// 页面加载总时间
loadTime: timing.loadEventEnd – timing.navigationStart,
// DOMContentLoaded 时间
domContentLoadedTime: timing.domContentLoadedEventEnd – timing.navigationStart
};
}
// 上报性能数据
window.addEventListener('load', () => {
const performanceData = getPerformanceData();
reportPerformance(performanceData);
});
自定义性能指标
// 自定义计时
function startTimer(name) {
performance.mark(`${name}_start`);
}
function endTimer(name) {
performance.mark(`${name}_end`);
performance.measure(name, `${name}_start`, `${name}_end`);
const measure = performance.getEntriesByName(name)[0];
if (measure) {
reportPerformance({
name,
duration: measure.duration,
timestamp: Date.now()
});
// 清理标记
performance.clearMarks(`${name}_start`);
performance.clearMarks(`${name}_end`);
performance.clearMeasures(name);
}
}
// 使用示例
startTimer('api_request');
await fetch('/api/data');
endTimer('api_request');
FPS 监控
// FPS 监控
class FPSMonitor {
constructor() {
this.frameCount = 0;
this.lastTime = performance.now();
this.fps = 60;
}
start() {
this.animationId = requestAnimationFrame(() => this.tick());
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
}
tick() {
this.frameCount++;
const currentTime = performance.now();
if (currentTime – this.lastTime >= 1000) {
this.fps = Math.round(this.frameCount * 1000 / (currentTime – this.lastTime));
this.frameCount = 0;
this.lastTime = currentTime;
// 上报 FPS
reportPerformance({
name: 'fps',
value: this.fps,
timestamp: Date.now()
});
}
this.animationId = requestAnimationFrame(() => this.tick());
}
}
// 使用示例
const fpsMonitor = new FPSMonitor();
fpsMonitor.start();
用户行为监控
点击事件追踪
// 自动追踪点击事件
document.addEventListener('click', (event) => {
const target = event.target;
// 只追踪有 data-track 属性的元素
if (target.dataset.track) {
const eventData = {
type: 'click',
action: target.dataset.track,
label: target.dataset.trackLabel || target.textContent,
timestamp: Date.now(),
url: window.location.href,
position: {
x: event.clientX,
y: event.clientY
}
};
reportBehavior(eventData);
}
}, true);
页面浏览追踪
// 页面浏览追踪
class PageTracker {
constructor() {
this.pageStartTime = Date.now();
this.currentPage = window.location.pathname;
}
trackPageView() {
const pageData = {
type: 'page_view',
page: window.location.pathname,
referrer: document.referrer,
timestamp: Date.now()
};
reportBehavior(pageData);
}
trackPageLeave() {
const pageData = {
type: 'page_leave',
page: this.currentPage,
duration: Date.now() – this.pageStartTime,
timestamp: Date.now()
};
reportBehavior(pageData);
}
}
// 使用示例
const pageTracker = new PageTracker();
pageTracker.trackPageView();
// 页面离开时上报
window.addEventListener('beforeunload', () => {
pageTracker.trackPageLeave();
});
数据上报
上报策略
class Reporter {
constructor() {
this.queue = [];
this.batchSize = 10;
this.maxQueueSize = 100;
this.flushInterval = 5000;
// 定时上报
setInterval(() => this.flush(), this.flushInterval);
// 页面卸载时上报
window.addEventListener('beforeunload', () => this.flush(true));
}
add(data) {
if (this.queue.length >= this.maxQueueSize) {
// 超出队列大小,丢弃最早的数据
this.queue.shift();
}
this.queue.push({
…data,
timestamp: Date.now(),
sessionId: this.getSessionId()
});
// 达到批量大小立即上报
if (this.queue.length >= this.batchSize) {
this.flush();
}
}
async flush(force = false) {
if (this.queue.length === 0) return;
const data = […this.queue];
this.queue = [];
try {
await fetch('/api/report', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
// 确保页面卸载时也能发送
keepalive: force
});
} catch (error) {
// 上报失败,放回队列
this.queue = […data, …this.queue];
}
}
getSessionId() {
let sessionId = localStorage.getItem('sessionId');
if (!sessionId) {
sessionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
localStorage.setItem('sessionId', sessionId);
}
return sessionId;
}
}
// 全局实例
export const reporter = new Reporter();
数据可视化
仪表盘设计
// 简化版仪表盘组件
class Dashboard {
constructor(container) {
this.container = container;
this.data = {};
}
update(data) {
this.data = { …this.data, …data };
this.render();
}
render() {
this.container.innerHTML = `
<div class="dashboard">
<div class="stat-card">
<div class="stat-value">${this.data.errorCount || 0}</div>
<div class="stat-label">今日错误数</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.data.avgLoadTime || 0}ms</div>
<div class="stat-label">平均加载时间</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.data.avgFps || 60}</div>
<div class="stat-label">平均 FPS</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.data.dau || 0}</div>
<div class="stat-label">日活用户</div>
</div>
</div>
`;
}
}
告警机制
告警规则配置
const alertRules = [
{
id: 'error_rate',
name: '错误率告警',
condition: (data) => data.errorRate > 1,
threshold: 1,
level: 'critical',
message: '错误率超过 1%'
},
{
id: 'slow_load',
name: '加载缓慢告警',
condition: (data) => data.avgLoadTime > 3000,
threshold: 3000,
level: 'warning',
message: '平均加载时间超过 3 秒'
},
{
id: 'fps_drop',
name: 'FPS 下降告警',
condition: (data) => data.avgFps < 30,
threshold: 30,
level: 'critical',
message: '平均 FPS 低于 30'
}
];
// 检查告警
function checkAlerts(data) {
const triggeredAlerts = [];
for (const rule of alertRules) {
if (rule.condition(data)) {
triggeredAlerts.push({
…rule,
timestamp: Date.now(),
currentValue: getCurrentValue(data, rule)
});
}
}
return triggeredAlerts;
}
总结
一个完整的前端监控体系应该包含:
技术应当有温度,一个好的监控系统不仅能帮助开发者发现问题,更能帮助产品团队做出数据驱动的决策,最终提升用户体验。
你在前端监控方面有什么经验?欢迎在评论区交流~




