前端错误监控与排查体系实战指南

🚨 前端错误监控与排查体系实战指南

作为架构师,我会带你从零构建一个实用、高效的错误监控体系。这是每个项目都应该有的基础设施


📦 第一步:基础错误监控SDK

1. 创建错误监控核心(可直接复制使用)

// src/utils/errorMonitor.js
class ErrorMonitor {
  constructor(options = {}) {
    this.config = {
      // 必填:错误上报地址
      endpoint: options.endpoint || 'https://your-api.com/error-collect',
      
      // 采样率:1.0表示100%上报,0.1表示10%
      sampleRate: options.sampleRate || 1.0,
      
      // 开发环境显示友好错误界面
      showUI: options.showUI ?? process.env.NODE_ENV !== 'production',
      
      // 是否监控未处理的Promise错误
      monitorUnhandledRejection: true,
      
      // 是否监控控制台错误
      monitorConsoleError: true,
      
      // 错误聚合:相同错误10分钟内只报一次
      deduplicate: true,
      dedupeWindow: 10 * 60 * 1000, // 10分钟
    };
    
    this.errorQueue = [];
    this.sentErrors = new Map(); // 用于去重
    this.init();
  }

  // 🚀 初始化所有错误监控
  init() {
    // 1. 全局错误捕获
    window.addEventListener('error', this.handleWindowError.bind(this));
    
    // 2. Promise未捕获错误
    window.addEventListener('unhandledrejection', this.handlePromiseError.bind(this));
    
    // 3. 控制台错误重写
    if (this.config.monitorConsoleError) {
      this.wrapConsole();
    }
    
    // 4. 资源加载错误
    window.addEventListener('error', this.handleResourceError.bind(this), true);
    
    // 5. 网络请求错误
    this.monitorFetch();
    this.monitorXMLHttpRequest();
    
    // 6. Vue/React框架错误(后面实现)
    
    // 7. 用户行为追踪(辅助排查)
    this.trackUserActions();
    
    // 定期上报
    setInterval(() => this.flush(), 10000);
    window.addEventListener('beforeunload', () => this.flush(true));
    
    console.log('✅ 错误监控已启动');
  }

  // 🪟 处理窗口错误(JS运行时错误)
  handleWindowError(event) {
    const { message, filename, lineno, colno, error } = event;
    
    // 忽略跨域脚本错误(无法获取详情)
    if (message === 'Script error.' && !filename) {
      return;
    }
    
    const errorInfo = {
      type: 'js_error',
      message,
      filename,
      lineno,
      colno,
      stack: error?.stack,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent,
    };
    
    // 增强错误信息
    this.enhanceError(errorInfo);
    
    // 阻止默认错误显示
    event.preventDefault();
    
    // 上报
    this.reportError(errorInfo);
    
    // 开发环境显示友好界面
    if (this.config.showUI) {
      this.showErrorUI(errorInfo);
    }
  }

  // 🤝 处理Promise错误
  handlePromiseError(event) {
    const error = event.reason;
    
    const errorInfo = {
      type: 'promise_error',
      message: error?.message || 'Unhandled Promise rejection',
      stack: error?.stack,
      timestamp: Date.now(),
      url: window.location.href,
    };
    
    this.reportError(errorInfo);
  }

  // 📦 处理资源加载错误
  handleResourceError(event) {
    const target = event.target;
    
    // 只处理资源加载错误
    if (target && (target.src || target.href)) {
      const errorInfo = {
        type: 'resource_error',
        tagName: target.tagName,
        resourceUrl: target.src || target.href,
        timestamp: Date.now(),
        url: window.location.href,
      };
      
      this.reportError(errorInfo);
    }
  }

  // 🌐 监控fetch请求
  monitorFetch() {
    const originalFetch = window.fetch;
    
    window.fetch = async (...args) => {
      const startTime = Date.now();
      const [url, options = {}] = args;
      
      // 生成请求ID用于追踪
      const requestId = Math.random().toString(36).substr(2, 9);
      
      try {
        const response = await originalFetch(...args);
        const endTime = Date.now();
        
        // 非200状态码视为错误
        if (!response.ok) {
          const errorInfo = {
            type: 'http_error',
            requestId,
            url: url.toString(),
            method: options.method || 'GET',
            status: response.status,
            statusText: response.statusText,
            duration: endTime - startTime,
            timestamp: Date.now(),
          };
          
          this.reportError(errorInfo);
        }
        
        return response;
      } catch (error) {
        const endTime = Date.now();
        const errorInfo = {
          type: 'network_error',
          requestId,
          url: url.toString(),
          method: options.method || 'GET',
          error: error.message,
          duration: endTime - startTime,
          timestamp: Date.now(),
        };
        
        this.reportError(errorInfo);
        throw error;
      }
    };
  }

  // 📡 监控XMLHttpRequest
  monitorXMLHttpRequest() {
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;
    
    XMLHttpRequest.prototype.open = function(method, url) {
      this._requestInfo = {
        method,
        url,
        startTime: Date.now(),
        requestId: Math.random().toString(36).substr(2, 9),
      };
      
      return originalOpen.apply(this, arguments);
    };
    
    XMLHttpRequest.prototype.send = function(body) {
      this.addEventListener('loadend', () => {
        const { method, url, startTime, requestId } = this._requestInfo;
        const endTime = Date.now();
        
        if (this.status >= 400) {
          const errorInfo = {
            type: 'xhr_error',
            requestId,
            url,
            method,
            status: this.status,
            statusText: this.statusText,
            duration: endTime - startTime,
            timestamp: Date.now(),
          };
          
          ErrorMonitor.getInstance().reportError(errorInfo);
        }
      });
      
      return originalSend.apply(this, arguments);
    };
  }

