每天一个高级前端知识 - Day 26
今日主题:前端监控与错误追踪 - 构建企业级监控系统
核心概念:你不能优化你无法测量的东西
前端监控不仅是“捕捉错误”,更是理解用户行为、衡量性能和保障体验的系统工程。
📊 前端监控体系全景图
┌─────────────────────────────────────────────────────────┐
│ 数据采集层 │
├──────────────┬──────────────┬──────────────┬───────────┤
│ 错误监控 │ 性能监控 │ 行为监控 │ 资源监控 │
│ JS Error │ Core Web │ PV/UV │ 静态资源 │
│ Promise │ Vitals │ 点击/滚动 │ API 请求 │
│ Resource │ 长任务 │ 路由跳转 │ 图片加载 │
└──────────────┴──────────────┴──────────────┴───────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 数据处理层 │
│ 数据清洗 → 聚合计算 → 告警判定 → 存储索引 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 数据展示层 │
│ 实时大盘 │ 错误追踪 │ 性能分析 │ 用户行为 │ 告警配置 │
└─────────────────────────────────────────────────────────┘
🔍 完整错误监控系统
// ============ 错误监控核心类 ============
// lib/monitoring/ErrorTracker.ts
interface ErrorReport {
id: string;
message: string;
stack?: string;
filename?: string;
lineno?: number;
colno?: number;
type: 'js' | 'promise' | 'resource' | 'http' | 'custom';
severity: 'fatal' | 'error' | 'warning' | 'info';
context: {
url: string;
userAgent: string;
timestamp: number;
userId?: string;
sessionId: string;
breadcrumbs: Breadcrumb[];
extra?: Record<string, any>;
};
tags?: Record<string, string>;
}
interface Breadcrumb {
type: 'click' | 'navigation' | 'request' | 'console' | 'custom';
message: string;
data?: any;
timestamp: number;
}
class ErrorTracker {
private static instance: ErrorTracker;
private config: {
dsn: string;
appId: string;
env: 'development' | 'staging' | 'production';
enableBreadcrumbs: boolean;
maxBreadcrumbs: number;
sampleRate: number;
beforeSend?: (report: ErrorReport) => ErrorReport | null;
};
private breadcrumbs: Breadcrumb[] = [];
private sessionId: string;
private reportQueue: ErrorReport[] = [];
private flushTimer: number | null = null;
private constructor(config: any) {
this.config = config;
this.sessionId = this.generateSessionId();
this.init();
}
static getInstance(config?: any): ErrorTracker {
if (!ErrorTracker.instance) {
ErrorTracker.instance = new ErrorTracker(config);
}
return ErrorTracker.instance;
}
private init() {
// 采样控制
if (Math.random() > (this.config.sampleRate || 1)) {
return;
}
this.setupErrorHandlers();
this.setupPromiseRejectionHandler();
this.setupConsoleCapture();
this.setupRouteChangeCapture();
// 定时上报
this.flushTimer = window.setInterval(() => this.flush(), 5000);
// 页面关闭时上报
window.addEventListener('beforeunload', () => this.flush(true));
window.addEventListener('pagehide', () => this.flush(true));
}
private setupErrorHandlers() {
// JS 运行时错误
window.addEventListener('error', (event) => {
const error = event.error;
this.captureException(error, {
type: 'js',
severity: 'error',
context: {
filename: event.filename,
lineno: event.lineno,
colno: event.colno
}
});
});
// 资源加载错误
window.addEventListener('error', (event) => {
const target = event.target as HTMLElement;
if (target && (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
this.captureMessage(`资源加载失败: ${(target as any).src || (target as any).href}`, {
type: 'resource',
severity: 'warning',
tags: { resourceType: target.tagName }
});
}
}, true);
}
private setupPromiseRejectionHandler() {
window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason;
this.captureException(reason, {
type: 'promise',
severity: 'error',
context: { reason: String(reason) }
});
});
}
private setupConsoleCapture() {
if (!this.config.enableBreadcrumbs) return;
const originalConsoleError = console.error;
console.error = (...args) => {
this.addBreadcrumb({
type: 'console',
message: args.join(' '),
data: { level: 'error' }
});
originalConsoleError.apply(console, args);
};
}
private setupRouteChangeCapture() {
// 监听 History API 路由变化
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = (...args) => {
this.addBreadcrumb({
type: 'navigation',
message: `路由跳转到: ${args[2]}`,
data: { method: 'pushState' }
});
return originalPushState.apply(history, args);
};
history.replaceState = (...args) => {
this.addBreadcrumb({
type: 'navigation',
message: `路由替换为: ${args[2]}`,
data: { method: 'replaceState' }
});
return originalReplaceState.apply(history, args);
};
// 监听 popstate (浏览器前进后退)
window.addEventListener('popstate', () => {
this.addBreadcrumb({
type: 'navigation',
message: `路由跳转到: ${window.location.pathname}`,
data: { method: 'popstate' }
});
});
}
// 捕获异常
captureException(error: Error, options?: Partial<ErrorReport>) {
const report: ErrorReport = {
id: this.generateId(),
message: error.message,
stack: error.stack,
type: options?.type || 'js',
severity: options?.severity || 'error',
context: {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
sessionId: this.sessionId,
userId: this.getUserId(),
breadcrumbs: [...this.breadcrumbs],
extra: options?.context
},
tags: options?.tags
};
// 前置处理钩子
if (this.config.beforeSend) {
const processed = this.config.beforeSend(report);
if (!processed) return;
}
this.addToQueue(report);
// 严重错误立即上报
if (report.severity === 'fatal') {
this.flush(true);
}
}
// 捕获消息
captureMessage(message: string, options?: Partial<ErrorReport>) {
const report: ErrorReport = {
id: this.generateId(),
message,
type: options?.type || 'custom',
severity: options?.severity || 'info',
context: {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
sessionId: this.sessionId,
userId: this.getUserId(),
breadcrumbs: [...this.breadcrumbs],
extra: options?.context
},
tags: options?.tags
};
this.addToQueue(report);
}
// 添加面包屑(用户行为轨迹)
addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>) {
if (!this.config.enableBreadcrumbs) return;
this.breadcrumbs.push({
...breadcrumb,
timestamp: Date.now()
});
// 保留最近的 N 条
if (this.breadcrumbs.length > this.config.maxBreadcrumbs) {
this.breadcrumbs.shift();
}
}
// 手动添加用户行为追踪
trackEvent(eventName: string, properties?: Record<string, any>) {
this.addBreadcrumb({
type: 'custom',
message: eventName,
data: properties
});
// 可选:上报到分析平台
this.captureMessage(`用户行为: ${eventName}`, {
type: 'custom',
severity: 'info',
tags: properties
});
}
private addToQueue(report: ErrorReport) {
this.reportQueue.push(report);
// 队列积压超过10条立即上报
if (this.reportQueue.length >= 10) {
this.flush();
}
}
private async flush(immediate = false) {
if (this.reportQueue.length === 0) return;
const reports = [...this.reportQueue];
this.reportQueue = [];
const payload = {
appId: this.config.appId,
env: this.config.env,
reports
};
if (immediate && navigator.sendBeacon) {
// 页面关闭时使用 sendBeacon 确保发送
navigator.sendBeacon(`${this.config.dsn}/api/errors/batch`, JSON.stringify(payload));
} else {
// 正常上报
try {
await fetch(`${this.config.dsn}/api/errors/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true
});
} catch (error) {
// 上报失败,重新入队
this.reportQueue.unshift(...reports);
}
}
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
}
private generateSessionId(): string {
let sessionId = sessionStorage.getItem('__monitor_session_id');
if (!sessionId) {
sessionId = this.generateId();
sessionStorage.setItem('__monitor_session_id', sessionId);
}
return sessionId;
}
private getUserId(): string | undefined {
// 从 localStorage 或全局状态中获取用户 ID
return localStorage.getItem('userId') || undefined;
}
// 销毁监控实例
destroy() {
if (this.flushTimer) {
clearInterval(this.flushTimer);
}
this.flush(true);
}
}
// 初始化
const errorTracker = ErrorTracker.getInstance({
dsn: 'https://monitor.example.com',
appId: 'my-app',
env: import.meta.env.MODE as any,
enableBreadcrumbs: true,
maxBreadcrumbs: 20,
sampleRate: 0.1, // 10% 采样
beforeSend: (report) => {
// 过滤敏感信息
if (report.message.includes('password')) {
return null;
}
return report;
}
});
// 挂载到全局方便调试
if (typeof window !== 'undefined') {
(window as any).__errorTracker = errorTracker;
}
⚡ 性能监控实现
// ============ 性能监控模块 ============
// lib/monitoring/PerformanceTracker.ts
interface PerformanceMetric {
name: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
timestamp: number;
context?: Record<string, any>;
}
class PerformanceTracker {
private metrics: PerformanceMetric[] = [];
private observers: PerformanceObserver[] = [];
private config: {
dsn: string;
appId: string;
reportUrl: string;
};
constructor(config: any) {
this.config = config;
this.init();
}
private init() {
this.observeLCP();
this.observeINP();
this.observeCLS();
this.observeFCP();
this.observeTTFB();
this.observeLongTasks();
this.observeResourceTiming();
// 页面关闭时上报
window.addEventListener('beforeunload', () => this.flush(true));
window.addEventListener('pagehide', () => this.flush(true));
}
// LCP (Largest Contentful Paint)
private observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1] as PerformanceEntry & {
renderTime?: number;
loadTime?: number;
element?: Element;
url?: string;
};
const value = lastEntry.renderTime || lastEntry.loadTime;
this.addMetric({
name: 'LCP',
value,
rating: this.getRating('LCP', value),
context: {
element: lastEntry.element?.tagName,
url: lastEntry.url
}
});
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
this.observers.push(observer);
}
// INP (Interaction to Next Paint)
private observeINP() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as PerformanceEventTiming[]) {
if (entry.interactionId) {
this.addMetric({
name: 'INP',
value: entry.processingEnd - entry.startTime,
rating: this.getRating('INP', entry.processingEnd - entry.startTime),
context: {
interactionType: entry.name,
target: (entry.target as HTMLElement)?.id || (entry.target as HTMLElement)?.tagName
}
});
}
}
});
observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
this.observers.push(observer);
}
// CLS (Cumulative Layout Shift)
private observeCLS() {
let clsValue = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as PerformanceLayoutShift[]) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
this.addMetric({
name: 'CLS',
value: clsValue,
rating: this.getRating('CLS', clsValue),
context: {
sources: entry.sources?.map(s => ({
node: s.node?.tagName,
previousRect: s.previousRect,
currentRect: s.currentRect
}))
}
});
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
this.observers.push(observer);
}
// FCP (First Contentful Paint)
private observeFCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const fcp = entries.find(entry => entry.name === 'first-contentful-paint');
if (fcp) {
this.addMetric({
name: 'FCP',
value: fcp.startTime,
rating: this.getRating('FCP', fcp.startTime)
});
}
});
observer.observe({ type: 'paint', buffered: true });
this.observers.push(observer);
}
// TTFB (Time To First Byte)
private observeTTFB() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const navigation = entries.find(e => e.entryType === 'navigation') as PerformanceNavigationTiming;
if (navigation) {
this.addMetric({
name: 'TTFB',
value: navigation.responseStart - navigation.requestStart,
rating: this.getRating('TTFB', navigation.responseStart - navigation.requestStart)
});
}
});
observer.observe({ type: 'navigation', buffered: true });
this.observers.push(observer);
}
// 长任务监控
private observeLongTasks() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as PerformanceEntry[]) {
if (entry.duration > 50) {
this.addMetric({
name: 'LongTask',
value: entry.duration,
rating: entry.duration > 200 ? 'poor' : entry.duration > 100 ? 'needs-improvement' : 'good',
context: {
startTime: entry.startTime,
attribution: (entry as any).attribution?.[0]?.containerType,
scriptUrl: (entry as any).attribution?.[0]?.scriptUrl
}
});
// 超过200ms的长任务单独告警
if (entry.duration > 200) {
errorTracker.captureMessage(`检测到长任务: ${entry.duration}ms`, {
type: 'performance',
severity: 'warning',
context: { longTask: entry }
});
}
}
}
});
observer.observe({ type: 'longtask', buffered: true });
this.observers.push(observer);
}
// 资源加载监控
private observeResourceTiming() {
const observer = new PerformanceObserver((list) => {
const slowResources = list.getEntries().filter(entry => entry.duration > 1000);
for (const resource of slowResources) {
this.addMetric({
name: 'SlowResource',
value: resource.duration,
rating: 'poor',
context: {
url: resource.name,
size: (resource as any).transferSize,
type: resource.initiatorType
}
});
errorTracker.captureMessage(`资源加载缓慢: ${resource.name}`, {
type: 'performance',
severity: 'warning',
context: { duration: resource.duration, size: (resource as any).transferSize }
});
}
});
observer.observe({ type: 'resource', buffered: true });
this.observers.push(observer);
}
private addMetric(metric: PerformanceMetric) {
this.metrics.push({
...metric,
timestamp: Date.now()
});
// 保持最近100条指标
if (this.metrics.length > 100) {
this.metrics.shift();
}
}
private getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
const thresholds: Record<string, { good: number; poor: number }> = {
LCP: { good: 2500, poor: 4000 },
INP: { good: 200, poor: 500 },
CLS: { good: 0.1, poor: 0.25 },
FCP: { good: 1800, poor: 3000 },
TTFB: { good: 800, poor: 1800 }
};
const threshold = thresholds[metric];
if (!threshold) return 'good';
if (value <= threshold.good) return 'good';
if (value <= threshold.poor) return 'needs-improvement';
return 'poor';
}
// 手动测量自定义性能点
measure(name: string, startMark: string, endMark: string) {
performance.mark(startMark);
// 执行操作...
performance.mark(endMark);
performance.measure(name, startMark, endMark);
const measure = performance.getEntriesByName(name)[0];
this.addMetric({
name: `Custom_${name}`,
value: measure.duration,
rating: 'good'
});
}
private async flush(immediate = false) {
if (this.metrics.length === 0) return;
const metrics = [...this.metrics];
this.metrics = [];
const payload = {
appId: this.config.appId,
type: 'performance',
metrics
};
if (immediate && navigator.sendBeacon) {
navigator.sendBeacon(this.config.reportUrl, JSON.stringify(payload));
} else {
try {
await fetch(this.config.reportUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true
});
} catch (error) {
this.metrics.unshift(...metrics);
}
}
}
destroy() {
this.observers.forEach(observer => observer.disconnect());
this.flush(true);
}
// 获取当前性能评分
getPerformanceScore(): number {
const latest: Record<string, PerformanceMetric> = {};
for (const metric of this.metrics.slice().reverse()) {
if (!latest[metric.name]) {
latest[metric.name] = metric;
}
}
const weights = { LCP: 0.25, INP: 0.25, CLS: 0.15, FCP: 0.2, TTFB: 0.15 };
let score = 0;
for (const [name, weight] of Object.entries(weights)) {
const metric = latest[name];
if (metric) {
const ratingScore = { good: 1, 'needs-improvement': 0.5, poor: 0 };
score += ratingScore[metric.rating] * weight;
}
}
return Math.round(score * 100);
}
}
📈 告警规则配置
// ============ 告警规则引擎 ============
// lib/monitoring/AlertEngine.ts
interface AlertRule {
name: string;
condition: (data: any) => boolean;
action: (data: any) => void;
throttle?: number; // 节流时间(毫秒)
lastTriggered?: number;
}
class AlertEngine {
private rules: AlertRule[] = [];
constructor() {
this.setupDefaultRules();
}
private setupDefaultRules() {
// 错误率告警
this.addRule({
name: 'HighErrorRate',
condition: (data) => {
const errorRate = data.errors / (data.requests || 1);
return errorRate > 0.05; // 错误率超过5%
},
action: (data) => {
console.error(`[告警] 错误率过高: ${(data.errorRate * 100).toFixed(2)}%`);
this.sendAlert({
level: 'critical',
title: '错误率告警',
message: `错误率已达到 ${(data.errorRate * 100).toFixed(2)}%`,
data
});
},
throttle: 60000 // 1分钟内不重复告警
});
// 性能告警
this.addRule({
name: 'PoorPerformance',
condition: (data) => {
return data.lcp > 4000 || data.cls > 0.25;
},
action: (data) => {
console.warn(`[告警] 性能指标超标: LCP=${data.lcp}ms, CLS=${data.cls}`);
this.sendAlert({
level: 'warning',
title: '性能告警',
message: `页面性能不达标 (LCP: ${data.lcp}ms, CLS: ${data.cls})`,
data
});
},
throttle: 300000 // 5分钟内不重复告警
});
// 长任务告警
this.addRule({
name: 'LongTask',
condition: (data) => {
return data.longTasks > 3; // 超过3个长任务
},
action: (data) => {
this.sendAlert({
level: 'warning',
title: '长任务告警',
message: `检测到 ${data.longTasks} 个长任务,最长 ${data.maxLongTaskDuration}ms`,
data
});
}
});
}
addRule(rule: AlertRule) {
this.rules.push(rule);
}
evaluate(data: any) {
for (const rule of this.rules) {
// 节流检查
if (rule.throttle && rule.lastTriggered) {
if (Date.now() - rule.lastTriggered < rule.throttle) {
continue;
}
}
if (rule.condition(data)) {
rule.action(data);
rule.lastTriggered = Date.now();
}
}
}
private sendAlert(alert: {
level: 'critical' | 'warning' | 'info';
title: string;
message: string;
data?: any;
}) {
// 发送到通知渠道
if (alert.level === 'critical') {
// 发送到钉钉/企业微信/Slack
this.sendToIM(alert);
// 发送到短信/电话(严重问题)
if (alert.level === 'critical') {
this.sendToSMS(alert);
}
}
// 记录告警日志
errorTracker.captureMessage(`[${alert.level}] ${alert.title}: ${alert.message}`, {
type: 'alert',
severity: alert.level === 'critical' ? 'error' : 'warning',
context: alert.data
});
}
private sendToIM(alert: any) {
// 实现发送到企业微信/钉钉/Slack
console.log('发送IM通知:', alert);
}
private sendToSMS(alert: any) {
// 实现发送短信/电话告警
console.log('发送短信告警:', alert);
}
}
🎯 今日挑战
构建一个完整的前端监控SDK,要求:
- 错误监控(JS错误、Promise错误、资源错误)
- 性能监控(Core Web Vitals、长任务)
- 用户行为追踪(点击、路由、请求)
- 采样和去重机制
- 数据上报(批量、重试、sendBeacon)
- 告警规则配置
// 使用示例
const monitor = new FrontendMonitor({
dsn: 'https://monitor.example.com',
appId: 'my-app',
enablePerformance: true,
enableErrors: true,
enableBreadcrumbs: true,
sampleRate: 0.5,
beforeSend: (event) => {
// 过滤敏感信息
return event;
}
});
monitor.start();
// 手动上报自定义事件
monitor.trackEvent('add_to_cart', { productId: '123', price: 99 });
// 设置用户信息
monitor.setUser({ id: '123', name: 'John' });
明日预告:前端性能优化的极致 - 从浏览器原理到实战技巧
💡 监控箴言:"监控不是目的,可操作的洞察才是。每个告警都应该指向一个可执行的优化动作。"