Skip to content

错误上报系统搭建 - 前端监控体系建设

1. 错误上报系统概述

1.1 系统架构

typescript
const errorReportingArchitecture = {
  客户端层: [
    '错误捕获: window.onerror, unhandledrejection等',
    '错误收集: 收集错误信息、上下文、环境',
    '错误过滤: 去重、采样、黑名单',
    '错误上报: 发送到服务器',
    '本地缓存: 离线时存储,联网后发送'
  ],
  传输层: [
    'HTTP请求: POST /api/errors',
    '图片上报: new Image().src',
    'Beacon API: navigator.sendBeacon',
    'WebSocket: 实时上报'
  ],
  服务端层: [
    '接收错误: API接口',
    '存储错误: 数据库',
    '错误分析: 聚合、统计',
    '告警通知: 邮件、短信、钉钉',
    '可视化: Dashboard展示'
  ],
  管理层: [
    '错误查询: 搜索、过滤',
    '错误详情: 堆栈、上下文',
    '错误追踪: SourceMap映射',
    '用户追踪: Session Replay'
  ]
};

1.2 核心功能

typescript
const coreFeatures = {
  错误捕获: ['JS错误', 'Promise错误', '资源加载错误', 'React错误', 'API错误'],
  错误信息: ['错误消息', '错误堆栈', '发生时间', '用户信息', '设备信息', '页面信息'],
  上报策略: ['实时上报', '批量上报', '采样上报', '离线缓存'],
  错误处理: ['去重', '过滤', '聚合', 'SourceMap还原'],
  分析展示: ['错误统计', '趋势分析', '影响范围', '告警通知']
};

2. 客户端错误捕获

2.1 全局错误捕获

javascript
// 错误捕获器类
class ErrorCatcher {
  constructor(config = {}) {
    this.config = {
      dsn: config.dsn,
      environment: config.environment || 'production',
      release: config.release || '1.0.0',
      sampleRate: config.sampleRate || 1.0,  // 采样率
      maxBreadcrumbs: config.maxBreadcrumbs || 50,  // 最大面包屑数量
      ...config
    };
    
    this.breadcrumbs = [];  // 面包屑
    this.errorQueue = [];   // 错误队列
    this.isOnline = navigator.onLine;
    
    this.init();
  }
  
  init() {
    // JS错误
    window.addEventListener('error', this.handleError.bind(this));
    
    // Promise错误
    window.addEventListener('unhandledrejection', this.handleUnhandledRejection.bind(this));
    
    // 资源加载错误
    window.addEventListener('error', this.handleResourceError.bind(this), true);
    
    // 网络状态
    window.addEventListener('online', () => {
      this.isOnline = true;
      this.flushQueue();
    });
    window.addEventListener('offline', () => {
      this.isOnline = false;
    });
    
    // 页面卸载时发送剩余错误
    window.addEventListener('beforeunload', () => {
      this.flushQueue(true);
    });
  }
  
  // 处理JS错误
  handleError(event) {
    const error = {
      type: 'javascript',
      message: event.message,
      filename: event.filename,
      lineno: event.lineno,
      colno: event.colno,
      stack: event.error?.stack,
      timestamp: Date.now()
    };
    
    this.captureError(error);
  }
  
  // 处理Promise错误
  handleUnhandledRejection(event) {
    const error = {
      type: 'unhandledrejection',
      message: event.reason?.message || event.reason,
      stack: event.reason?.stack,
      timestamp: Date.now()
    };
    
    this.captureError(error);
  }
  
  // 处理资源加载错误
  handleResourceError(event) {
    if (event.target !== window && (event.target.tagName === 'SCRIPT' || event.target.tagName === 'LINK' || event.target.tagName === 'IMG')) {
      const error = {
        type: 'resource',
        message: `Failed to load ${event.target.tagName.toLowerCase()}`,
        filename: event.target.src || event.target.href,
        timestamp: Date.now()
      };
      
      this.captureError(error);
    }
  }
  