  // 🎭 包装console.error
  wrapConsole() {
    const originalError = console.error;
    
    console.error = (...args) => {
      // 调用原始方法
      originalError.apply(console, 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.reportError(errorInfo);
    };
  }

  // 📝 追踪用户行为(辅助排查)
  trackUserActions() {
    let actions = [];
    let lastActionTime = Date.now();
    
    // 记录点击
    document.addEventListener('click', (e) => {
      const target = e.target;
      const path = this.getDomPath(target);
      
      actions.push({
        type: 'click',
        path,
        text: target.textContent?.slice(0, 50),
        timestamp: Date.now() - lastActionTime,
      });
      
      // 只保留最近20个动作
      if (actions.length > 20) {
        actions.shift();
      }
      
      lastActionTime = Date.now();
    });
    
    // 记录输入
    document.addEventListener('input', (e) => {
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
        actions.push({
          type: 'input',
          name: e.target.name || e.target.id,
          value: e.target.value?.slice(0, 100),
          timestamp: Date.now() - lastActionTime,
        });
      }
    });
    
    // 保存到全局,供错误上报时使用
    window.__USER_ACTIONS = actions;
  }

  // 🔧 增强错误信息
  enhanceError(errorInfo) {
    // 添加用户行为轨迹
    if (window.__USER_ACTIONS) {
      errorInfo.userActions = window.__USER_ACTIONS.slice(-5); // 最近5个动作
    }
    
    // 添加页面状态
    errorInfo.pageState = {
      url: window.location.href,
      referrer: document.referrer,
      screen: `${window.screen.width}x${window.screen.height}`,
      viewport: `${window.innerWidth}x${window.innerHeight}`,
      userAgent: navigator.userAgent,
      cookies: document.cookie ? 'available' : 'empty',
      localStorage: localStorage.length > 0 ? 'available' : 'empty',
    };
    
    // 添加性能信息
    if (performance.timing) {
      errorInfo.performance = {
        loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart,
        domReady: performance.timing.domContentLoadedEventStart - performance.timing.navigationStart,
      };
    }
    
    // 添加内存信息(Chrome)
    if (performance.memory) {
      errorInfo.memory = {
        used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + 'MB',
        total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + 'MB',
      };
    }
  }

  // 📤 上报错误
  reportError(errorInfo) {
    // 采样控制
    if (Math.random() > this.config.sampleRate) {
      return;
    }
    
    // 错误去重
    if (this.config.deduplicate) {
      const errorKey = this.getErrorKey(errorInfo);
      const lastSent = this.sentErrors.get(errorKey);
      
      if (lastSent && Date.now() - lastSent < this.config.dedupeWindow) {
        return; // 最近上报过相同错误
      }
      
      this.sentErrors.set(errorKey, Date.now());
    }
    
    // 添加到队列
    this.errorQueue.push(errorInfo);
    
    // 开发环境打印
    if (this.config.showUI) {
      console.group('🚨 捕获到错误');
      console.error(errorInfo);
      console.groupEnd();
    }
    
    // 立即上报严重错误
    if (this.isCriticalError(errorInfo)) {
      this.flush(true);
    }
  }

  // 🚨 判断是否为严重错误
  isCriticalError(errorInfo) {
    const criticalMessages = [
      'script error',
      'syntax error',
      'type error',
      'reference error',
      'network error',
      'failed to fetch',
    ];
    
    return (
      errorInfo.type === 'network_error' ||
      errorInfo.type === 'http_error' && errorInfo.status >= 500 ||
      criticalMessages.some(msg => 
        errorInfo.message?.toLowerCase().includes(msg)
      )
    );
  }

  // 🔑 生成错误唯一标识(用于去重)
  getErrorKey(errorInfo) {
    // 基于错误信息生成唯一key
    if (errorInfo.stack) {
      // 使用错误堆栈的第一行
      const stackLines = errorInfo.stack.split('\n');
      const firstLine = stackLines[0] || '';
      return `${errorInfo.type}:${firstLine}`;
    }
    
    return `${errorInfo.type}:${errorInfo.message}:${errorInfo.filename}:${errorInfo.lineno}`;
  }

