Sentry 错误上报接入与实践
先说清楚一件事:当前这个仓库是文档站,代码里还没有直接安装 @sentry/* 相关依赖,也没有现成的初始化文件或构建上传脚本。
这篇文章写的是一套前端项目里常见的接法。重点放在四件事上:
- 项目里通常怎么接 Sentry
- source map 怎么处理
- 是否开启录屏,以及怎么开
- 业务里怎么做统一上报封装
如果你们项目是 Next.js、React 或 Vite,这套思路基本都能直接用。
先把接入目标说清楚
Sentry 真正解决的不是“日志多一个平台可看”,而是线上出了问题之后,能不能尽快回答这几个问题:
- 哪个页面报的
- 哪个用户遇到的
- 当时的环 境是什么
- 代码堆栈能不能还原到源码
- 这是不是一批同类问题
所以接入时至少要把这几层接完整:
- SDK 初始化
- release / environment 标记
- source map 上传
- 业务统一上报层
- 可选的 Replay、用户信息、tag 和上下文
一、项目里的接入配置
1. 基础依赖
前端项目里常见的依赖大致是这些:
pnpm add @sentry/browser
pnpm add @sentry/react
pnpm add -D @sentry/cli
如果是 Next.js,通常会直接用:
pnpm add @sentry/nextjs
如果是 Vite 项目,运行时还是 @sentry/react 或 @sentry/browser,构建期再配 @sentry/cli 或对应插件。
2. 常用环境变量
这几项基本是最常见的一组:
SENTRY_DSN=
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_RELEASE=
SENTRY_ENVIRONMENT=
它们分别负责:
SENTRY_DSN:前端 SDK 把事件发到哪里SENTRY_AUTH_TOKEN:构建时上传 source map 用SENTRY_ORG:Sentry 组织名SENTRY_PROJECT:Sentry 项目名SENTRY_RELEASE:这次发布的版本号SENTRY_ENVIRONMENT:环境标记,比如development、staging、production
项目里最好把 release 固定成能追踪构建的值,比如 Git commit SHA、CI build number 或发版号。这样线上报错能直接对上产物版本。
3. 初始化示例
React 项目里,一般会在入口文件做初始化:
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
release: import.meta.env.VITE_APP_RELEASE,
enabled: import.meta.env.PROD,
tracesSampleRate: 0.2,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 1,
integrations: [],
beforeSend(event, hint) {
if (import.meta.env.DEV) {
return null;
}
return event;
},
});
这里有几个点最好一开始就定下来:
enabled不要默认所有环境都开environment和release要一起带上beforeSend不要空着,后面一般会放脱敏和过滤逻辑- 采样率不要拍脑袋写成
1
4. Next.js 常见接法
如果是 Next.js,一般会把初始化拆到客户端和服务端配置里,再在构建时打开 source map 上传。
next.config.js 常见写法大致像这样:
const { withSentryConfig } = require("@sentry/nextjs");
const nextConfig = {
productionBrowserSourceMaps: true,
};
module.exports = withSentryConfig(nextConfig, {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
release: process.env.SENTRY_RELEASE,
silent: true,
widenClientFileUpload: true,
hideSourceMaps: true,
disableLogger: true,
});
几个容易忽略的点:
productionBrowserSourceMaps: true只是让构建产出 source map,不等于已经上传到 SentryauthToken、org、project少一个,上传就会失败hideSourceMaps: true可以避免浏览器直接暴露 source map 路径
二、source map 这一层到底要怎么配
source map 这件事不能省。没它也能看到报错,但堆栈里大多只剩压缩后的变量名和行号,排查会慢很多。
1. source map 的作用
线上代码通常会经过压缩、混淆、切包。报错到了 Sentry 之后,如果没有对应版本的 source map,看到的多半是这种信息:
main.a1b2c3.js- 第几行第几列
- 一个几乎认不出来的函数名
上传 source map 之后,Sentry 才能把堆栈还原回源码位置。
2. 正确做法
比较稳的流程一般是:
- 构建产物时生成 source map
- 在 CI 或发布流程里上传到 Sentry
- 把这次构建的
release一并写入前端 SDK - 让 Sentry 用
release把报错和 source map 对上
如果只做了第 1 步,没做上传,基本等于白做。
3. 构建上传示例
如果没有框架插件,直接用 sentry-cli 也可以:
sentry-cli releases new "$SENTRY_RELEASE"
sentry-cli releases files "$SENTRY_RELEASE" upload-sourcemaps ./dist \
--url-prefix "~/static/js" \
--validate
sentry-cli releases finalize "$SENTRY_RELEASE"
如果是前端静态资源分目录发布,--url-prefix 一定要和线上访问路径对齐。这个参数不对,上传会成功,但堆栈对不上,最后看起来像“明明传了也没生效”。
4. source map 要不要公开
通常不建议把 source map 直接暴露给所有人下载。更稳的做法是:
- 产物里生成 source map
- 在发布链路里上传到 Sentry
- 线上静态资源不公开 source map
如果项目确实需要保留 source map 文件,也最好配访问控制,别让它直接裸奔在公网。
三、是否录屏,怎么开启
这里说的“录屏”,通常是 Sentry Replay,也就是 Session Replay。
它不是传统意义上的完整视频录屏,更接近“用户会话重建”:页面发生了什么、用户点了哪里、错误出现前后大致发生了什么。
1. 默认要不要开
我的建议很简单:
- 默认不要全量开
- 可以只对报错会话开高采样
- 正式环境先小流量试运行
原因也不复杂:Replay 的确有用,但它会增加数据量、成本和隐私治理压力。
2. 开启方式
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [
Sentry.replayIntegration(),
],
replaysSessionSampleRate: 0.05,
replaysOnErrorSampleRate: 1,
});
这两个采样率可以这样理解:
replaysSessionSampleRate:普通会话抽样多少replaysOnErrorSampleRate:发生错误的会话保留多少
一套比较常见的起步值是:
- 平时录少量:
0.01到0.1 - 报错会话尽量保留:
1
3. 隐私和脱敏
Replay 一旦开启,最好同步检查:
- 输入框是否需要遮罩
- 用户名、手机号、邮箱是否需要打码
- 是否有支付、证件、后台管理类敏感页面
这一步别拖到后面补。等数据已经进平台了,再回头清理会麻烦很多。
四、beforeSend 一般放什么
很多项目接入 Sentry 时,真正开始“像样”起来,通常不是装完 SDK 的那一刻,而是把 beforeSend 用起来之后。
它是事件送到 Sentry 之前的最后一道关口。项目里常见的几类事情,基本都放在这里做:
- 敏感数据打码
- 无价值事件过滤
- 事件字段清理
- 错误归类和补充标记
如果团队没有统一封装层,beforeSend 往往就是第一层总闸。即便后面已经封了 reportException,这里也还是值得保留,因为它 离真正发送最近,最适合做兜底。
1. 常见用途
数据清理
有些异常对象会带一堆没必要上报的字段,比如超长响应体、整段 HTML、整包表单数据。直接发上去,排查不一定更快,噪音倒是先上来了。
这时候一般会在 beforeSend 里做瘦身,只保留排障真的需要的部分。
敏感数据打码
这是最常见的一类。
项目里通常会处理这些内容:
- 手机号
- 邮箱
- 身份证号
- token
- cookie
- 支付信息
- 地址、姓名这类个人信息
处理方式通常有两种:
- 直接删除
- 部分打码后保留
如果只是为了定位用户,通常留一个截断后的 ID 或 hash 就够了,没必要把原始值完整上报。
数据筛选
不是所有异常都值得进 Sentry。
常见会被过滤掉的有:
- 用户主动取消请求
- 浏览器扩展注入导致的报错
- 已知的三方脚本噪音
- 明确属于降级分支的预期异常
- 重复量太大但暂时无处理价值的低级告警
这类内容不先过滤,后面的告警看板很快就会脏掉。
数据整理和分类
有些错误本身信息不够直观,项目里会在 beforeSend 里顺手补一点分类字段,比如:
- 这是接口错误还是运行时错误
- 属于哪个模块
- 是不是某个 feature flag 打开后才出现
- 是否来自某个租户、站点或渠道
这类信息放在 tag、extra 或 context 里,后面筛选会轻松很多。
2. 一个更接近项目里的写法
import * as Sentry from "@sentry/react";
function maskPhone(value: string) {
return value.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
}
function shouldIgnoreError(event: Sentry.ErrorEvent, hint?: Sentry.EventHint) {
const error = hint?.originalException;
if (error instanceof Error) {
const message = error.message || "";
if (message.includes("AbortError")) return true;
if (message.includes("Non-Error promise rejection captured")) return true;
if (message.includes("ResizeObserver loop limit exceeded")) return true;
}
return false;
}
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
beforeSend(event, hint) {
if (shouldIgnoreError(event, hint)) {
return null;
}
const requestData = event.request?.data;
if (requestData && typeof requestData === "object") {
const data = requestData as Record<string, unknown>;
if (typeof data.phone === "string") {
data.phone = maskPhone(data.phone);
}
delete data.token;
delete data.password;
}
if (event.user?.email) {
event.user.email = "[masked]";
}
event.tags = {
...event.tags,
error_type: event.exception ? "runtime" : "message",
app_side: "client",
};
return event;
},
});
3. 实际落地时常见的分工
比较顺手的一种做法是这样分:
- 业务代码里决定“要不要报”
- 统一封装层决定“按什么格式报”
beforeSend决定“发出去之前还要不要删、改、拦”
这样职责会比较清楚,也不容易把所有逻辑都堆进某一个方法里。
4. beforeSend 里不太建议做什么
它很重要,但也别把它写成垃圾场。
不太建议放进去的内容有:
- 很重的异步逻辑
- 依赖页面实时状态的大段计算
- 跟业务流程强耦合的判断
- 大量难以维护的正则拼装
beforeSend 更适合做最后一步的规整,而不是再临时拼一套业务系统。
五、怎么开启上报
“怎么开启上报”通常有三层意思:
1. SDK 开关
最直接的是 enabled:
enabled: process.env.NODE_ENV === "production"
这适合控制“这个环境到底发不发”。
2. 环境级控制
很多团队会把开关交给环境变量:
SENTRY_ENABLED=true
然后在初始化里读取:
enabled: process.env.SENTRY_ENABLED === "true"
这样可以做到:
- 本地默认不开
- preview 可选开
- staging 低采样开
- production 正式开
3. 业务级控制
有些错误不值得上报,比如:
- 已知的用户取消操作
- 预期内的接口 4xx
- 被降级逻辑兜住的异常
这类场景更适合在统一封装层里过滤,不要把所有 catch 都直接往 Sentry 里丢。
六、上报的常用方法
Sentry 常见的上报方式并不复杂,麻烦的是团队一旦不统一,后面会越来越乱。
1. captureException
最常用。适合真实异常对象。
try {
await submitOrder();
} catch (error) {
Sentry.captureException(error);
}
2. captureMessage
适合主动记录一条业务告警,不一定是 Error。
Sentry.captureMessage("checkout payload missing skuId", "warning");
3. withScope
适合给 单次事件临时补充信息。
Sentry.withScope((scope) => {
scope.setTag("module", "checkout");
scope.setLevel("error");
scope.setContext("payload", {
orderId,
skuId,
});
Sentry.captureException(error);
});
4. setUser、setTag、setContext
适合把当前会话的公共信息挂上去。
Sentry.setUser({
id: user.id,
username: user.name,
});
Sentry.setTag("tenant", tenantId);
Sentry.setTag("app", "web");
七、统一上报封装层怎么做
这层很值得做。项目一旦大起来,如果每个人都直接写 Sentry.captureException,最后通常会变成两种情况:
- 字段乱,事件结构不统一
- 同类问题聚合不起来
一个简单但够用的封装大概像这样:
import * as Sentry from "@sentry/react";
type ReportLevel = "info" | "warning" | "error" | "fatal";
type ReportOptions = {
level?: ReportLevel;
module?: string;
action?: string;
tags?: Record<string, string>;
extra?: Record<string, unknown>;
user?: {
id?: string;
username?: string;
email?: string;
};
fingerprint?: string[];
};
export function reportException(error: unknown, options: ReportOptions = {}) {
Sentry.withScope((scope) => {
if (options.level) scope.setLevel(options.level);
if (options.module) scope.setTag("module", options.module);
if (options.action) scope.setTag("action", options.action);
Object.entries(options.tags ?? {}).forEach(([key, value]) => {
scope.setTag(key, value);
});
if (options.extra) {
Object.entries(options.extra).forEach(([key, value]) => {
scope.setExtra(key, value);
});
}
if (options.user) {
scope.setUser(options.user);
}
if (options.fingerprint) {
scope.setFingerprint(options.fingerprint);
}
scope.captureException(
error instanceof Error ? error : new Error(String(error)),
);
});
}
export function reportMessage(message: string, options: ReportOptions = {}) {
Sentry.withScope((scope) => {
if (options.level) scope.setLevel(options.level);
if (options.module) scope.setTag("module", options.module);
if (options.action) scope.setTag("action", options.action);
Object.entries(options.tags ?? {}).forEach(([key, value]) => {
scope.setTag(key, value);
});
if (options.extra) {
Object.entries(options.extra).forEach(([key, value]) => {
scope.setExtra(key, value);
});
}
scope.captureMessage(message);
});
}
这层封装的价值主要有四个:
- 统一字段命名
- 统一过滤逻辑
- 统一事件分类
- 方便以后替换底层实现
比如业务代码里最后就只需要这样写:
reportException(error, {
level: "error",
module: "checkout",
action: "submit-order",
tags: {
page: "order-confirm",
},
extra: {
orderId,
skuId,
},
});
八、上报时常用的参数类型
这部分最容易越用越乱,所以最好早点约束。
1. level
常见等级:
infowarningerrorfatal
经验上别把所有事情都打成 error。否则看板很快就会失真。
2. tag
tag 适合放可检索、可聚合、值比较短的字段,比如:
module=checkoutpage=product-detailtenant=cnapi=create-orderenv=production
tag 的好处是筛选方便。缺点也很明显:如果值太散,比如直接塞整段 URL、整段请求参数,平台维度会很快变脏。
3. extra
extra 适合放排障细节,比如:
- 请求参数片段
- 当前路由
- feature flag
- 本次操作的业务 ID
这类信息适合辅助排查,但不适合拿来做聚合维度。
4. context
context 更适合结构化对象,比如:
- 当前用户环境
- 订单信息摘要
- 浏览器能力
- 页面状态快照
示例:
scope.setContext("order", {
orderId,
amount,
currency,
});
5. user
用户信息常见会带:
idusernameemail
这里别贪多。能定位就行,敏感信息按合规要求做脱敏。
6. fingerprint
当默认聚合不够准时,可以手动指定归类方式:
scope.setFingerprint(["checkout", "create-order", errorCode]);
这对同一类业务错误的合并很有用。
7. breadcrumb
breadcrumb 更像“错误发生前发生了什么”。比如:
- 点击了哪个按钮
- 请求了哪个接口
- 跳转了哪个页面
这类信息对还原现场很有帮助,但也别什么都记,不然噪音会很多。
九、建议先约束一套分类规则
如果团队还没定规范,最少先把这三类分开:
1. 系统异常
比如:
- JS runtime error
- 资源加载失败
- 白屏
- 未捕获 Promise 异常
这类通常直接走 captureException。
2. 接口异常
比如:
- 服务端 5xx
- 网关超时
- 返回结构不符合预期
这类可以按接口名、业务模块和状态码补 tag。
3. 业务告警
比如:
- 关键字段缺失
- 状态机进入非法分支
- 降级逻辑被触发
这类更适合 captureMessage 或统一封装后的业务告警方法。
十、一个比较实用的落地建议
如果现在要在项目里补这套能力,我会按这个顺序做:
- 先接 SDK 初始 化和环境变量
- 再把 source map 上传打通
- 然后补统一上报层
- 最后再开 Replay 和性能采样
别一上来就把所有能力全开。source map 没接好之前,事件再多也不好查。统一封装没做好之前,字段只会越堆越乱。
十一、最后给一份最小检查清单
- 是否已经初始化 SDK
- 是否区分
development、staging、production - 是否给每次发布带了
release - 是否真的上传了 source map
- source map 是否和 release 成功对上
- 是否有统一上报封装
- 是否过滤掉预期内错误
- 是否评估过 Replay 的采样和隐私问题
- 是否约束了
tag、extra、context的使用边界
小结
Sentry 真正有用的,不是“装上 SDK”那一下,而是后面这条链路有没有接完整。
接得完整,线上报错能很快落到具体源码、具体页面和具体会话。接得不完整,平台里虽然也会有一堆红点,但很多时候只是把混乱搬到了另一个地方。
先把 source map、release 和统一上报层做好。Replay 和性能采样,再按成本和隐私要求慢慢加。