  // 捕获错误
  captureError(error) {
    // 采样
    if (Math.random() > this.config.sampleRate) {
      return;
    }
    
    // 增强错误信息
    const enhancedError = {
      ...error,
      environment: this.config.environment,
      release: this.config.release,
      userAgent: navigator.userAgent,
      url: window.location.href,
      breadcrumbs: this.breadcrumbs.slice(),
      deviceInfo: this.getDeviceInfo(),
      userInfo: this.getUserInfo()
    };
    
    // 添加到队列
    this.errorQueue.push(enhancedError);
    
    // 立即发送或批量发送
    if (this.config.immediate) {
      this.sendError(enhancedError);
    } else {
      this.debouncedFlush();
    }
  }
  
  // 获取设备信息
  getDeviceInfo() {
    return {
      screenWidth: window.screen.width,
      screenHeight: window.screen.height,
      windowWidth: window.innerWidth,
      windowHeight: window.innerHeight,
      pixelRatio: window.devicePixelRatio,
      platform: navigator.platform,
      language: navigator.language
    };
  }
  
  // 获取用户信息(需要自行实现)
  getUserInfo() {
    return {
      userId: window.currentUser?.id,
      userName: window.currentUser?.name,
      userEmail: window.currentUser?.email
    };
  }
  
  // 添加面包屑
  addBreadcrumb(breadcrumb) {
    this.breadcrumbs.push({
      ...breadcrumb,
      timestamp: Date.now()
    });
    
    if (this.breadcrumbs.length > this.config.maxBreadcrumbs) {
      this.breadcrumbs.shift();
    }
  }
  
  // 发送错误
  sendError(error) {
    if (!this.config.dsn) return;
    
    const payload = {
      ...error,
      sdkVersion: '1.0.0'
    };
    
    // 使用Beacon API确保发送成功
    if (navigator.sendBeacon) {
      const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
      navigator.sendBeacon(this.config.dsn, blob);
    } else {
      // 降级到fetch
      fetch(this.config.dsn, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
        keepalive: true
      }).catch(err => {
        console.error('Failed to send error:', err);
        // 存储到本地,稍后重试
        this.saveToLocalStorage(payload);
      });
    }
  }
  
  // 刷新队列
  flushQueue(force = false) {
    if (this.errorQueue.length === 0) return;
    
    if (force || this.isOnline) {
      const errors = this.errorQueue.splice(0);
      
      if (errors.length === 1) {
        this.sendError(errors[0]);
      } else {
        // 批量发送
        this.sendBatch(errors);
      }
    }
  }
  
  // 批量发送
  sendBatch(errors) {
    if (!this.config.dsn) return;
    
    const payload = {
      batch: errors,
      timestamp: Date.now()
    };
    
    fetch(this.config.dsn + '/batch', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    }).catch(err => {
      console.error('Failed to send batch:', err);
    });
  }
  
  // 防抖刷新
  debouncedFlush = (() => {
    let timer = null;
    return () => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        this.flushQueue();
      }, 2000);
    };
  })();
  
  // 本地存储
  saveToLocalStorage(error) {
    try {
      const stored = JSON.parse(localStorage.getItem('errorReports') || '[]');
      stored.push(error);
      
      // 限制数量
      if (stored.length > 50) {
        stored.splice(0, stored.length - 50);
      }
      
      localStorage.setItem('errorReports', JSON.stringify(stored));
    } catch (err) {
      console.error('Failed to save to localStorage:', err);
    }
  }
}

// 初始化
const errorCatcher = new ErrorCatcher({
  dsn: 'https://api.yourapp.com/errors',
  environment: process.env.NODE_ENV,
  release: process.env.REACT_APP_VERSION,
  sampleRate: 1.0,
  immediate: false
});

// 暴露全局API
window.ErrorCatcher = errorCatcher;