  // 🚚 批量上报
  async flush(isUrgent = false) {
    if (this.errorQueue.length === 0) return;
    
    const batch = this.errorQueue.splice(0, 10); // 每次最多上报10个
    
    try {
      const payload = {
        errors: batch,
        environment: {
          env: process.env.NODE_ENV,
          version: process.env.REACT_APP_VERSION || 'unknown',
          timestamp: Date.now(),
        },
      };
      
      if (navigator.sendBeacon && isUrgent) {
        // 页面卸载时使用sendBeacon
        const blob = new Blob([JSON.stringify(payload)], {
          type: 'application/json',
        });
        navigator.sendBeacon(this.config.endpoint, blob);
      } else {
        // 正常上报
        await fetch(this.config.endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
          keepalive: isUrgent, // 页面卸载时保持连接
        });
      }
      
      console.log(`✅ 上报 ${batch.length} 个错误`);
    } catch (error) {
      console.warn('错误上报失败:', error);
      // 失败后放回队列
      this.errorQueue.unshift(...batch);
    }
  }

  // 🎨 开发环境显示友好错误界面
  showErrorUI(errorInfo) {
    // 如果已经显示过错误界面,不再重复显示
    if (document.getElementById('error-monitor-ui')) {
      return;
    }
    
    const errorUI = document.createElement('div');
    errorUI.id = 'error-monitor-ui';
    errorUI.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      background: #fff3cd;
      border: 1px solid #ffeaa7;
      padding: 15px;
      border-radius: 8px;
      max-width: 400px;
      z-index: 99999;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
      font-size: 14px;
    `;
    
    errorUI.innerHTML = `
      <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
        <strong style="color: #e74c3c;">🚨 发现错误</strong>
        <button onclick="this.parentElement.parentElement.remove()" 
                style="background: none; border: none; font-size: 20px; cursor: pointer; color: #666;">
          ×
        </button>
      </div>
      <div style="margin-bottom: 10px;">
        <div><strong>类型:</strong> ${errorInfo.type}</div>
        <div><strong>信息:</strong> ${errorInfo.message?.slice(0, 100) || '无'}</div>
        <div><strong>页面:</strong> ${window.location.pathname}</div>
      </div>
      <div style="display: flex; gap: 10px;">
        <button onclick="window.location.reload()" 
                style="background: #3498db; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
          刷新页面
        </button>
        <button onclick="console.error(${JSON.stringify(errorInfo)})" 
                style="background: #95a5a6; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
          查看详情
        </button>
      </div>
      <div style="margin-top: 10px; font-size: 12px; color: #666;">
        此界面仅在开发环境显示
      </div>
    `;
    
    document.body.appendChild(errorUI);
    
    // 10秒后自动关闭
    setTimeout(() => {
      if (errorUI.parentNode) {
        errorUI.remove();
      }
    }, 10000);
  }

  // 🔍 获取DOM路径(辅助排查)
  getDomPath(element) {
    const path = [];
    while (element) {
      let selector = element.tagName.toLowerCase();
      
      if (element.id) {
        selector += `#${element.id}`;
        path.unshift(selector);
        break;
      } else {
        let sibling = element;
        let nth = 1;
        while (sibling.previousElementSibling) {
          sibling = sibling.previousElementSibling;
          if (sibling.tagName === element.tagName) {
            nth++;
          }
        }
        
        if (nth !== 1) {
          selector += `:nth-of-type(${nth})`;
        }
      }
      
      path.unshift(selector);
      element = element.parentElement;
    }
    
    return path.join(' > ');
  }

  // 📝 手动上报错误(业务代码中使用)
  captureError(error, context = {}) {
    const errorInfo = {
      type: 'manual_error',
      message: error?.message || String(error),
      stack: error?.stack,
      timestamp: Date.now(),
      url: window.location.href,
      context,
    };
    
    this.reportError(errorInfo);
  }

  // 🧪 测试错误上报
  testError() {
    try {
      throw new Error('这是一个测试错误,用于验证错误监控系统');
    } catch (error) {
      this.captureError(error, { test: true });
    }
  }

  // 📊 获取错误统计
  getStats() {
    const oneHourAgo = Date.now() - 3600000;
    const recentErrors = this.errorQueue.filter(e => e.timestamp > oneHourAgo);
    
    return {
      total: this.errorQueue.length,
      recent: recentErrors.length,
      byType: this.groupBy(recentErrors, 'type'),
      byPage: this.groupBy(recentErrors, 'url'),
    };
  }

  groupBy(array, key) {
    return array.reduce((result, item) => {
      const value = item[key];
      result[value] = (result[value] || 0) + 1;
      return result;
    }, {});
  }

  // 单例模式
  static getInstance(options) {
    if (!this.instance) {
      this.instance = new ErrorMonitor(options);
    }
    return this.instance;
  }
}

// 导出单例
export const errorMonitor = ErrorMonitor.getInstance({
  endpoint: process.env.REACT_APP_ERROR_ENDPOINT,
  sampleRate: parseFloat(process.env.REACT_APP_ERROR_SAMPLE_RATE || '1.0'),
});

⚛️ 第二步:集成到React项目

1. React错误边界组件

