每天一个高级前端知识 - Day 26

9 阅读4分钟

每天一个高级前端知识 - Day 26

今日主题:前端监控与错误追踪 - 构建企业级监控系统

核心概念:你不能优化你无法测量的东西

前端监控不仅是“捕捉错误”,更是理解用户行为衡量性能保障体验的系统工程。

📊 前端监控体系全景图

┌─────────────────────────────────────────────────────────┐
│                      数据采集层                          │
├──────────────┬──────────────┬──────────────┬───────────┤
│  错误监控    │  性能监控    │  行为监控    │  资源监控  │
│  JS ErrorCore WebPV/UV       │  静态资源  │
│  PromiseVitals      │  点击/滚动   │  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,要求:

  1. 错误监控(JS错误、Promise错误、资源错误)
  2. 性能监控(Core Web Vitals、长任务)
  3. 用户行为追踪(点击、路由、请求)
  4. 采样和去重机制
  5. 数据上报(批量、重试、sendBeacon)
  6. 告警规则配置
// 使用示例
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' });

明日预告:前端性能优化的极致 - 从浏览器原理到实战技巧

💡 监控箴言:"监控不是目的,可操作的洞察才是。每个告警都应该指向一个可执行的优化动作。"