2.2 React集成

jsx
// React错误捕获
class ReactErrorCatcher extends React.Component {
  componentDidCatch(error, errorInfo) {
    // 捕获React错误
    errorCatcher.captureError({
      type: 'react',
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
      timestamp: Date.now()
    });
  }
  
  render() {
    return this.props.children;
  }
}

// Hook方式捕获
function useErrorCatcher() {
  const captureError = useCallback((error, context = {}) => {
    errorCatcher.captureError({
      type: 'hook',
      message: error.message,
      stack: error.stack,
      context,
      timestamp: Date.now()
    });
  }, []);
  
  return captureError;
}

// 使用
function Component() {
  const captureError = useErrorCatcher();
  
  const handleClick = async () => {
    try {
      await riskyOperation();
    } catch (error) {
      captureError(error, { action: 'button-click' });
    }
  };
  
  return <button onClick={handleClick}>Click Me</button>;
}

2.3 API错误捕获

javascript
// 拦截fetch
const originalFetch = window.fetch;
window.fetch = function(...args) {
  const [url, options] = args;
  
  // 添加面包屑
  errorCatcher.addBreadcrumb({
    category: 'fetch',
    message: `${options?.method || 'GET'} ${url}`,
    level: 'info'
  });
  
  return originalFetch(...args).then(response => {
    if (!response.ok) {
      // 捕获HTTP错误
      errorCatcher.captureError({
        type: 'http',
        message: `HTTP ${response.status}: ${response.statusText}`,
        url,
        method: options?.method || 'GET',
        status: response.status,
        timestamp: Date.now()
      });
    }
    return response;
  }).catch(error => {
    // 捕获网络错误
    errorCatcher.captureError({
      type: 'network',
      message: error.message,
      url,
      method: options?.method || 'GET',
      timestamp: Date.now()
    });
    throw error;
  });
};

// 拦截XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.open = function(method, url, ...args) {
  this._method = method;
  this._url = url;
  return originalXHROpen.call(this, method, url, ...args);
};

XMLHttpRequest.prototype.send = function(...args) {
  this.addEventListener('error', () => {
    errorCatcher.captureError({
      type: 'xhr',
      message: 'XHR request failed',
      url: this._url,
      method: this._method,
      timestamp: Date.now()
    });
  });
  
  this.addEventListener('load', () => {
    if (this.status >= 400) {
      errorCatcher.captureError({
        type: 'xhr',
        message: `XHR ${this.status}: ${this.statusText}`,
        url: this._url,
        method: this._method,
        status: this.status,
        timestamp: Date.now()
      });
    }
  });
  
  return originalXHRSend.call(this, ...args);
};

3. 错误上报优化

3.1 错误去重

javascript
class ErrorDeduplicator {
  constructor(maxSize = 100) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }
  
  // 生成错误指纹
  getFingerprint(error) {
    const parts = [
      error.type,
      error.message,
      error.filename,
      error.lineno,
      error.colno
    ].filter(Boolean);
    
    return parts.join(':');
  }
  
  // 检查是否重复
  isDuplicate(error, timeWindow = 60000) {
    const fingerprint = this.getFingerprint(error);
    const cached = this.cache.get(fingerprint);
    
    if (cached) {
      const timeDiff = Date.now() - cached.timestamp;
      
      if (timeDiff < timeWindow) {
        // 更新计数
        cached.count++;
        return true;
      } else {
        // 超时,更新缓存
        this.cache.set(fingerprint, {
          timestamp: Date.now(),
          count: 1
        });
        return false;
      }
    }
    
    // 首次出现
    this.cache.set(fingerprint, {
      timestamp: Date.now(),
      count: 1
    });
    
    // 限制缓存大小
    if (this.cache.size > this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    return false;
  }
  
  // 获取错误统计
  getStats(error) {
    const fingerprint = this.getFingerprint(error);
    return this.cache.get(fingerprint);
  }
}