// src/components/ErrorBoundary.jsx
import React from 'react';
import { errorMonitor } from '../utils/errorMonitor';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { 
      hasError: false, 
      error: null,
      errorInfo: null 
    };
  }

  static getDerivedStateFromError(error) {
    // 更新state使下一次渲染显示降级UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 上报错误
    errorMonitor.captureError(error, {
      componentStack: errorInfo.componentStack,
      componentName: this.props.componentName || 'Unknown',
      props: this.safeProps(this.props),
    });

    this.setState({
      error,
      errorInfo
    });
  }

  // 安全地序列化props(避免循环引用)
  safeProps(props) {
    const safe = {};
    Object.keys(props).forEach(key => {
      if (key !== 'children' && key !== 'history' && key !== 'location') {
        try {
          safe[key] = typeof props[key] === 'function' 
            ? '[Function]' 
            : JSON.stringify(props[key]);
        } catch (e) {
          safe[key] = '[Unserializable]';
        }
      }
    });
    return safe;
  }

  handleReset = () => {
    this.setState({ hasError: false, error: null, errorInfo: null });
    window.location.reload();
  };

  render() {
    if (this.state.hasError) {
      return (
        <div style={{
          padding: '20px',
          margin: '20px',
          border: '1px solid #e74c3c',
          borderRadius: '8px',
          background: '#fdf2f2'
        }}>
          <h3 style={{ color: '#e74c3c', marginTop: 0 }}>
            ⚠️ 组件出错了
          </h3>
          
          <div style={{ marginBottom: '15px' }}>
            <strong>错误信息:</strong> {this.state.error?.toString()}
          </div>
          
          {process.env.NODE_ENV === 'development' && (
            <details style={{ marginBottom: '15px' }}>
              <summary>错误堆栈</summary>
              <pre style={{ 
                background: '#f5f5f5', 
                padding: '10px', 
                overflow: 'auto',
                fontSize: '12px'
              }}>
                {this.state.errorInfo?.componentStack}
              </pre>
            </details>
          )}
          
          <div style={{ display: 'flex', gap: '10px' }}>
            <button
              onClick={this.handleReset}
              style={{
                background: '#3498db',
                color: 'white',
                border: 'none',
                padding: '8px 16px',
                borderRadius: '4px',
                cursor: 'pointer'
              }}
            >
              刷新页面
            </button>
            
            <button
              onClick={() => {
                errorMonitor.captureError(this.state.error, {
                  manualReport: true,
                  componentStack: this.state.errorInfo?.componentStack
                });
                alert('错误已重新上报');
              }}
              style={{
                background: '#95a5a6',
                color: 'white',
                border: 'none',
                padding: '8px 16px',
                borderRadius: '4px',
                cursor: 'pointer'
              }}
            >
              重新上报错误
            </button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

// 高阶组件:包装组件添加错误边界
export function withErrorBoundary(WrappedComponent, componentName) {
  return function WithErrorBoundary(props) {
    return (
      <ErrorBoundary componentName={componentName}>
        <WrappedComponent {...props} />
      </ErrorBoundary>
    );
  };
}

export default ErrorBoundary;

2. 在React应用中使用

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ErrorBoundary from './components/ErrorBoundary';
import { errorMonitor } from './utils/errorMonitor';

// 初始化错误监控
errorMonitor.testError(); // 测试错误上报

// 监听React渲染错误
const originalConsoleError = console.error;
console.error = (...args) => {
  // 检查是否是React渲染错误
  if (typeof args[0] === 'string' && args[0].includes('React')) {
    errorMonitor.captureError(new Error(args[0]), {
      type: 'react_render_error',
      args: args.slice(1)
    });
  }
  originalConsoleError.apply(console, args);
};

ReactDOM.render(
  <React.StrictMode>
    <ErrorBoundary>
      <App />
    </ErrorBoundary>
  </React.StrictMode>,
  document.getElementById('root')
);

3. 包装业务组件

// src/components/ProductDetail.jsx
import React, { useState, useEffect } from 'react';
import { withErrorBoundary } from './ErrorBoundary';
import { errorMonitor } from '../utils/errorMonitor';

function ProductDetail({ productId }) {
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const loadProduct = async () => {
      try {
        const response = await fetch(`/api/products/${productId}`);
        if (!response.ok) {
          throw new Error(`产品加载失败: ${response.status}`);
        }
        const data = await response.json();
        setProduct(data);
      } catch (error) {
        // 手动上报错误
        errorMonitor.captureError(error, {
          productId,
          component: 'ProductDetail',
          action: 'load_product'
        });
        
        // 显示错误状态
        setProduct(null);
      } finally {
        setLoading(false);
      }
    };

    loadProduct();
  }, [productId]);

  const handleAddToCart = async () => {
    try {
      const response = await fetch('/api/cart/add', {
        method: 'POST',
        body: JSON.stringify({ productId }),
        headers: { 'Content-Type': 'application/json' }
      });
      
      if (!response.ok) {
        throw new Error('加入购物车失败');
      }
      
      alert('添加成功!');
    } catch (error) {
      // 业务错误上报
      errorMonitor.captureError(error, {
        productId,
        action: 'add_to_cart',
        severity: 'high' // 高优先级错误
      });
      
      alert('操作失败,请重试');
    }
  };

  if (loading) return <div>加载中...</div>;
  if (!product) return <div>产品不存在</div>;

  return (
    <div className="product-detail">
      <h1>{product.name}</h1>
      <button onClick={handleAddToCart}>加入购物车</button>
    </div>
  );
}

// 使用错误边界包装组件
export default withErrorBoundary(ProductDetail, 'ProductDetail');

🖥️ 第三步:搭建错误监控后台

1. 简单的Node.js错误接收服务

// server/error-server.js
const express = require('express');
const app = express();
const port = 3002;

app.use(express.json());

// 内存存储(生产环境用数据库)
const errorsDB = [];

// 接收错误数据
app.post('/api/error-collect', (req, res) => {
  const { errors, environment } = req.body;
  
  console.log(`📥 收到 ${errors?.length || 0} 个错误`);
  
  if (Array.isArray(errors)) {
    errors.forEach(error => {
      // 添加环境信息
      const enhancedError = {
        ...error,
        environment,
        receivedAt: new Date().toISOString(),
        id: Date.now() + Math.random().toString(36).substr(2, 9),
      };
      
      errorsDB.push(enhancedError);
      
      // 控制台输出重要错误
      if (error.type === 'js_error' || error.type === 'promise_error') {
        console.error(`🚨 ${error.type}: ${error.message}`);
        if (error.stack) {
          console.error(error.stack);
        }
      }
      
      // 触发告警(这里简化处理)
      checkAndAlert(enhancedError);
    });
  }
  
  res.json({ success: true });
});

// 错误告警检查
function checkAndAlert(error) {
  const alertRules = [
    {
      condition: (e) => e.type === 'network_error',
      message: '网络请求失败',
      level: 'high'
    },
    {
      condition: (e) => e.type === 'http_error' && e.status >= 500,
      message: '服务器内部错误',
      level: 'critical'
    },
    {
      condition: (e) => e.message?.includes('SyntaxError'),
      message: '语法错误',
      level: 'high'
    },
  ];
  
  alertRules.forEach(rule => {
    if (rule.condition(error)) {
      sendAlert(rule.message, error, rule.level);
    }
  });
}

// 发送告警(简化版)
function sendAlert(message, error, level) {
  console.log(`🚨 [${level.toUpperCase()}] ${message}`);
  console.log('错误详情:', {
    type: error.type,
    message: error.message,
    url: error.url,
    timestamp: new Date(error.timestamp).toLocaleString(),
  });
  
  // 实际项目这里应该:
  // 1. 发送到钉钉/企业微信
  // 2. 发送邮件
  // 3. 调用告警平台API
}

// 获取错误列表API
app.get('/api/errors', (req, res) => {
  const { 
    type, 
    page = 1, 
    limit = 20,
    startTime,
    endTime 
  } = req.query;
  
  let filtered = [...errorsDB];
  
  // 过滤
  if (type) {
    filtered = filtered.filter(e => e.type === type);
  }
  
  if (startTime) {
    filtered = filtered.filter(e => e.timestamp >= parseInt(startTime));
  }
  
  if (endTime) {
    filtered = filtered.filter(e => e.timestamp <= parseInt(endTime));
  }
  
  // 排序(最新的在前)
  filtered.sort((a, b) => b.timestamp - a.timestamp);
  
  // 分页
  const start = (page - 1) * limit;
  const paginated = filtered.slice(start, start + limit);
  
  res.json({
    total: filtered.length,
    page: parseInt(page),
    limit: parseInt(limit),
    data: paginated,
  });
});

// 错误统计API
app.get('/api/errors/stats', (req, res) => {
  const oneHourAgo = Date.now() - 3600000;
  const oneDayAgo = Date.now() - 86400000;
  
  const recentErrors = errorsDB.filter(e => e.timestamp > oneHourAgo);
  const dailyErrors = errorsDB.filter(e => e.timestamp > oneDayAgo);
  
  const stats = {
    total: errorsDB.length,
    lastHour: recentErrors.length,
    last24Hours: dailyErrors.length,
    byType: groupBy(errorsDB, 'type'),
    byPage: Object.entries(groupBy(errorsDB, 'url'))
      .sort(([,a], [,b]) => b - a)
      .slice(0, 10) // Top 10页面
      .reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {}),
    trend: getHourlyTrend(),
  };
  
  res.json(stats);
});

