在当今复杂的前端应用中,监控系统已经从"可有可无"变成了"必不可少"的基础设施。然而,很多团队仍然停留在简单的 window.onerror 或 console.log 阶段,错失了通过全面监控提升应用质量和用户体验的机会。本文将带你从零构建一个现代化的前端监控系统,涵盖错误追踪、性能监控、用户行为分析等核心功能。
为什么需要完整的监控系统?
在深入技术实现之前,我们先明确一个现代监控系统应该解决的核心问题:
- 错误追踪:不仅仅是捕获异常,还要能定位问题根源
- 性能监控:了解真实用户的体验,而不只是实验室数据
- 用户行为分析:将技术指标与业务指标关联
- 实时告警:在用户投诉前发现问题
- 数据可视化:让数据说话,指导优化方向
架构设计
一个完整的前端监控系统通常包含以下组件:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 客户端SDK │───▶│ 收集服务 │───▶│ 存储引擎 │
│ (数据采集) │ │ (数据清洗) │ │ (数据持久化) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ 分析引擎 │───▶│ 可视化平台 │
│ (数据处理) │ │ (数据展示) │
└─────────────────┘ └─────────────────┘
核心模块实现
1. 错误监控模块
错误监控不仅仅是捕获异常,还需要收集足够的上下文信息。
class ErrorMonitor {
constructor(options = {}) {
this.options = {
maxErrors: 50,
samplingRate: 1.0,
...options
};
this.errors = [];
this.init();
}
init() {
// 捕获未处理的Promise错误
window.addEventListener('unhandledrejection', (event) => {
this.captureError({
type: 'unhandledrejection',
message: event.reason?.message || 'Unhandled Promise Rejection',
stack: event.reason?.stack,
time: Date.now(),
reason: event.reason
});
});
// 捕获全局JavaScript错误
window.addEventListener('error', (event) => {
this.captureError({
type: 'error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
time: Date.now()
});
});
// 重写console.error
const originalConsoleError = console.error;
console.error = (...args) => {
this.captureError({
type: 'console_error',
message: args.map(arg => String(arg)).join(' '),
time: Date.now(),
stack: new Error().stack
});
originalConsoleError.apply(console, args);
};
}
captureError(errorInfo) {
// 采样控制
if (Math.random() > this.options.samplingRate) return;
// 添加上下文信息
const enrichedError = {
...errorInfo,
url: window.location.href,
userAgent: navigator.userAgent,
referrer: document.referrer,
viewport: `${window.innerWidth}x${window.innerHeight}`,
memory: performance.memory ? {
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
totalJSHeapSize: performance.memory.totalJSHeapSize,
usedJSHeapSize: performance.memory.usedJSHeapSize
} : null,
network: navigator.connection ? {
effectiveType: navigator.connection.effectiveType,
downlink: navigator.connection.downlink,
rtt: navigator.connection.rtt
} : null
};
this.errors.push(enrichedError);
// 限制错误数量
if (this.errors.length > this.options.maxErrors) {
this.errors.shift();
}
// 发送到服务器
this.sendToServer(enrichedError);
}
async sendToServer(error) {
try {
const response = await fetch(this.options.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'error',
data: error,
timestamp: Date.now(),
sessionId: this.getSessionId()
})
});
if (!response.ok) {
console.warn('Failed to send error to server:', response.status);
}
} catch (err) {
console.warn('Error sending to server:', err);
}
}
getSessionId() {
let sessionId = sessionStorage.getItem('monitoring_session_id');
if (!sessionId) {
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
sessionStorage.setItem('monitoring_session_id', sessionId);
}
return sessionId;
}
}
2. 性能监控模块
性能监控需要关注关键性能指标,而不仅仅是加载时间。
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// 使用Performance API收集核心Web指标
if ('PerformanceObserver' in window) {
this.observeLCP(); // 最大内容绘制
this.observeFID(); // 首次输入延迟
this.observeCLS(); // 累积布局偏移
}
// 传统性能指标
this.collectNavigationTiming();
this.collectResourceTiming();
}
observeLCP() {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.LCP = {
value: lastEntry.startTime,
element: lastEntry.element?.tagName,
url: lastEntry.url,
timestamp: Date.now()
};
this.sendMetric('LCP', this.metrics.LCP);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
}
observeFID() {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
entries.forEach(entry => {
this.metrics.FID = {
value: entry.processingStart - entry.startTime,
name: entry.name,
timestamp: Date.now()
};
this.sendMetric('FID', this.metrics.FID);
});
});
observer.observe({ type: 'first-input', buffered: true });
}
collectNavigationTiming() {
if (performance.getEntriesByType) {
const navigation = performance.getEntriesByType('navigation')[0];
if (navigation) {
this.metrics.navigation = {
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
ssl: navigation.connectEnd - navigation.secureConnectionStart,
ttfb: navigation.responseStart - navigation.requestStart,
download: navigation.responseEnd - navigation.responseStart,
domReady: navigation.domContentLoadedEventEnd - navigation.startTime,
load: navigation.loadEventEnd - navigation.startTime
};
}
}
}
collectResourceTiming() {
if (performance.getEntriesByType) {
const resources = performance.getEntriesByType('resource');
this.metrics.resources = resources.map(resource => ({
name: resource.name,
duration: resource.duration,
type: resource.initiatorType,
size: resource.transferSize,
startTime: resource.startTime
}));
}
}
sendMetric(name, data) {
// 发送性能指标到服务器
fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'performance',
name,
data,
timestamp: Date.now(),
url: window.location.href
})
});
}
}
3. 用户行为追踪
了解用户在应用中的行为模式对于问题诊断至关重要。
class UserBehaviorTracker {
constructor() {
this.events = [];
this.init();
}
init() {
// 追踪点击事件
document.addEventListener('click', this.trackClick.bind(this), true);
// 追踪路由变化
this.trackRouting();
// 追踪AJAX请求
this.trackAjax();
// 追踪控制台输出
this.trackConsole();
}
trackClick(event) {
const target = event.target;
const path = this.getElementPath(target);
const clickEvent