聊天讨论 前端错误监控最全指南:捕获 JS 异常、Promise 拒绝、资源加载失败,附上报代码

193577746(kyriewen) · 2026年06月17日 · 9 次阅读

一、错误分类与捕获方式

错误类型 捕获方式 备注
JS 运行时错误 window.onerror 同步代码、未捕获的异常
Promise 拒绝 unhandledrejection async/await 未 catch
资源加载失败 error 事件(捕获阶段) 图片、脚本、样式加载失败
语法错误 error 事件 + try-catch 在捕获阶段可捕获
接口异常 拦截 XMLHttpRequest / fetch 需额外封装

二、各类型错误捕获实现

2.1 运行时错误:window.onerror

window.onerror = function(message, source, lineno, colno, error) {
  const report = {
    type: 'js_error',
    message,
    source,
    lineno,
    colno,
    stack: error?.stack || '',
    userAgent: navigator.userAgent,
    url: location.href,
    timestamp: Date.now(),
  };
  sendReport(report);
  return true; // 阻止默认行为
};

2.2 Promise 拒绝:unhandledrejection

window.addEventListener('unhandledrejection', (event) => {
  const report = {
    type: 'promise_rejection',
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack || '',
    userAgent: navigator.userAgent,
    url: location.href,
    timestamp: Date.now(),
  };
  sendReport(report);
});

2.3 资源加载失败:error 事件(捕获阶段)

window.addEventListener('error', (event) => {
  const target = event.target;
  if (target && (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
    const report = {
      type: 'resource_error',
      tag: target.tagName,
      src: target.src || target.href || '',
      userAgent: navigator.userAgent,
      url: location.href,
      timestamp: Date.now(),
    };
    sendReport(report);
  }
}, true); // 必须使用捕获阶段

2.4 接口异常:拦截 fetch

const originalFetch = window.fetch;
window.fetch = function(...args) {
  return originalFetch.apply(this, args).catch((error) => {
    const report = {
      type: 'fetch_error',
      url: args[0],
      method: args[1]?.method || 'GET',
      message: error.message,
      stack: error.stack,
      timestamp: Date.now(),
    };
    sendReport(report);
    throw error; // 继续抛出,不影响业务逻辑
  });
};

三、上报函数设计

function sendReport(data) {
  // 批量上报:累积到一定数量或时间再发送
  const queue = [];
  queue.push(data);

  if (queue.length >= 10) {
    flush(queue);
  }
}

function flush(queue) {
  const data = queue.splice(0, queue.length);
  const body = JSON.stringify(data);

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/monitor', body);
  } else {
    // 降级:使用 fetch(keepalive)
    fetch('/api/monitor', {
      method: 'POST',
      body,
      headers: { 'Content-Type': 'application/json' },
      keepalive: true,
    }).catch(() => {});
  }
}

3.2 完整 SDK 封装

class FrontendMonitor {
  constructor(options) {
    this.endpoint = options.endpoint || '/api/monitor';
    this.queue = [];
    this.maxQueueSize = options.maxQueueSize || 10;
    this.flushInterval = options.flushInterval || 5000;
    this.appId = options.appId || 'default';
    this.enabled = options.enabled !== false;

    if (this.enabled) {
      this.init();
    }
  }

  init() {
    this.initJSMonitor();
    this.initPromiseMonitor();
    this.initResourceMonitor();
    this.initFetchMonitor();
    this.setupFlushTimer();
    this.handleBeforeUnload();
  }

  initJSMonitor() {
    window.onerror = (message, source, lineno, colno, error) => {
      this.report({
        type: 'js_error',
        message,
        source,
        lineno,
        colno,
        stack: error?.stack || '',
        appId: this.appId,
      });
      return true;
    };
  }

  initPromiseMonitor() {
    window.addEventListener('unhandledrejection', (event) => {
      this.report({
        type: 'promise_rejection',
        message: event.reason?.message || String(event.reason),
        stack: event.reason?.stack || '',
        appId: this.appId,
      });
    });
  }

  initResourceMonitor() {
    window.addEventListener('error', (event) => {
      const target = event.target;
      if (target && ['IMG', 'SCRIPT', 'LINK'].includes(target.tagName)) {
        this.report({
          type: 'resource_error',
          tag: target.tagName,
          src: target.src || target.href || '',
          appId: this.appId,
        });
      }
    }, true);
  }

  initFetchMonitor() {
    const originalFetch = window.fetch;
    window.fetch = (...args) => {
      return originalFetch.apply(this, args).catch((error) => {
        this.report({
          type: 'fetch_error',
          url: args[0],
          method: args[1]?.method || 'GET',
          message: error.message,
          appId: this.appId,
        });
        throw error;
      });
    };
  }

  report(data) {
    const payload = {
      ...data,
      userAgent: navigator.userAgent,
      url: location.href,
      timestamp: Date.now(),
    };
    this.queue.push(payload);
    if (this.queue.length >= this.maxQueueSize) {
      this.flush();
    }
  }

  flush() {
    if (this.queue.length === 0) return;
    const data = this.queue.splice(0, this.queue.length);
    const body = JSON.stringify(data);

    if (navigator.sendBeacon) {
      navigator.sendBeacon(this.endpoint, body);
    } else {
      fetch(this.endpoint, {
        method: 'POST',
        body,
        headers: { 'Content-Type': 'application/json' },
        keepalive: true,
      }).catch(() => {});
    }
  }

  setupFlushTimer() {
    setInterval(() => this.flush(), this.flushInterval);
  }

  handleBeforeUnload() {
    window.addEventListener('beforeunload', () => this.flush());
  }
}

// 使用方式
const monitor = new FrontendMonitor({
  endpoint: 'https://your-api.com/monitor',
  appId: 'your-app-id',
  maxQueueSize: 10,
  flushInterval: 5000,
});

四、Source Map 还原堆栈

线上代码经过压缩混淆,堆栈信息无法直接定位。需要上传 Source Map 并在服务端还原:

  1. 构建时生成 .map 文件,上传到内部服务器(不要上传到 CDN)
  2. 服务端使用 source-map 库还原
// 服务端 Node.js
import { SourceMapConsumer } from 'source-map';

async function parseStack(stack, mapFile) {
  const consumer = await new SourceMapConsumer(mapFile);
  const parsed = stack.map(frame => {
    const original = consumer.originalPositionFor({
      line: frame.line,
      column: frame.column,
    });
    return original;
  });
  return parsed;
}

五、总结

  • 4 类错误全覆盖:JS 错误、Promise 拒绝、资源加载失败、接口异常
  • sendBeacon + 批量上报:不阻塞页面、不影响用户体验
  • Source Map 还原:线上压缩代码也能定位源码位置
  • 文中的 SDK 可直接复制,接入后即可开始收集线上错误
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号