// 分组函数
function groupBy(array, key) {
  return array.reduce((result, item) => {
    const value = item[key];
    result[value] = (result[value] || 0) + 1;
    return result;
  }, {});
}

// 获取小时趋势
function getHourlyTrend() {
  const now = new Date();
  const trends = [];
  
  for (let i = 23; i >= 0; i--) {
    const hourStart = new Date(now);
    hourStart.setHours(now.getHours() - i, 0, 0, 0);
    const hourEnd = new Date(hourStart);
    hourEnd.setHours(hourStart.getHours() + 1);
    
    const count = errorsDB.filter(e => {
      const time = new Date(e.timestamp);
      return time >= hourStart && time < hourEnd;
    }).length;
    
    trends.push({
      hour: hourStart.getHours(),
      count,
      time: hourStart.toLocaleTimeString('zh-CN', { hour: '2-digit' }),
    });
  }
  
  return trends;
}

// 错误详情API
app.get('/api/errors/:id', (req, res) => {
  const error = errorsDB.find(e => e.id === req.params.id);
  
  if (!error) {
    return res.status(404).json({ error: '错误不存在' });
  }
  
  // 查找相似错误(基于错误信息)
  const similar = errorsDB.filter(e => 
    e.id !== error.id && 
    e.type === error.type && 
    e.message === error.message
  ).slice(0, 5);
  
  res.json({
    error,
    similar,
    totalOccurrences: errorsDB.filter(e => 
      e.type === error.type && e.message === error.message
    ).length,
  });
});

// 标记错误为已解决
app.post('/api/errors/:id/resolve', (req, res) => {
  const error = errorsDB.find(e => e.id === req.params.id);
  
  if (!error) {
    return res.status(404).json({ error: '错误不存在' });
  }
  
  error.resolved = true;
  error.resolvedAt = new Date().toISOString();
  error.resolvedBy = req.body.user || 'system';
  
  res.json({ success: true });
});

app.listen(port, () => {
  console.log(`🚨 错误监控服务运行在 http://localhost:${port}`);
  console.log(`📤 错误接收地址: POST http://localhost:${port}/api/error-collect`);
  console.log(`📊 错误查看地址: GET http://localhost:${port}/api/errors`);
});

2. 运行错误监控服务

