从零构建一个现代化的前端监控系统:不只是错误捕获

3 阅读1分钟

在当今复杂的前端应用中,监控系统已经从“可有可无”变成了“必不可少”。一个完善的前端监控系统不仅能帮助我们快速定位和修复问题,更能提供宝贵的用户行为数据,为产品优化提供数据支持。本文将带你从零开始,构建一个现代化的前端监控系统,涵盖错误监控、性能监控、用户行为追踪等核心功能。

为什么需要前端监控?

在深入技术实现之前,我们先明确前端监控的价值:

  1. 快速定位问题:当用户遇到错误时,能第一时间获取错误信息和上下文
  2. 性能优化:识别性能瓶颈,提升用户体验
  3. 业务分析:了解用户行为路径,优化产品设计
  4. 主动预警:在问题影响扩大前及时发现并处理

系统架构设计

一个完整的前端监控系统通常包含以下几个模块:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│                 │    │                 │    │                 │
│   数据采集层     │───▶│   数据传输层     │───▶│   数据存储层     │
│                 │    │                 │    │                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                        │                        │
         ▼                        ▼                        ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│                 │    │                 │    │                 │
│  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) => {