// 使用去重器
const deduplicator = new ErrorDeduplicator();

// 在captureError中使用
function captureErrorWithDedup(error) {
  if (deduplicator.isDuplicate(error)) {
    console.log('Duplicate error, skipped');
    return;
  }
  
  const stats = deduplicator.getStats(error);
  errorCatcher.captureError({
    ...error,
    occurrenceCount: stats.count
  });
}

3.2 错误采样

javascript
class ErrorSampler {
  constructor(config = {}) {
    this.globalSampleRate = config.globalSampleRate || 1.0;
    this.ruleSampleRates = config.ruleSampleRates || {};
    this.userSampleRates = new Map();
  }
  
  // 判断是否应该上报
  shouldSample(error) {
    // 规则采样
    const ruleRate = this.getRuleSampleRate(error);
    if (ruleRate !== null && Math.random() > ruleRate) {
      return false;
    }
    
    // 用户采样
    const userRate = this.getUserSampleRate(error);
    if (userRate !== null && Math.random() > userRate) {
      return false;
    }
    
    // 全局采样
    return Math.random() <= this.globalSampleRate;
  }
  
  // 获取规则采样率
  getRuleSampleRate(error) {
    for (const [pattern, rate] of Object.entries(this.ruleSampleRates)) {
      if (error.message && error.message.includes(pattern)) {
        return rate;
      }
    }
    return null;
  }
  
  // 获取用户采样率
  getUserSampleRate(error) {
    const userId = error.userInfo?.userId;
    if (!userId) return null;
    
    if (this.userSampleRates.has(userId)) {
      return this.userSampleRates.get(userId);
    }
    
    // 根据用户ID哈希决定采样率
    const hash = this.hashCode(userId);
    const rate = (hash % 100) / 100;
    this.userSampleRates.set(userId, rate);
    
    return rate;
  }
  
  // 简单哈希函数
  hashCode(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return Math.abs(hash);
  }
}

// 使用采样器
const sampler = new ErrorSampler({
  globalSampleRate: 0.8,  // 80%采样率
  ruleSampleRates: {
    'ResizeObserver': 0.1,  // 降低ResizeObserver错误采样
    'Non-Error': 0.2        // 降低非标准错误采样
  }
});

// 在captureError中使用
function captureErrorWithSampling(error) {
  if (!sampler.shouldSample(error)) {
    console.log('Error sampled out');
    return;
  }
  
  errorCatcher.captureError(error);
}

3.3 错误压缩

javascript
class ErrorCompressor {
  // 压缩错误数据
  compress(error) {
    return {
      t: error.type,
      m: error.message?.substring(0, 200),  // 限制长度
      f: this.shortenFilename(error.filename),
      l: error.lineno,
      c: error.colno,
      s: this.compressStack(error.stack),
      ts: error.timestamp,
      u: error.url && new URL(error.url).pathname,  // 只保留路径
      ua: this.compressUserAgent(error.userAgent)
    };
  }
  
  // 解压错误数据
  decompress(compressed) {
    return {
      type: compressed.t,
      message: compressed.m,
      filename: compressed.f,
      lineno: compressed.l,
      colno: compressed.c,
      stack: compressed.s,
      timestamp: compressed.ts,
      url: compressed.u,
      userAgent: compressed.ua
    };
  }
  
  // 缩短文件名
  shortenFilename(filename) {
    if (!filename) return null;
    const url = new URL(filename);
    return url.pathname.split('/').pop();
  }
  
  // 压缩堆栈
  compressStack(stack) {
    if (!stack) return null;
    
    // 只保留前5层堆栈
    const lines = stack.split('\n').slice(0, 5);
    
    // 去除重复信息
    return lines.map(line => {
      return line
        .replace(/https?:\/\/[^/]+/, '')  // 移除域名
        .replace(/\?.+$/, '')              // 移除查询参数
        .trim();
    }).join('\n');
  }
  