# 启动错误监控服务
node server/error-server.js

# 前端配置错误上报地址
# 在.env文件中添加
REACT_APP_ERROR_ENDPOINT=http://localhost:3002/api/error-collect
REACT_APP_ERROR_SAMPLE_RATE=1.0

🖥️ 第四步:错误看板(前端界面)

1. 简易错误看板组件

// src/components/ErrorDashboard.jsx
import React, { useState, useEffect } from 'react';

function ErrorDashboard() {
  const [errors, setErrors] = useState([]);
  const [stats, setStats] = useState(null);
  const [loading, setLoading] = useState(true);
  const [filter, setFilter] = useState('all');
  
  // 只在开发环境显示
  if (process.env.NODE_ENV === 'production') {
    return null;
  }
  
  useEffect(() => {
    fetchErrors();
    fetchStats();
    
    // 每30秒刷新一次
    const interval = setInterval(fetchErrors, 30000);
    return () => clearInterval(interval);
  }, [filter]);
  
  const fetchErrors = async () => {
    try {
      const response = await fetch('http://localhost:3002/api/errors?limit=10');
      const data = await response.json();
      setErrors(data.data || []);
    } catch (error) {
      console.error('获取错误列表失败:', error);
    } finally {
      setLoading(false);
    }
  };
  
  const fetchStats = async () => {
    try {
      const response = await fetch('http://localhost:3002/api/errors/stats');
      const data = await response.json();
      setStats(data);
    } catch (error) {
      console.error('获取统计失败:', error);
    }
  };
  
  const filteredErrors = filter === 'all' 
    ? errors 
    : errors.filter(e => e.type === filter);
  
  const getErrorColor = (type) => {
    const colors = {
      js_error: '#e74c3c',
      promise_error: '#e67e22',
      http_error: '#3498db',
      network_error: '#9b59b6',
      console_error: '#95a5a6',
      resource_error: '#1abc9c',
    };
    return colors[type] || '#7f8c8d';
  };
  
  const formatTime = (timestamp) => {
    const date = new Date(timestamp);
    return date.toLocaleTimeString('zh-CN');
  };
  
  return (
    <div style={{
      position: 'fixed',
      bottom: '20px',
      left: '20px',
      background: 'white',
      border: '1px solid #ddd',
      borderRadius: '8px',
      padding: '15px',
      maxWidth: '500px',
      maxHeight: '400px',
      overflow: 'auto',
      boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
      zIndex: 9999,
      fontSize: '12px',
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
        <h3 style={{ margin: 0 }}>🚨 错误监控面板</h3>
        <div>
          <button 
            onClick={fetchErrors}
            style={{ marginRight: '10px', padding: '2px 8px' }}
          >
            刷新
          </button>
          <select 
            value={filter} 
            onChange={(e) => setFilter(e.target.value)}
            style={{ padding: '2px 5px' }}
          >
            <option value="all">全部类型</option>
            <option value="js_error">JS错误</option>
            <option value="promise_error">Promise错误</option>
            <option value="http_error">HTTP错误</option>
            <option value="network_error">网络错误</option>
          </select>
        </div>
      </div>
      
      {stats && (
        <div style={{ 
          display: 'grid', 
          gridTemplateColumns: 'repeat(3, 1fr)',
          gap: '10px',
          marginBottom: '15px',
          padding: '10px',
          background: '#f8f9fa',
          borderRadius: '4px'
        }}>
          <div style={{ textAlign: 'center' }}>
            <div style={{ fontSize: '18px', fontWeight: 'bold' }}>{stats.total}</div>
            <div style={{ fontSize: '11px', color: '#666' }}>总错误数</div>
          </div>
          <div style={{ textAlign: 'center' }}>
            <div style={{ fontSize: '18px', fontWeight: 'bold', color: '#e74c3c' }}>{stats.lastHour}</div>
            <div style={{ fontSize: '11px', color: '#666' }}>1小时内</div>
          </div>
          <div style={{ textAlign: 'center' }}>
            <div style={{ fontSize: '18px', fontWeight: 'bold', color: '#e67e22' }}>{stats.last24Hours}</div>
            <div style={{ fontSize: '11px', color: '#666' }}>24小时内</div>
          </div>
        </div>
      )}
      
      {loading ? (
        <div>加载中...</div>
      ) : filteredErrors.length === 0 ? (
        <div style={{ textAlign: 'center', color: '#666', padding: '20px' }}>
          🎉 暂无错误
        </div>
      ) : (
        <div>
          {filteredErrors.map((error, index) => (
            <div 
              key={error.id || index}
              style={{
                borderLeft: `3px solid ${getErrorColor(error.type)}`,
                padding: '10px',
                marginBottom: '8px',
                background: '#f8f9fa',
                borderRadius: '0 4px 4px 0',
              }}
            >
              <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }}>
                <strong>{error.type}</strong>
                <span style={{ fontSize: '11px', color: '#666' }}>{formatTime(error.timestamp)}</span>
              </div>
              <div style={{ 
                fontSize: '11px', 
                color: '#333',
                marginBottom: '5px',
                overflow: 'hidden',
                textOverflow: 'ellipsis',
                whiteSpace: 'nowrap'
              }}>
                {error.message || '无错误信息'}
              </div>
              {error.url && (
                <div style={{ fontSize: '10px', color: '#666' }}>
                  页面: {error.url.replace(window.location.origin, '')}
                </div>
              )}
              <button 
                onClick={() => console.error('错误详情:', error)}
                style={{
                  marginTop: '5px',
                  fontSize: '10px',
                  padding: '2px 6px',
                  background: 'none',
                  border: '1px solid #ddd',
                  borderRadius: '3px',
                  cursor: 'pointer'
                }}
              >
                查看详情
              </button>
            </div>
          ))}
        </div>
      )}
      
      <div style={{ marginTop: '10px', fontSize: '11px', color: '#666', textAlign: 'center' }}>
        错误数据来自本地监控服务
      </div>
    </div>
  );
}

