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

5 阅读3分钟

在当今复杂的前端应用中,监控系统已经从"可有可无"变成了"必不可少"的基础设施。然而,很多团队仍然停留在简单的 window.onerrorconsole.log 阶段,错失了通过全面监控提升应用质量和用户体验的机会。本文将带你从零构建一个现代化的前端监控系统,涵盖错误追踪、性能监控、用户行为分析等核心功能。

为什么需要完整的监控系统?

在深入技术实现之前,我们先明确一个现代监控系统应该解决的核心问题:

  1. 错误追踪:不仅仅是捕获异常,还要能定位问题根源
  2. 性能监控:了解真实用户的体验,而不只是实验室数据
  3. 用户行为分析:将技术指标与业务指标关联
  4. 实时告警:在用户投诉前发现问题
  5. 数据可视化:让数据说话,指导优化方向

架构设计

一个完整的前端监控系统通常包含以下组件:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   客户端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