  // 压缩UserAgent
  compressUserAgent(ua) {
    if (!ua) return null;
    
    // 提取关键信息
    const match = ua.match(/(Chrome|Firefox|Safari|Edge)\/(\d+)/);
    if (match) {
      return `${match[1]}/${match[2]}`;
    }
    return ua.substring(0, 50);
  }
}

const compressor = new ErrorCompressor();

// 在发送前压缩
function sendCompressedError(error) {
  const compressed = compressor.compress(error);
  
  fetch(dsn, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(compressed)
  });
}

4. 服务端实现

4.1 Express错误接收

javascript
// server.js
const express = require('express');
const app = express();

app.use(express.json());

// 错误接收接口
app.post('/api/errors', async (req, res) => {
  try {
    const error = req.body;
    
    // 验证错误数据
    if (!error.type || !error.message) {
      return res.status(400).json({ error: 'Invalid error data' });
    }
    
    // 增强错误信息
    const enhancedError = {
      ...error,
      receivedAt: new Date(),
      ip: req.ip,
      headers: {
        userAgent: req.get('user-agent'),
        referer: req.get('referer')
      }
    };
    
    // 存储到数据库
    await saveError(enhancedError);
    
    // 检查是否需要告警
    await checkAndAlert(enhancedError);
    
    res.json({ success: true });
  } catch (err) {
    console.error('Failed to process error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 批量接收接口
app.post('/api/errors/batch', async (req, res) => {
  try {
    const { batch } = req.body;
    
    if (!Array.isArray(batch)) {
      return res.status(400).json({ error: 'Invalid batch data' });
    }
    
    // 批量处理
    await Promise.all(batch.map(error => {
      const enhancedError = {
        ...error,
        receivedAt: new Date(),
        ip: req.ip
      };
      return saveError(enhancedError);
    }));
    
    res.json({ success: true, count: batch.length });
  } catch (err) {
    console.error('Failed to process batch:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 存储错误
async function saveError(error) {
  // 使用MongoDB示例
  const db = await getDatabase();
  await db.collection('errors').insertOne(error);
}

// 检查并告警
async function checkAndAlert(error) {
  // 错误数量阈值告警
  const count = await countRecentErrors(error.type, 5 * 60 * 1000);  // 5分钟内
  
  if (count > 10) {
    await sendAlert({
      title: `Error Spike: ${error.type}`,
      message: `${error.type} occurred ${count} times in the last 5 minutes`,
      severity: 'high'
    });
  }
}

app.listen(3000, () => {
  console.log('Error reporting server running on port 3000');
});

4.2 数据库存储

javascript
// MongoDB Schema
const errorSchema = {
  type: String,
  message: String,
  stack: String,
  filename: String,
  lineno: Number,
  colno: Number,
  url: String,
  userAgent: String,
  environment: String,
  release: String,
  timestamp: Date,
  receivedAt: Date,
  userId: String,
  deviceInfo: Object,
  breadcrumbs: Array,
  resolved: Boolean,
  resolvedAt: Date,
  resolvedBy: String,
  fingerprint: String,  // 用于分组
  occurrenceCount: Number
};

// 创建索引
db.collection('errors').createIndex({ fingerprint: 1, timestamp: -1 });
db.collection('errors').createIndex({ type: 1, timestamp: -1 });
db.collection('errors').createIndex({ userId: 1, timestamp: -1 });
db.collection('errors').createIndex({ environment: 1, timestamp: -1 });

// TTL索引,自动删除旧数据
db.collection('errors').createIndex(
  { receivedAt: 1 },
  { expireAfterSeconds: 30 * 24 * 60 * 60 }  // 30天后删除
);

4.3 错误聚合

javascript
// 错误聚合服务
class ErrorAggregator {
  constructor(db) {
    this.db = db;
  }
  
  // 按指纹聚合
  async aggregateByFingerprint(startDate, endDate) {
    return await this.db.collection('errors').aggregate([
      {
        $match: {
          timestamp: { $gte: startDate, $lte: endDate }
        }
      },
      {
        $group: {
          _id: '$fingerprint',
          count: { $sum: 1 },
          firstSeen: { $min: '$timestamp' },
          lastSeen: { $max: '$timestamp' },
          affectedUsers: { $addToSet: '$userId' },
          type: { $first: '$type' },
          message: { $first: '$message' },
          sample: { $first: '$$ROOT' }
        }
      },
      {
        $sort: { count: -1 }
      },
      {
        $limit: 100
      }
    ]).toArray();
  }
  
  // 按时间聚合
  async aggregateByTime(interval = 'hour') {
    const groupFormat = interval === 'hour' 
      ? { $dateToString: { format: '%Y-%m-%d %H:00', date: '$timestamp' } }
      : { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } };
    
    return await this.db.collection('errors').aggregate([
      {
        $group: {
          _id: groupFormat,
          count: { $sum: 1 },
          types: { $addToSet: '$type' }
        }
      },
      {
        $sort: { _id: 1 }
      }
    ]).toArray();
  }
  
  // Top错误
  async getTopErrors(limit = 10) {
    return await this.db.collection('errors').aggregate([
      {
        $match: {
          timestamp: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
        }
      },
      {
        $group: {
          _id: { type: '$type', message: '$message' },
          count: { $sum: 1 },
          sample: { $first: '$$ROOT' }
        }
      },
      {
        $sort: { count: -1 }
      },
      {
        $limit: limit
      }
    ]).toArray();
  }
}

5. SourceMap支持

5.1 SourceMap解析

javascript
// SourceMap解析器
const sourceMap = require('source-map');
const fs = require('fs').promises;

class SourceMapResolver {
  constructor(mapDirectory) {
    this.mapDirectory = mapDirectory;
    this.cache = new Map();
  }
  
  // 解析错误堆栈
  async resolveStack(stack, filename) {
    const lines = stack.split('\n');
    const resolvedLines = [];
    
    for (const line of lines) {
      const match = line.match(/at .+ \((.+):(\d+):(\d+)\)/);
      
      if (match) {
        const [, file, lineStr, colStr] = match;
        const lineNum = parseInt(lineStr);
        const colNum = parseInt(colStr);
        
        try {
          const original = await this.resolve(file, lineNum, colNum);
          resolvedLines.push(
            line.replace(
              `${file}:${lineNum}:${colNum}`,
              `${original.source}:${original.line}:${original.column}`
            )
          );
        } catch (err) {
          resolvedLines.push(line);
        }
      } else {
        resolvedLines.push(line);
      }
    }
    
    return resolvedLines.join('\n');
  }
  
  // 解析单个位置
  async resolve(filename, line, column) {
    const consumer = await this.getConsumer(filename);
    
    const original = consumer.originalPositionFor({
      line,
      column
    });
    
    return {
      source: original.source,
      line: original.line,
      column: original.column,
      name: original.name
    };
  }
  
  // 获取SourceMap Consumer
  async getConsumer(filename) {
    if (this.cache.has(filename)) {
      return this.cache.get(filename);
    }
    
    const mapFile = `${this.mapDirectory}/${filename}.map`;
    const mapContent = await fs.readFile(mapFile, 'utf-8');
    const rawSourceMap = JSON.parse(mapContent);
    
    const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap);
    this.cache.set(filename, consumer);
    
    return consumer;
  }
}

// 使用
const resolver = new SourceMapResolver('./build/static/js');

// 在错误处理中使用
async function processError(error) {
  if (error.stack && error.filename) {
    try {
      error.resolvedStack = await resolver.resolveStack(error.stack, error.filename);
    } catch (err) {
      console.error('Failed to resolve source map:', err);
    }
  }
  
  await saveError(error);
}

6. 告警通知

6.1 告警规则引擎

javascript
class AlertEngine {
  constructor() {
    this.rules = [];
    this.alertHistory = new Map();
  }
  
  // 添加规则
  addRule(rule) {
    this.rules.push(rule);
  }
  
  // 检查是否应该告警
  async shouldAlert(error, stats) {
    for (const rule of this.rules) {
      if (await rule.match(error, stats)) {
        // 检查冷却期
        const lastAlert = this.alertHistory.get(rule.id);
        if (lastAlert && Date.now() - lastAlert < rule.cooldown) {
          continue;
        }
        
        // 发送告警
        await rule.alert(error, stats);
        this.alertHistory.set(rule.id, Date.now());
      }
    }
  }
}

// 定义告警规则
const alertEngine = new AlertEngine();

// 规则1: 错误数量激增
alertEngine.addRule({
  id: 'error-spike',
  cooldown: 5 * 60 * 1000,  // 5分钟冷却
  match: async (error, stats) => {
    const count = stats.last5Minutes;
    return count > 50;
  },
  alert: async (error, stats) => {
    await sendDingTalkAlert({
      title: '错误数量激增',
      text: `过去5分钟内发生${stats.last5Minutes}个错误`,
      level: 'critical'
    });
  }
});

// 规则2: 新错误类型
alertEngine.addRule({
  id: 'new-error-type',
  cooldown: 10 * 60 * 1000,
  match: async (error) => {
    const count = await countErrorsByFingerprint(error.fingerprint);
    return count === 1;  // 首次出现
  },
  alert: async (error) => {
    await sendEmailAlert({
      subject: '发现新错误类型',
      body: `错误类型: ${error.type}\n错误消息: ${error.message}`,
      level: 'warning'
    });
  }
});

// 规则3: 影响用户数过多
alertEngine.addRule({
  id: 'affected-users',
  cooldown: 15 * 60 * 1000,
  match: async (error, stats) => {
    return stats.affectedUsers > 100;
  },
  alert: async (error, stats) => {
    await sendSlackAlert({
      channel: '#alerts',
      text: `错误影响了${stats.affectedUsers}个用户`,
      level: 'high'
    });
  }
});

6.2 通知渠道

javascript
// 钉钉通知
async function sendDingTalkAlert(alert) {
  const webhook = process.env.DINGTALK_WEBHOOK;
  
  await fetch(webhook, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      msgtype: 'markdown',
      markdown: {
        title: alert.title,
        text: `### ${alert.title}\n\n${alert.text}\n\n**级别**: ${alert.level}`
      }
    })
  });
}

// 邮件通知
const nodemailer = require('nodemailer');

async function sendEmailAlert(alert) {
  const transporter = nodemailer.createTransporter({
    host: process.env.SMTP_HOST,
    port: process.env.SMTP_PORT,
    secure: true,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS
    }
  });
  
  await transporter.sendMail({
    from: 'alerts@yourapp.com',
    to: 'team@yourapp.com',
    subject: alert.subject,
    text: alert.body
  });
}

// Slack通知
async function sendSlackAlert(alert) {
  const webhook = process.env.SLACK_WEBHOOK;
  
  await fetch(webhook, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      channel: alert.channel,
      text: alert.text,
      attachments: [{
        color: alert.level === 'critical' ? 'danger' : 'warning',
        fields: [{
          title: 'Level',
          value: alert.level,
          short: true
        }]
      }]
    })
  });
}

7. 总结

构建完整的错误上报系统需要:

  1. 客户端: 全局错误捕获、React集成、API拦截
  2. 优化: 去重、采样、压缩
  3. 服务端: 接收、存储、聚合
  4. SourceMap: 堆栈还原
  5. 告警: 规则引擎、多渠道通知

完善的错误上报系统是保障应用质量的重要基础设施。