export default ErrorDashboard;

2. 在应用中使用看板

// src/App.jsx
import React from 'react';
import ProductDetail from './components/ProductDetail';
import ErrorDashboard from './components/ErrorDashboard';
import { errorMonitor } from './utils/errorMonitor';

function App() {
  // 测试错误按钮(开发环境)
  const triggerTestError = () => {
    errorMonitor.testError();
    alert('测试错误已触发,查看控制台或错误面板');
  };
  
  return (
    <div className="App">
      <header style={{ padding: '20px', background: '#f5f5f5' }}>
        <h1>电商网站 - 错误监控演示</h1>
        {process.env.NODE_ENV !== 'production' && (
          <button 
            onClick={triggerTestError}
            style={{
              background: '#e74c3c',
              color: 'white',
              border: 'none',
              padding: '8px 16px',
              borderRadius: '4px',
              cursor: 'pointer'
            }}
          >
            触发测试错误
          </button>
        )}
      </header>
      
      <main style={{ padding: '20px' }}>
        <ProductDetail productId="123" />
      </main>
      
      {/* 开发环境显示错误面板 */}
      {process.env.NODE_ENV !== 'production' && <ErrorDashboard />}
    </div>
  );
}

export default App;

🔍 第五步:错误排查实战指南

1. 常见错误排查流程

// src/utils/errorDebugger.js
class ErrorDebugger {
  // 错误诊断工具
  static diagnose(error) {
    const diagnosis = {
      severity: 'low',
      likelyCause: '未知',
      suggestedFix: '查看错误详情',
      relatedIssues: [],
    };
    
    // 根据错误类型和消息诊断
    if (error.message?.includes('NetworkError')) {
      diagnosis.likelyCause = '网络连接问题';
      diagnosis.suggestedFix = '检查网络连接,重试操作';
      diagnosis.severity = 'medium';
    }
    
    if (error.message?.includes('SyntaxError')) {
      diagnosis.likelyCause = 'JavaScript语法错误';
      diagnosis.suggestedFix = '检查相关代码语法';
      diagnosis.severity = 'high';
    }
    
    if (error.message?.includes('Unexpected token')) {
      diagnosis.likelyCause = 'JSON解析错误';
      diagnosis.suggestedFix = '检查API返回的数据格式';
      diagnosis.severity = 'high';
    }
    
    if (error.status === 404) {
      diagnosis.likelyCause = '请求的资源不存在';
      diagnosis.suggestedFix = '检查API地址是否正确';
      diagnosis.severity = 'medium';
    }
    
    if (error.status === 500) {
      diagnosis.likelyCause = '服务器内部错误';
      diagnosis.suggestedFix = '联系后端开发人员';
      diagnosis.severity = 'high';
    }
    
    return diagnosis;
  }
  
  // 重现错误步骤
  static generateReproSteps(error) {
    const steps = [];
    
    // 从用户行为中提取重现步骤
    if (error.userActions) {
      steps.push('重现步骤:');
      error.userActions.forEach((action, i) => {
        steps.push(`${i + 1}. ${action.type}: ${action.path || action.name}`);
      });
    }
    
    // 添加环境信息
    steps.push('\n环境信息:');
    steps.push(`- 浏览器: ${navigator.userAgent}`);
    steps.push(`- 页面URL: ${error.url}`);
    steps.push(`- 时间: ${new Date(error.timestamp).toLocaleString()}`);
    
    return steps.join('\n');
  }
  
  // 检查相关依赖
  static checkDependencies(error) {
    const issues = [];
    
    // 检查API端点是否可达
    if (error.type === 'network_error') {
      issues.push('检查API服务是否运行正常');
    }
    
    // 检查资源文件
    if (error.type === 'resource_error') {
      issues.push('检查静态资源文件是否存在');
    }
    
    return issues;
  }
}

// 在错误上报时自动诊断
errorMonitor.reportError = function(errorInfo) {
  // 原上报逻辑...
  
  // 添加诊断信息
  errorInfo.diagnosis = ErrorDebugger.diagnose(errorInfo);
  errorInfo.reproSteps = ErrorDebugger.generateReproSteps(errorInfo);
  
  // 调用原始上报
  // ...
};

2. 错误追踪ID系统

// 为每个用户会话生成唯一ID
class TraceIdSystem {
  constructor() {
    this.sessionId = this.generateSessionId();
    this.traceIds = new Map();
  }
  
  generateSessionId() {
    return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  }
  
