前端错误监控生产级方案:从 onerror 捕获到 Source Map 精准还原

前端错误监控生产级方案:从 onerror 捕获到 Source Map 精准还原

线上一报错,控制台却只有一行压缩后的 app.4f3a.js:1:1234?这种"能看见错误、却定位不到代码"的体验,几乎每个前端都经历过。

本文带你从 0 搭一套可落地的错误监控链路:浏览器端兜底捕获 → 无阻塞上报 → Node 服务接收 → Source Map 把压缩报错还原成源码行号。所有代码均可直接复制使用。


一、捕获:把错误"接住"

全局错误主要有三类:JS 运行时错误未处理的 Promise 拒绝资源加载失败(图片/脚本 404)。

js 复制代码
// error-capture.js
const REPORT_URL = '/api/error';

function report(payload) {
  const body = JSON.stringify(payload);
  // sendBeacon 不阻塞主线程,页面卸载时也能发出
  if (navigator.sendBeacon) {
    navigator.sendBeacon(REPORT_URL, new Blob([body], { type: 'application/json' }));
  } else {
    fetch(REPORT_URL, { method: 'POST', body, keepalive: true });
  }
}

// 1. JS 运行时错误 + 资源加载错误(需 useCapture=true)
window.addEventListener('error', (e) => {
  // 资源错误没有 error 对象,信息挂在 target 上
  if (e.target && (e.target.src || e.target.href)) {
    report({ type: 'resource', tag: e.target.tagName, url: e.target.src || e.target.href });
    return;
  }
  report({
    type: 'js',
    message: e.message,
    stack: e.error?.stack,
    filename: e.filename,
    lineno: e.lineno,
    colno: e.colno,
  });
}, true);

// 2. Promise 未捕获的 rejection
window.addEventListener('unhandledrejection', (e) => {
  const reason = e.reason;
  report({
    type: 'promise',
    message: reason?.message || String(reason),
    stack: reason?.stack,
  });
});

注意:error 事件必须在捕获阶段useCapture=true)监听,才能拿到 <img><script> 这类资源的加载失败------它们的错误不会冒泡。


二、上报:别让上报本身拖慢页面

navigator.sendBeacon 是最稳的做法:它在浏览器空闲时异步发送,不抢主线程,且在 beforeunload、页面跳转时也能成功送达。降级到 fetch(..., { keepalive: true }) 是同样思路的兜底。

建议额外采集这些上下文,排查时非常有用:

js 复制代码
function enrich() {
  return {
    ua: navigator.userAgent,
    url: location.href,
    ts: Date.now(),
    uid: localStorage.getItem('uid') || 'anonymous',
  };
}
// report({ ...enrich(), type: 'js', message: e.message, stack: e.error?.stack });

三、接收:一个最小 Node 服务

js 复制代码
// server.js
import express from 'express';
import { mkdirSync, existsSync, readFile } from 'node:fs';
import { join } from 'node:path';
import { SourceMapConsumer } from 'source-map';

const app = express();
app.use(express.json({ limit: '1mb' }));

const MAP_DIR = join(process.cwd(), 'sourcemaps');
mkdirSync(MAP_DIR, { recursive: true });

// 接收前端上报
app.post('/api/error', (req, res) => {
  console.log('[ERROR]', new Date().toISOString(), req.body);
  res.sendStatus(204);
});

// 接收构建产物生成的 sourcemap(生产环境务必加鉴权/内网)
app.post('/api/sourcemap', (req, res) => {
  const { file, content } = req.body;
  // 写入 MAP_DIR,供后续还原使用
  import('node:fs/promises').then(({ writeFile }) =>
    writeFile(join(MAP_DIR, file), content)
  );
  res.sendStatus(204);
});

// 还原接口:压缩行列号 → 源码位置
app.post('/api/resolve', async (req, res) => {
  const { file, line, column } = req.body; // 如 app.4f3a.js
  const mapPath = join(MAP_DIR, file + '.map');
  if (!existsSync(mapPath)) return res.json(req.body);
  const raw = await readFile(mapPath, 'utf8');
  const consumer = await new SourceMapConsumer(raw);
  // Source Map 列号从 0 开始,错误事件给的是 1-based,需 -1
  const original = consumer.originalPositionFor({ line, column: column - 1 });
  consumer.destroy();
  res.json({ ...req.body, original });
});

app.listen(3000, () => console.log('monitor server on :3000'));

四、还原:让 app.4f3a.js:1:1234 变回 src/utils/request.ts:42

关键点就两个:构建时生成 Source Map ,以及把它安全地上传给监控服务

js 复制代码
// vite.config.js
export default {
  build: {
    sourcemap: true,            // 产出 .map 文件
    sourcemapExcludeSources: false,
  },
};

构建后把 .map 推到服务端(CI 里跑,不要打进用户包):

js 复制代码
// upload-sourcemap.mjs
import { glob } from 'glob';
import { readFile } from 'node:fs/promises';

const files = await glob('dist/assets/**/*.map');
for (const f of files) {
  await fetch('http://localhost:3000/api/sourcemap', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      file: f.split('/').pop(),
      content: await readFile(f, 'utf8'),
    }),
  });
  console.log('uploaded', f);
}

之后只要把线上报错里的 line/column 丢给 /api/resolve,就能拿到 source / line / column / name------直接对应你写的源码位置。


五、上线前必看的避坑清单

  1. Source Map 不要随业务包下发:放进内网/对象存储,靠后端接口按需读取,否则等于把源码免费送人。
  2. 列号偏移originalPositionFor 的列是 0-based,错误事件是 1-based,记得减 1。
  3. 跨域脚本<script crossorigin> 必须加,否则拿不到第三方脚本的错误堆栈。
  4. 去重与采样:相同堆栈合并计数,高频报错做采样,避免把服务端打挂。
  5. 敏感信息 :上报前脱敏 url 里的 token、表单里的密码字段。

小结

一条完整的链路是:window.onerror / unhandledrejection 兜底 → sendBeacon 无阻塞上报 → Node 接收落库 → 构建期 Source Map 上传 → source-map 还原行号

搭好这套之后,线上报错不再是"一行看不懂的压缩代码",而是精确的源码位置,平均定位时间能从几十分钟降到几秒。

如果你在用 Sentry / Fundebug 这类平台,底层也是这套机制------理解了原理,配置和排障都会顺手很多。

相关推荐
渣波1 小时前
从零手写 Vite Mock 流式 AI 对话,搞懂 LLM 的“打字机”特效
前端·javascript
温柔过期啦1 小时前
网站改了标题但是搜索引擎却没改?怎么样才能快速更新?
前端·搜索引擎·百度
cxxcode1 小时前
JavaScript 模块化:从模块规范到打包原理
前端·webpack
驰子1051 小时前
不重写、不换框架:让十年 jQuery 老项目写出 Vue 手感的渐进式改造
前端
月弦笙音1 小时前
# SSE 连接可靠性方案
前端
weedsfly1 小时前
从表单校验到订单流转,彻底搞懂前端开发中的策略模式与状态模式
前端·javascript·面试
qq_589666051 小时前
npm install npm install -g @vue/cli 分别是什么意思以及安装路径
前端·vue.js·npm
用户0207199207722 小时前
Kafka 与事务 Outbox
前端
IT_陈寒2 小时前
Vite的静态资源路径搞得我头秃,原来这样配才对
前端·人工智能·后端