跳到主要内容

Sentry 错误上报接入与实践

先说清楚一件事:当前这个仓库是文档站,代码里还没有直接安装 @sentry/* 相关依赖,也没有现成的初始化文件或构建上传脚本。

这篇文章写的是一套前端项目里常见的接法。重点放在四件事上:

  • 项目里通常怎么接 Sentry
  • source map 怎么处理
  • 是否开启录屏,以及怎么开
  • 业务里怎么做统一上报封装

如果你们项目是 Next.js、React 或 Vite,这套思路基本都能直接用。

先把接入目标说清楚

Sentry 真正解决的不是“日志多一个平台可看”,而是线上出了问题之后,能不能尽快回答这几个问题:

  • 哪个页面报的
  • 哪个用户遇到的
  • 当时的环境是什么
  • 代码堆栈能不能还原到源码
  • 这是不是一批同类问题

所以接入时至少要把这几层接完整:

  1. SDK 初始化
  2. release / environment 标记
  3. source map 上传
  4. 业务统一上报层
  5. 可选的 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:环境标记,比如 developmentstagingproduction

项目里最好把 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 不要默认所有环境都开
  • environmentrelease 要一起带上
  • 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,不等于已经上传到 Sentry
  • authTokenorgproject 少一个,上传就会失败
  • hideSourceMaps: true 可以避免浏览器直接暴露 source map 路径

二、source map 这一层到底要怎么配

source map 这件事不能省。没它也能看到报错,但堆栈里大多只剩压缩后的变量名和行号,排查会慢很多。

1. source map 的作用

线上代码通常会经过压缩、混淆、切包。报错到了 Sentry 之后,如果没有对应版本的 source map,看到的多半是这种信息:

  • main.a1b2c3.js
  • 第几行第几列
  • 一个几乎认不出来的函数名

上传 source map 之后,Sentry 才能把堆栈还原回源码位置。

2. 正确做法

比较稳的流程一般是:

  1. 构建产物时生成 source map
  2. 在 CI 或发布流程里上传到 Sentry
  3. 把这次构建的 release 一并写入前端 SDK
  4. 让 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.010.1
  • 报错会话尽量保留:1

3. 隐私和脱敏

Replay 一旦开启,最好同步检查:

  • 输入框是否需要遮罩
  • 用户名、手机号、邮箱是否需要打码
  • 是否有支付、证件、后台管理类敏感页面

这一步别拖到后面补。等数据已经进平台了,再回头清理会麻烦很多。

四、beforeSend 一般放什么

很多项目接入 Sentry 时,真正开始“像样”起来,通常不是装完 SDK 的那一刻,而是把 beforeSend 用起来之后。

它是事件送到 Sentry 之前的最后一道关口。项目里常见的几类事情,基本都放在这里做:

  • 敏感数据打码
  • 无价值事件过滤
  • 事件字段清理
  • 错误归类和补充标记

如果团队没有统一封装层,beforeSend 往往就是第一层总闸。即便后面已经封了 reportException,这里也还是值得保留,因为它离真正发送最近,最适合做兜底。

1. 常见用途

数据清理

有些异常对象会带一堆没必要上报的字段,比如超长响应体、整段 HTML、整包表单数据。直接发上去,排查不一定更快,噪音倒是先上来了。

这时候一般会在 beforeSend 里做瘦身,只保留排障真的需要的部分。

敏感数据打码

这是最常见的一类。

项目里通常会处理这些内容:

  • 手机号
  • 邮箱
  • 身份证号
  • token
  • cookie
  • 支付信息
  • 地址、姓名这类个人信息

处理方式通常有两种:

  • 直接删除
  • 部分打码后保留

如果只是为了定位用户,通常留一个截断后的 ID 或 hash 就够了,没必要把原始值完整上报。

数据筛选

不是所有异常都值得进 Sentry。

常见会被过滤掉的有:

  • 用户主动取消请求
  • 浏览器扩展注入导致的报错
  • 已知的三方脚本噪音
  • 明确属于降级分支的预期异常
  • 重复量太大但暂时无处理价值的低级告警

这类内容不先过滤,后面的告警看板很快就会脏掉。

数据整理和分类

有些错误本身信息不够直观,项目里会在 beforeSend 里顺手补一点分类字段,比如:

  • 这是接口错误还是运行时错误
  • 属于哪个模块
  • 是不是某个 feature flag 打开后才出现
  • 是否来自某个租户、站点或渠道

这类信息放在 tagextracontext 里,后面筛选会轻松很多。

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. setUsersetTagsetContext

适合把当前会话的公共信息挂上去。

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

常见等级:

  • info
  • warning
  • error
  • fatal

经验上别把所有事情都打成 error。否则看板很快就会失真。

2. tag

tag 适合放可检索、可聚合、值比较短的字段,比如:

  • module=checkout
  • page=product-detail
  • tenant=cn
  • api=create-order
  • env=production

tag 的好处是筛选方便。缺点也很明显:如果值太散,比如直接塞整段 URL、整段请求参数,平台维度会很快变脏。

3. extra

extra 适合放排障细节,比如:

  • 请求参数片段
  • 当前路由
  • feature flag
  • 本次操作的业务 ID

这类信息适合辅助排查,但不适合拿来做聚合维度。

4. context

context 更适合结构化对象,比如:

  • 当前用户环境
  • 订单信息摘要
  • 浏览器能力
  • 页面状态快照

示例:

scope.setContext("order", {
orderId,
amount,
currency,
});

5. user

用户信息常见会带:

  • id
  • username
  • email

这里别贪多。能定位就行,敏感信息按合规要求做脱敏。

6. fingerprint

当默认聚合不够准时,可以手动指定归类方式:

scope.setFingerprint(["checkout", "create-order", errorCode]);

这对同一类业务错误的合并很有用。

7. breadcrumb

breadcrumb 更像“错误发生前发生了什么”。比如:

  • 点击了哪个按钮
  • 请求了哪个接口
  • 跳转了哪个页面

这类信息对还原现场很有帮助,但也别什么都记,不然噪音会很多。

九、建议先约束一套分类规则

如果团队还没定规范,最少先把这三类分开:

1. 系统异常

比如:

  • JS runtime error
  • 资源加载失败
  • 白屏
  • 未捕获 Promise 异常

这类通常直接走 captureException

2. 接口异常

比如:

  • 服务端 5xx
  • 网关超时
  • 返回结构不符合预期

这类可以按接口名、业务模块和状态码补 tag。

3. 业务告警

比如:

  • 关键字段缺失
  • 状态机进入非法分支
  • 降级逻辑被触发

这类更适合 captureMessage 或统一封装后的业务告警方法。

十、一个比较实用的落地建议

如果现在要在项目里补这套能力,我会按这个顺序做:

  1. 先接 SDK 初始化和环境变量
  2. 再把 source map 上传打通
  3. 然后补统一上报层
  4. 最后再开 Replay 和性能采样

别一上来就把所有能力全开。source map 没接好之前,事件再多也不好查。统一封装没做好之前,字段只会越堆越乱。

十一、最后给一份最小检查清单

  • 是否已经初始化 SDK
  • 是否区分 developmentstagingproduction
  • 是否给每次发布带了 release
  • 是否真的上传了 source map
  • source map 是否和 release 成功对上
  • 是否有统一上报封装
  • 是否过滤掉预期内错误
  • 是否评估过 Replay 的采样和隐私问题
  • 是否约束了 tagextracontext 的使用边界

小结

Sentry 真正有用的,不是“装上 SDK”那一下,而是后面这条链路有没有接完整。

接得完整,线上报错能很快落到具体源码、具体页面和具体会话。接得不完整,平台里虽然也会有一堆红点,但很多时候只是把混乱搬到了另一个地方。

先把 source map、release 和统一上报层做好。Replay 和性能采样,再按成本和隐私要求慢慢加。