  // 为请求生成追踪ID
  generateTraceId(context = '') {
    const traceId = `trace_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
    this.traceIds.set(traceId, {
      timestamp: Date.now(),
      context,
      sessionId: this.sessionId,
    });
    
    return traceId;
  }
  
  // 在fetch请求中添加追踪ID
  instrumentFetch() {
    const originalFetch = window.fetch;
    
    window.fetch = async (...args) => {
      const traceId = this.generateTraceId(`fetch:${args[0]}`);
      
      // 添加追踪头部
      const [url, options = {}] = args;
      const headers = {
        ...options.headers,
        'X-Trace-Id': traceId,
        'X-Session-Id': this.sessionId,
      };
      
      try {
        const response = await originalFetch(url, { ...options, headers });
        
        // 从响应中获取后端追踪ID
        const backendTraceId = response.headers.get('X-Backend-Trace-Id');
        if (backendTraceId) {
          this.traceIds.get(traceId).backendTraceId = backendTraceId;
        }
        
        return response;
      } catch (error) {
        // 在错误中记录追踪ID
        error.traceId = traceId;
        throw error;
      }
    };
  }
  
  // 获取追踪信息
  getTraceInfo(traceId) {
    return this.traceIds.get(traceId) || null;
  }
}

// 使用追踪ID
const traceSystem = new TraceIdSystem();
traceSystem.instrumentFetch();

// 在错误中记录追踪ID
errorMonitor.captureError = function(error, context) {
  const traceId = traceSystem.generateTraceId(`error:${error.message}`);
  
  const enhancedError = {
    ...error,
    traceId,
    sessionId: traceSystem.sessionId,
    context,
  };
  
  this.reportError(enhancedError);
};

📋 第六步:项目部署清单

1. 快速启动清单

## 第一天:基础错误监控
- [ ] 复制 errorMonitor.js 到项目
- [ ] 在入口文件初始化监控
- [ ] 启动本地错误服务器
- [ ] 测试错误上报(点击测试按钮)
- [ ] 查看控制台是否有错误数据

## 第一周:完善监控
- [ ] 为所有页面组件添加ErrorBoundary
- [ ] 监控关键API接口
- [ ] 配置生产环境上报地址
- [ ] 设置错误告警(邮件/钉钉)

## 第一个月:深度优化  
- [ ] 分析错误趋势,找到最多错误页面
- [ ] 修复高频错误
- [ ] 建立错误处理规范
- [ ] 设置错误处理SOP(标准作业程序)

## 长期维护
- [ ] 每周查看错误报告
- [ ] 每月分析错误趋势
- [ ] 更新错误处理策略
- [ ] 团队分享错误排查经验

2. 实际项目配置

// .env 配置示例
REACT_APP_ERROR_ENDPOINT_DEV=http://localhost:3002/api/error-collect
REACT_APP_ERROR_ENDPOINT_PROD=https://api.yourcompany.com/error-collect
REACT_APP_ERROR_SAMPLE_RATE_DEV=1.0      # 开发环境100%上报
REACT_APP_ERROR_SAMPLE_RATE_PROD=0.1     # 生产环境10%采样
REACT_APP_ENABLE_ERROR_DASHBOARD=true    # 开发环境显示看板

// package.json 脚本
{
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "error-server": "node server/error-server.js",
    "error-server:prod": "NODE_ENV=production node server/error-server.js",
    "test-error": "curl -X POST http://localhost:3002/api/error-collect -H 'Content-Type: application/json' -d '{\"errors\":[{\"type\":\"test\",\"message\":\"测试错误\"}]}'"
  }
}

3. 团队协作规范

# 错误处理规范

## 错误上报要求
1. 所有异步操作必须使用 try-catch
2. 关键业务操作必须手动上报错误
3. 组件必须使用 ErrorBoundary 包装
4. HTTP状态码 >= 400 必须上报

## 错误信息规范
- 错误信息必须包含上下文
- 网络错误必须包含URL和方法
- 业务错误必须包含用户ID和操作
- 组件错误必须包含组件名和props

## 错误处理SOP
1. 收到告警后,立即查看错误详情
2. 根据traceId追踪完整请求链路
3. 参考reproSteps重现错误
4. 根据diagnosis建议尝试修复
5. 修复后标记错误为已解决

## 错误分级处理
- P0(严重):立即修复,影响核心功能
- P1(高):24小时内修复,影响用户体验
- P2(中):本周内修复,功能降级可用
- P3(低):规划修复,不影响使用

🎯 总结:立即行动

5分钟快速开始:

  1. 复制 errorMonitor.js 到你的项目
  2. index.js 添加
    import { errorMonitor } from './utils/errorMonitor';
    errorMonitor.testError(); // 测试
    
  3. 运行错误服务器
    node server/error-server.js
    
  4. 启动项目,点击页面上的"测试错误"按钮
  5. 查看控制台,确认错误已上报

重点关注这些错误:

  • 🔴 红色警报:JS语法错误、网络错误、500错误
  • 🟠 橙色警报:404错误、API超时、资源加载失败
  • 🟡 黄色警报:控制台错误、警告信息
  • 🟢 正常:已处理并降级的错误

错误排查三板斧:

  1. 看堆栈:错误发生在哪一行?
  2. 看上下文:用户做了什么操作?
  3. 看环境:什么浏览器?什么网络?

记住:

  • 没有完美的系统,只有完善的监控
  • 错误是信息,不是失败
  • 监控是为了发现,发现是为了改进
  • 从最简单的开始,逐步完善

现在就开始,复制代码到你的项目,今天就能拥有错误监控能力!

posted @ 2025-12-22 14:32  XiaoZhengTou  阅读(32)  评论(0)    收藏  举报