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

一、错误分类与捕获方式

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

二、各类型错误捕获实现

2.1 运行时错误:window.onerror

javascript 复制代码
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

javascript 复制代码
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 事件(捕获阶段)

javascript 复制代码
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

javascript 复制代码
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; // 继续抛出,不影响业务逻辑
  });
};

三、上报函数设计

javascript 复制代码
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 封装

javascript 复制代码
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: 'your-api/monitor',
  appId: 'your-app-id',
  maxQueueSize: 10,
  flushInterval: 5000,
});

四、Source Map 还原堆栈

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

  1. 构建时生成 .map 文件,上传到内部服务器(不要上传到 CDN)
  2. 服务端使用 source-map 库还原
javascript 复制代码
// 服务端 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 可直接复制,接入后即可开始收集线上错误
相关推荐
meilindehuzi_a4 小时前
从零理解并实现 RAG:用 LangChain.js 构建语义检索问答系统
开发语言·javascript·langchain
世界哪有真情5 小时前
拿人类意识卡 AI?等于用 bug 验收正式产品
前端·人工智能·后端
鱼樱前端5 小时前
别再自己拼凑了!这款 Vue 3.5+ 严肃产品级组件库,把 AI 对话、工作流和复杂数据全包了!
javascript·vue.js·github
Listen·Rain6 小时前
Vue3:setup详解
前端·javascript·vue.js
Patrick_Wilson7 小时前
修好 bug 只是开始:一次由监控需求反向重构日志结构的复盘
前端·监控·数据可视化
kyriewen7 小时前
面试官让我优化一个卡死的表格,我打开 Claude 五秒重写,他放下咖啡问我要不要来当组长
前端·javascript·面试
小粉粉hhh8 小时前
React(二)——dom、组件间通信、useEffect
前端·javascript·react.js
Csvn8 小时前
🤖 AI 生成前端代码的 5 个典型翻车现场及修复指南
前端
IT_陈寒9 小时前
SpringBoot自动装配坑了我一天,原来问题出在这
前端·人工智能·后端
索西引擎10 小时前
【React】key 属性:协调算法中的元素标识机制与最佳实践
前端·javascript·react.js