在当今复杂的前端应用中,监控系统已经从“可有可无”变成了“必不可少”。一个完善的前端监控系统不仅能帮助我们快速定位和修复问题,更能提供宝贵的用户行为数据,为产品优化提供数据支持。本文将带你从零开始,构建一个现代化的前端监控系统,涵盖错误监控、性能监控、用户行为追踪等核心功能。
为什么需要前端监控?
在深入技术实现之前,我们先明确前端监控的价值:
- 快速定位问题:当用户遇到错误时,能第一时间获取错误信息和上下文
- 性能优化:识别性能瓶颈,提升用户体验
- 业务分析:了解用户行为路径,优化产品设计
- 主动预警:在问题影响扩大前及时发现并处理
系统架构设计
一个完整的前端监控系统通常包含以下几个模块:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ 数据采集层 │───▶│ 数据传输层 │───▶│ 数据存储层 │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ JS错误监控 │ │ Beacon API │ │ Elasticsearch │
│ 资源加载错误 │ │ Fetch API │ │ ClickHouse │
│ 性能指标 │ │ WebSocket │ │ MySQL/PostgreSQL│
│ 用户行为 │ │ │ │ │
│ API请求监控 │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
核心模块实现
1. 错误监控
错误监控是前端监控的基础,我们需要捕获多种类型的错误:
class ErrorMonitor {
constructor(options = {}) {
this.options = {
maxErrorCount: 20,
samplingRate: 1.0,
...options
};
this.errorQueue = [];
this.init();
}
init() {
// 监听全局错误
window.addEventListener('error', this.handleError.bind(this), true);
// 监听Promise未捕获的异常
window.addEventListener('unhandledrejection', this.handlePromiseError.bind(this));
// 监听console.error
this.interceptConsoleError();
// 监听资源加载错误
window.addEventListener('load', () => {
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
if (resource.initiatorType === 'script' ||
resource.initiatorType === 'link' ||
resource.initiatorType === 'img') {
if (resource.duration === 0) {
this.reportResourceError(resource);
}
}
});
});
}
handleError(event) {
const errorInfo = {
type: 'JS_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,
// 收集更多上下文信息
viewport: `${window.innerWidth}x${window.innerHeight}`,
referrer: document.referrer,
// 收集用户行为快照(最后5个操作)
userActions: this.getUserActionsSnapshot()
};
this.addToQueue(errorInfo);
}
handlePromiseError(event) {
const errorInfo = {
type: 'PROMISE_ERROR',
message: event.reason?.message || 'Unhandled Promise Rejection',
stack: event.reason?.stack,
timestamp: Date.now(),
url: window.location.href
};
this.addToQueue(errorInfo);
}
interceptConsoleError() {
const originalConsoleError = console.error;
console.error = (...args) => {
const errorInfo = {
type: 'CONSOLE_ERROR',
messages: args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
),
timestamp: Date.now(),
url: window.location.href
};
this.addToQueue(errorInfo);
originalConsoleError.apply(console, args);
};
}
reportResourceError(resource) {
const errorInfo = {
type: 'RESOURCE_ERROR',
name: resource.name,
initiatorType: resource.initiatorType,
duration: resource.duration,
timestamp: Date.now(),
url: window.location.href
};
this.addToQueue(errorInfo);
}
addToQueue(errorInfo) {
// 采样控制
if (Math.random() > this.options.samplingRate) {
return;
}
this.errorQueue.push(errorInfo);
// 达到阈值时上报
if (this.errorQueue.length >= this.options.maxErrorCount) {
this.reportErrors();
}
}
reportErrors() {
if (this.errorQueue.length === 0) return;
const errorsToReport = [...this.errorQueue];
this.errorQueue = [];
// 使用navigator.sendBeacon确保页面卸载时也能上报
const blob = new Blob([JSON.stringify({
errors: errorsToReport,
appId: this.options.appId,
sessionId: this.getSessionId(),
userId: this.getUserId()
})], { type: 'application/json' });
navigator.sendBeacon(`${this.options.endpoint}/errors`, blob);
}
getUserActionsSnapshot() {
// 实现用户行为追踪,记录最后N个用户操作
return window.__USER_ACTIONS__?.slice(-5) || [];
}
getSessionId() {
let sessionId = sessionStorage.getItem('monitor_session_id');
if (!sessionId) {
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
sessionStorage.setItem('monitor_session_id', sessionId);
}
return sessionId;
}
getUserId() {
// 根据业务实际情况获取用户ID
return localStorage.getItem('user_id') || 'anonymous';
}
}
2. 性能监控
现代浏览器提供了Performance API,让我们能够获取丰富的性能数据:
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// 等待页面完全加载后收集性能指标
if (document.readyState === 'complete') {
this.collectMetrics();
} else {
window.addEventListener('load', () => {
// 延迟收集,确保所有资源加载完成
setTimeout(() => this.collectMetrics(), 0);
});
}
// 监听长任务(Long Tasks)
if ('PerformanceObserver' in window) {
this.observeLongTasks();
}
// 监控FPS(帧率)
this.monitorFPS();
}
collectMetrics() {
const timing = performance.timing;
this.metrics = {
// 关键性能指标
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ssl: timing.connectEnd - timing.secureConnectionStart,
ttfb: timing.responseStart - timing.requestStart,
download: timing.responseEnd - timing.responseStart,
domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
onLoad: timing.loadEventEnd - timing.navigationStart,
// 核心Web Vitals
fcp: this.getFirstContentfulPaint(),
lcp: this.getLargestContentfulPaint(),
fid: this.getFirstInputDelay(),
cls: this.getCumulativeLayoutShift()
};
// 上报性能数据
this.reportPerformance();
}
getFirstContentfulPaint() {
const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0];
return fcpEntry ? fcpEntry.startTime : 0;
}
getLargestContentfulPaint() {
// 使用PerformanceObserver动态获取LCP
return new Promise((resolve) => {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.startTime);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
});
}
getFirstInputDelay() {
// 监听首次输入延迟
return new Promise((resolve) => {