线上 JS 报错压缩后只剩一行,SourceMap 又没上传。本文记录如何从零搭建前端错误监控体系,配合 AI 反向定位源码,把此类问题的平均排查时间从数小时降到 10 分钟。
凌晨两点,用户群炸了
“支付页面白屏,点不了。” “我也是,刷新好多次了。” “安卓和 iPhone 都不行。”
我被运营的电话叫醒,迷迷糊糊打开电脑。生产环境没有报错监控,我只好登录服务器,在 Nginx 日志里找到一条请求记录,然后凭着直觉去翻前端代码。直到早上六点,我终于在压缩后的 bundle 里找到了一行代码:
// 压缩混淆后的代码,从线上 Chunk 中扒出来的
var t = n.data.paymentInfo; t.getAmount()
t 是个 undefined,调用 getAmount 报错。为什么是 undefined?不知道。后端接口返回的数据有时确实缺少这个字段,但理论上不应该。
最后发现,是当天下午后端发版,修改了支付接口的返回字段名,paymentInfo 改成了 payment_info。没有通知前端,没有更新接口文档,没有做兼容。前端代码里 3 个地方引用了旧字段,其中 2 个在发布前被检查出来,第 3 个藏在一个很少触发的优惠券计算逻辑里,只有在特定组合下才会执行。测试环境的数据刚好没触发这个组合。
从用户反馈到定位根因,花了将近 4 个小时。其中 3 个小时花在“猜原因”和“找代码”上。
这次之后,我花了一周把前端监控体系彻底搭了起来。现在的排查流程:告警 → 点击链接 → 看到源码级错误堆栈 + 用户操作回放 → 10 分钟内定位。这篇文章是完整的搭建记录。
第一步:梳理前端监控体系需要哪些能力
复盘那次事故,理想的排查体验应该满足:
- 报错自动收集:不用等用户反馈,系统自动感知线上错误。
- 源码映射:压缩后的报错能直接定位到原始文件的行号。
- 上下文信息:报错时用户的页面 URL、操作、设备、网络状态。
- 版本关联:能知道是哪个版本的代码引入的。
- 操作回放:能复现用户的操作路径(可选但很管用)。
我评估了三个方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Sentry (SaaS) | 开箱即用,功能齐全,免费额度够小团队用 | 数据存储在第三方,有隐私顾虑 |
| Sentry (自部署) | 数据可控,功能相同 | 需要维护服务器和数据库 |
| 自研 (window.onerror + 上报) | 完全可控,无成本 | 需要自己处理 SourceMap、聚合、通知、可视化 |
最终我选了自部署 Sentry。原因是:我们团队对数据隐私有要求,同时不想从零造轮子。Sentry 自部署版开源,功能完整,社区活跃,踩坑有人帮。
第二步:部署 Sentry 并接入前端项目
部署 Sentry (Docker 方式)
Sentry 官方提供了一键部署脚本。一台 2C4G 的轻量服务器就能跑。
# 克隆自部署仓库
git clone https://github.com/getsentry/self-hosted.git
cd self-hosted
# 安装(会拉取所有依赖镜像)
./install.sh
# 启动
docker compose up -d
启动后访问 http://your-server:9000,创建项目和团队,获取 DSN(Data Source Name)。
踩坑一:install.sh 在执行时要求至少 4GB 内存,我的测试服务器只有 2GB,脚本直接退出。临时加了 swap 解决:
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
踩坑二:默认的 Kafka 和 ClickHouse 吃内存厉害。如果是小团队(<10 人),可以在 docker-compose.yml 中限制这些服务的资源,或者直接用 Sentry 的 SaaS 免费版。我最终把 Kafka 的堆内存限制从 1G 降到 512M。
前端接入 Sentry SDK
npm install @sentry/react
在入口文件初始化:
// src/index.tsx
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
Sentry.init({
dsn: 'https://xxx@sentry.example.com/1',
integrations: [
new BrowserTracing({
tracePropagationTargets: ['localhost', 'api.example.com'],
}),
],
// 采样率:生产环境建议 0.1-0.5,避免海量上报
tracesSampleRate: 0.1,
// 只上报错误,不上报普通日志
beforeSend(event) {
if (event.level === 'error') return event;
return null;
},
});
接入后,Sentry 自动捕获未处理的异常和 Promise 拒绝。对于主动 try-catch 的业务错误,手动上报:
try {
await submitOrder(data);
} catch (error) {
Sentry.captureException(error, {
tags: { module: 'order' },
extra: { orderId: data.orderId },
});
// 继续业务降级逻辑
}
到这里,线上报错能自动收集了。但报错信息长这样:
TypeError: undefined is not an object (evaluating 't.getAmount')
at n (chunk-abc123.js:1:2345)
at onClick (chunk-abc123.js:1:5678)
还是没法定位源码。因为生产打包后的代码经过了压缩混淆,堆栈里的行号是 bundle 里的位置。
第三步:上传 SourceMap,让报错堆栈还原源码
这是整个流程中最关键也最容易出错的环节。
Webpack 配置生成 SourceMap
// webpack.prod.js
module.exports = {
devtool: 'hidden-source-map', // 生成 map 但不给 bundle 添加引用,防止浏览器直接下载
output: {
sourceMapFilename: 'sourcemaps/[name].[contenthash].js.map',
},
plugins: [
new SentryWebpackPlugin({
org: 'my-org',
project: 'my-app',
authToken: process.env.SENTRY_AUTH_TOKEN,
release: process.env.RELEASE_VERSION,
include: './dist',
ignore: ['node_modules'],
urlPrefix: '~/', // 对应 CDN 路径前缀
validate: true, // 上传后验证 map 是否有效
}),
],
};
关键点:
devtool: 'hidden-source-map':生产环境必须用这个,既能生成 SourceMap 又不会暴露源码。如果用source-map,浏览器控制台可以直接看到原始代码,安全风险极大。release:每次构建的版本号必须唯一。我用了git rev-parse --short HEAD+ 日期。urlPrefix:如果 JS 文件部署在 CDN 上(如https://cdn.example.com/static/js/main.123.js),这里要填~/或完整的 CDN 地址。Sentry 会拿着报错里的文件 URL 去匹配这个前缀。
CI 集成
# .github/workflows/deploy.yml
jobs:
deploy:
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
env:
RELEASE_VERSION: ${{ github.sha }}-${{ github.run_number }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- run: npx sentry-cli releases new $RELEASE_VERSION
- run: npx sentry-cli releases files $RELEASE_VERSION upload-sourcemaps ./dist
- run: npx sentry-cli releases finalize $RELEASE_VERSION
# 部署到 CDN...
踩坑三:SourceMap 文件必须和对应的 JS 文件同名匹配。我一开始用了 hidden-source-map 但忘了配置 sourceMapFilename,生成的 map 文件名和 JS 不匹配,Sentry 匹配不到。排查了半天才发现是名字里缺了 contenthash。
踩坑四:忘记把 .map 文件从 CDN 部署目录中剔除。用 hidden-source-map 生成的 map 虽然在 bundle 中没有引用,但如果 map 文件也上传到了公开 CDN,用户可以通过 Chrome DevTools 直接加载。我写了一个简单的构建后脚本,在 CDN 上传前移动所有 .map 文件到一个私有目录,只传给 Sentry。
验证 SourceMap 是否生效
部署后,故意在代码里写一个 throw new Error('test') 触发上报。在 Sentry Issue 详情页看到堆栈:
TypeError: test
at getAmount (src/utils/payment.ts:42:15)
at calculateDiscount (src/components/DiscountCoupon.tsx:88:10)
源码文件名 + 行号都精确还原了。到这里,线上报错的定位速度已经上了一个台阶。
第四步:用 AI 辅助分析报错堆栈
Sentry 解决了收集和还原,但分析仍需人力。对于一个复杂的报错,可能涉及多个文件、多个函数调用,关联到后端接口返回数据。我试了把报错堆栈粘贴给 AI,发现它能大幅加速分析。
提示词模板:
你是一位资深前端工程师。以下是一个生产环境的报错信息,包含完整堆栈和上下文数据。
## 错误信息
TypeError: Cannot read properties of undefined (reading 'getAmount')
at getAmount (src/utils/payment.ts:42:15)
at calculateDiscount (src/components/DiscountCoupon.tsx:88:10)
at onClick (src/pages/Checkout.tsx:156:5)
## 用户操作上下文
- 页面: /checkout
- 设备: iPhone 15, iOS 18
- 网络: 4G
- 用户操作: 点击“使用优惠券”按钮
## 额外数据
- orderData: { total: 99.9, items: [...], discount: null }
注意: paymentInfo 字段不存在
请分析:
1. 这个错误最可能的根本原因是什么?
2. 为什么 paymentInfo 可能缺失?
3. 给出最小修复方案(代码),包含防御性判断。
4. 如何避免类似问题?
AI 的输出通常很精准:
- 根本原因:
orderData中缺少paymentInfo字段,getAmount未做空值保护。 - 为什么缺失:后端接口可能在某些订单状态下不返回
paymentInfo,或者字段名变更。 - 修复方案:在
getAmount函数入口加可选链orderData?.paymentInfo?.getAmount?.()或者在上层调用处保证paymentInfo存在。 - 避免方法:前后端字段变更需要走接口契约测试;前端对后端返回数据做 schema 校验。
我把 AI 的分析直接贴到 Sentry Issue 的评论里,作为排查笔记。 后来团队其他人遇到类似报错时,先看评论就能快速了解历史处理方式。
第五步:配置告警规则,不用半夜被叫醒
有了错误收集和源码分析,下一个痛点是:怎么知道什么时候该看、什么时候可以睡?
Sentry 支持自定义告警规则。我设置了几个关键指标:
# Sentry 告警配置(Web UI 操作或用 Terraform 管理)
Alerts:
- name: "关键流程错误激增"
conditions:
- event.type: error
- tags.module: [payment, order, login]
- count: > 50 in 1h
actions:
- slack: "#frontend-alerts"
- pagerduty: critical
- name: "新错误首次出现"
conditions:
- event.type: error
- issue.age: < 1h
- event.count: > 3
actions:
- slack: "#frontend-alerts"
- name: "低频但不该出现的错误"
conditions:
- event.type: error
- tags.severity: high
- count: > 1
actions:
- email: frontend-team@example.com
核心原则:
- 高频且关键流程的错误(支付、订单)必须立刻通知。
- 新错误头三次出现时通知,可能是新发布引入的。
- 低频但不该发生的错误(如白屏、JS 加载失败)每次都要知道。
不要把所有的 console.error 都上报,否则告警噪音会让人麻木。我只上报了:
- 未捕获的异常(
onerror) - 未处理的 Promise 拒绝(
onunhandledrejection) - 主动调用的
Sentry.captureException(用于关键业务 try-catch 内)
第六步:关联版本和 Git Commit
Sentry 支持将 release 和 Git commit 关联。这样当一个错误出现时,可以直接看到是哪个 commit 引入的,作者是谁。
# 在 CI 中关联 commit
npx sentry-cli releases set-commits $RELEASE_VERSION --auto
--auto 参数会自动读取当前 Git 仓库的 commit 信息,关联到 Sentry Release。
配置完后,Sentry Issue 详情页会显示:
Release: abc123-42
Commits: 3 (by @zhangsan)
First seen: 2026-05-26 14:32:15
点击 commit 直接跳转到 GitLab,看到代码 diff。这比手动去翻部署记录快太多了。
第七步:可选——用户操作回放
定位到错误后,如果还是难以复现,Sentry 的 Session Replay 功能可以回放用户的操作过程。这能解决“这个按钮只有用户 A 在特定步骤点击时才会报错”的谜题。
import { Replay } from '@sentry/replay';
Sentry.init({
integrations: [
new Replay({
maskAllText: true, // 脱敏用户输入
blockAllMedia: true, // 不录制媒体内容
maskAllInputs: true, // 脱敏表单输入
}),
],
replaysSessionSampleRate: 0.01, // 1% 的会话录制回放
replaysOnErrorSampleRate: 1.0, // 报错时 100% 录制
});
隐私合规:必须开启脱敏选项,并在隐私政策中告知用户。我们法务审核后要求默认关闭,只在用户同意的情况下开启,这是一个权衡。
这套体系的最终效果
接入三个月后,数据如下:
| 指标 | 之前 | 之后 |
|---|---|---|
| 错误发现时间 | 用户反馈(平均 2 小时) | 自动告警(<5 分钟) |
| 错误定位时间 | 平均 2 小时(翻源码、查日志) | 平均 10 分钟(堆栈 + 上下文 + AI 分析) |
| 未复现错误比例 | 约 40% | <5%(有操作回放) |
| 同类型错误重复率 | 高(修复后无追踪) | 低(Release 对比) |
最让我满意的不是数字,而是一种“掌控感”。以前面对用户反馈的“页面点不了”,我总是心里没底——不知道是偶发还是批量,不知道影响多少人,不知道什么时候开始。现在打开 Sentry Dashboard,所有信息一目了然。
踩过的坑(完整版)
-
SourceMap 公开泄露:必须用
hidden-source-map,并把.map文件移到非公开路径。验证方法:部署后访问https://cdn.example.com/static/js/main.xxx.js.map,应该返回 404。 -
Sentry 自部署的内存占用:Kafka + ClickHouse 是小内存服务器的噩梦。如果团队 <5 人,直接用 Sentry SaaS 免费版更省心(每月 5000 errors 免费)。
-
跨域脚本错误:CDN 上的 JS 如果报了错,浏览器会因为跨域限制只给
Script error,没有堆栈。必须给 script 标签加crossorigin="anonymous",CDN 侧配Access-Control-Allow-Origin。 -
不发送重复错误:如果同一个错误在循环里触发,可能瞬间发几十万条,打爆 Sentry 额度。在初始化时设置
beforeSend过滤,或者用 Sentry 的去重逻辑(默认基于堆栈聚合)。 -
性能影响:Sentry SDK 体积约 20KB gzip,对性能影响很小。但 Session Replay 会录屏,对低端机有性能开销,采样率不要设太高。
快速搭建检查清单
## 前端错误监控搭建清单
### 基础接入
- [ ] 部署 Sentry(自部署或 SaaS)
- [ ] 前端接入 @sentry/react SDK
- [ ] 配置 error / unhandledrejection 自动捕获
- [ ] 关键业务逻辑手动 Sentry.captureException
### SourceMap
- [ ] Webpack/Vite 配置 hidden-source-map
- [ ] CI 中上传 .map 文件到 Sentry
- [ ] 验证线上报错是否显示源码堆栈
- [ ] 确认 .map 文件未公开
### 告警
- [ ] 关键流程错误激增 → 即时通知
- [ ] 新错误首次出现 → 即时通知
- [ ] 低频高危错误 → 邮件通知
- [ ] 告警测试,确认能收到
### 版本关联
- [ ] 每次构建生成唯一 release 号
- [ ] 关联 Git commit
- [ ] Release 页面验证 commit 列表
### 隐私合规
- [ ] 脱敏用户数据
- [ ] 隐私政策告知
- [ ] 用户可关闭错误上报
最后
那天的凌晨四点,我对着压缩后的一行 JS 发呆的感觉,现在还记得很清楚。错误的排查成本完全取决于你投入了多少监控基础设施。省掉监控的时间,迟早会在排查时加倍还回来。
如果你现在的项目还没有前端错误监控,从 Sentry 免费版开始,花一个小时接入,下一个线上报错你就能在几分钟内找到原因。
你的项目用的是什么错误监控方案?有没有遇到过特别离谱的线上问题?欢迎评论区聊聊。