1. 为什么 Node.js 后端绕不开 Sentry 这件事
做 Node.js 后端的朋友,尤其是用 Express 搭服务的,大概都有过这种经历:线上接口突然 500,用户反馈过来,你打开服务器日志,满屏的堆栈信息,但就是定位不到是哪一行代码、哪个请求参数触发的。更难受的是,有些错误是间歇性的,等你连上服务器去看的时候,它又不出现了。这种“幽灵 Bug”消耗的排查时间,往往比写代码本身还多。
Sentry 就是来解决这个问题的。它是一套开源的错误追踪与性能监控平台,支持 Node.js、Python、Java、Go 等主流语言。对于 Node.js 后端来说,Sentry 能做的事情很具体:自动捕获未处理的异常、记录请求上下文、还原错误堆栈、聚合相同错误、在错误发生时第一时间推送告警。你不需要在代码里到处写console.log和try-catch,Sentry 的 SDK 会以中间件的形式挂载到 Express 应用上,静默地帮你收集一切。
这篇文章面向的是已经在写 Node.js 后端、或者正准备把服务往线上推的开发者。不管你用的是 Express 还是 Koa,数据库是 MongoDB 还是别的,Sentry 的接入思路是相通的。我会从整体设计思路讲起,然后拆解核心细节,再走一遍完整的实操流程,最后把日常排查中遇到的典型问题和排查技巧整理出来。内容基于我在实际项目中的经验,结合常见实践做合理补充,目标是让你看完就能在自己的项目里落地。
2. 整体设计与接入思路拆解
2.1 为什么选 Sentry 而不是自己搭日志系统
很多人第一反应是:我直接用 Winston 或 Pino 写日志,再配个 ELK 不就完了?这个思路没错,但有几个现实问题。第一,日志系统记录的是“发生了什么”,而 Sentry 记录的是“为什么发生”——它会自动关联错误堆栈、请求头、用户信息、面包屑(breadcrumbs),这些是纯日志很难结构化呈现的。第二,日志的量级很大,你需要自己去写聚合规则、去重逻辑、告警阈值,而 Sentry 开箱即用。第三,Sentry 的 Issue 分组算法能自动把相同根因的错误归并到一起,避免你被同一个 Bug 的上千条日志淹没。
当然,Sentry 不是替代日志系统的,它是补充。我的做法是:Winston 负责记录业务流水和调试信息,Sentry 负责捕获异常和性能问题。两者各司其职,互不干扰。
2.2 接入位置的选择:中间件还是全局钩子
在 Express 中接入 Sentry,核心是搞清楚“在哪里挂”。Sentry 的 Node.js SDK 提供了两种主要方式:一是作为 Express 中间件,二是通过全局的uncaughtException和unhandledRejection钩子。实际项目中,这两者要配合使用。
中间件的优势在于它能拿到完整的请求上下文——req对象里的 URL、method、headers、cookies、甚至 body(需要配置),这些信息在排查时极其关键。而全局钩子能兜住那些不在请求生命周期内抛出的错误,比如定时任务里的异常、数据库连接池的异步错误。
顺序上有个坑:Sentry 的请求处理中间件必须放在所有路由之前,但错误处理中间件必须放在所有路由之后。这个顺序搞反了,要么捕获不到请求上下文,要么错误被 Express 默认的错误处理器吞掉。
2.3 与 MongoDB 的配合:别让数据库错误变成黑盒
Node.js 后端用 MongoDB 的场景很常见,Mongoose 是主流 ODM。Mongoose 的错误有个特点:它会在 ValidationError、CastError、DocumentNotFoundError 等类型上附加很多有用的信息,但这些信息默认不会出现在 Sentry 的报错里。你需要做一层转换,把 Mongoose 的错误对象序列化成 Sentry 能识别的格式。
我的做法是在 Sentry 的beforeSend钩子里判断错误类型,如果是 Mongoose 的错误,就把error.errors里的字段级信息提取出来,附加到 Sentry 的extra数据里。这样在 Sentry 面板上就能直接看到是哪个字段校验失败了,而不是只看到一个笼统的 ValidationError。
2.4 性能监控的取舍:什么时候开 Tracing
Sentry 的性能监控(Tracing)能记录每个请求的耗时、数据库查询时间、外部 API 调用时间。听起来很美好,但它有成本:一是数据量会显著增加,二是对性能有轻微影响(通常 1%-3%)。我的建议是,项目初期先只开错误监控,等业务稳定了、确实需要优化性能时再开 Tracing。如果开了,采样率(tracesSampleRate)不要设太高,生产环境 0.1 到 0.2 就够了,开发环境可以设 1.0 方便调试。
3. 核心细节解析与实操要点
3.1 SDK 初始化:那些文档里没写的参数
Sentry 的初始化看起来简单,就一个Sentry.init(),但有几个参数直接决定了你后续排查的体验。
首先是dsn,这是 Sentry 项目的唯一标识,格式是https://xxx@xxx.ingest.sentry.io/xxx。这个值必须放在环境变量里,绝对不能硬编码到代码中。我见过有人把它提交到了公开仓库,结果被恶意灌入了大量垃圾数据。
其次是environment,这个参数用来区分开发、测试、生产环境。如果不设,所有环境的错误会混在一起,排查时非常痛苦。我的习惯是设为process.env.NODE_ENV,然后在 Sentry 面板上按环境过滤。
然后是release,这个参数关联到你的代码版本。每次部署时传入 Git commit hash 或版本号,Sentry 就能告诉你这个错误是从哪个版本开始出现的。配合 Source Map 上传,还能直接看到源码行号。
还有一个容易被忽略的是serverName,在多实例部署的场景下,这个参数能帮你区分是哪个 Pod 或哪台机器出的问题。
Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV, release: process.env.APP_VERSION, serverName: process.env.HOSTNAME, tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, beforeSend(event, hint) { // 过滤掉一些不需要上报的错误 const error = hint.originalException; if (error && error.message && error.message.includes('ECONNRESET')) { return null; } return event; } });3.2 Express 中间件的挂载顺序:一步错步步错
Express 的中间件是洋葱模型,顺序至关重要。Sentry 的接入需要三个中间件,顺序如下:
Sentry.Handlers.requestHandler():必须放在所有路由之前,用来捕获请求上下文。- 你的业务路由和中间件。
Sentry.Handlers.errorHandler():必须放在所有路由之后,用来捕获路由中抛出的错误。- 你自己的错误处理中间件:放在 Sentry 的 errorHandler 之后,用来做统一的错误响应。
这里有个细节:Sentry 的 errorHandler 会捕获错误并上报,但它不会终止请求。也就是说,错误会继续传递给后面的错误处理中间件。所以你的自定义错误处理中间件仍然需要返回响应,否则请求会挂起。
const express = require('express'); const Sentry = require('@sentry/node'); const app = express(); // 1. 请求处理中间件,必须在所有路由之前 app.use(Sentry.Handlers.requestHandler()); // 2. 业务路由 app.use('/api/users', userRouter); app.use('/api/orders', orderRouter); // 3. Sentry 错误处理中间件,必须在所有路由之后 app.use(Sentry.Handlers.errorHandler()); // 4. 自定义错误处理中间件 app.use((err, req, res, next) => { const statusCode = err.statusCode || 500; res.status(statusCode).json({ code: statusCode, message: err.message || 'Internal Server Error', requestId: req.id }); });3.3 请求上下文的增强:让每个错误都有迹可循
Sentry 默认会记录请求的 URL、method、headers,但有些信息需要手动附加。比如当前登录的用户 ID、请求的 trace ID、业务相关的参数。这些信息在排查时能帮你快速定位到具体的用户和操作。
我通常会在一个自定义中间件里做这件事,放在 Sentry 的 requestHandler 之后:
app.use((req, res, next) => { // 附加用户信息 if (req.user) { Sentry.setUser({ id: req.user.id, username: req.user.username, email: req.user.email }); } // 附加自定义标签 Sentry.setTag('request_id', req.id); Sentry.setTag('api_version', req.headers['x-api-version'] || 'v1'); // 附加额外上下文 Sentry.setContext('request_body', { body: req.body, query: req.query, params: req.params }); next(); });注意,req.body里可能包含密码等敏感信息,一定要在beforeSend里做脱敏处理,或者只记录必要的字段。
3.4 手动捕获:什么时候该用 captureException
Sentry 的自动捕获能覆盖大部分场景,但有些错误是“预期内”的,比如第三方 API 返回了业务错误码,这时候你不会抛异常,但你想记录下来。这时候就需要手动调用Sentry.captureException()或Sentry.captureMessage()。
我的原则是:只有那些“需要人工介入”的错误才上报。比如支付回调验签失败、数据库连接池耗尽、关键配置缺失。普通的业务校验失败(比如用户输入格式不对)不需要上报,否则 Sentry 会被噪音淹没。
try { const result = await paymentGateway.verifyCallback(params); if (!result.success) { Sentry.captureMessage('Payment callback verification failed', { level: 'warning', extra: { params, result } }); } } catch (err) { Sentry.captureException(err, { tags: { module: 'payment' }, extra: { orderId: params.orderId } }); }3.5 Source Map 上传:让堆栈信息可读
Node.js 后端如果用了 TypeScript 或 Babel,线上跑的是编译后的代码,Sentry 捕获的堆栈信息会是编译后的行号,根本没法看。解决办法是在构建时上传 Source Map 到 Sentry。
用@sentry/webpack-plugin或sentry-cli都可以。我推荐用sentry-cli,因为它不依赖构建工具,在 CI/CD 流程里更灵活。关键步骤是:构建时生成 Source Map,然后用sentry-cli releases files <version> upload-sourcemaps ./dist上传,最后在 Sentry 初始化时设置release为同一个版本号。
有个坑:上传完 Source Map 后,记得在构建产物里删除.map文件,否则会暴露源码。Sentry 上传后会自己保存一份,不需要你保留本地文件。
4. 完整实操流程与核心环节实现
4.1 环境准备与依赖安装
假设你已经有一个 Express + MongoDB 的项目,Node.js 版本建议 18 LTS 以上。先安装 Sentry 的 Node.js SDK:
npm install @sentry/node @sentry/tracing如果你用 TypeScript,还需要安装类型定义(通常 SDK 自带)。然后在项目根目录创建一个.env文件,写入 Sentry 的 DSN:
SENTRY_DSN=https://your-dsn@sentry.io/your-project-id APP_VERSION=1.0.0 NODE_ENV=productionDSN 从 Sentry 项目的 Settings -> Client Keys 里获取。注意不要把这个文件提交到 Git。
4.2 初始化 Sentry 并挂载中间件
创建一个sentry.js文件,专门负责 Sentry 的初始化和导出:
const Sentry = require('@sentry/node'); const { ProfilingIntegration } = require('@sentry/profiling-node'); function initSentry(app) { Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV || 'development', release: process.env.APP_VERSION || 'unknown', integrations: [ new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Express({ app }), new ProfilingIntegration() ], tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, profilesSampleRate: 0.1, beforeSend(event, hint) { const error = hint.originalException; // 过滤掉 MongoDB 连接重置的错误 if (error && error.name === 'MongoNetworkError') { return null; } // 脱敏处理 if (event.request && event.request.data) { const data = event.request.data; if (data.password) data.password = '[Filtered]'; if (data.token) data.token = '[Filtered]'; } return event; } }); // 请求处理中间件 app.use(Sentry.Handlers.requestHandler()); // Tracing 中间件 app.use(Sentry.Handlers.tracingHandler()); } function setupSentryErrorHandler(app) { // 错误处理中间件 app.use(Sentry.Handlers.errorHandler()); } module.exports = { initSentry, setupSentryErrorHandler };然后在app.js里这样用:
const express = require('express'); const { initSentry, setupSentryErrorHandler } = require('./sentry'); const app = express(); // 初始化 Sentry,必须在所有路由之前 initSentry(app); // 解析 body app.use(express.json()); // 业务路由 app.use('/api', require('./routes')); // Sentry 错误处理,必须在所有路由之后 setupSentryErrorHandler(app); // 自定义错误处理 app.use((err, req, res, next) => { console.error(err); res.status(err.statusCode || 500).json({ code: err.statusCode || 500, message: err.message || 'Internal Server Error' }); }); module.exports = app;4.3 MongoDB 错误的结构化处理
Mongoose 的错误需要特殊处理才能让 Sentry 更好地展示。我写了一个工具函数,在beforeSend里调用:
function normalizeMongooseError(error) { if (error.name === 'ValidationError') { const fields = Object.keys(error.errors).map(key => ({ field: key, message: error.errors[key].message, value: error.errors[key].value })); return { type: 'ValidationError', fields, message: error.message }; } if (error.name === 'CastError') { return { type: 'CastError', field: error.path, value: error.value, kind: error.kind, message: error.message }; } if (error.name === 'MongoServerError' && error.code === 11000) { return { type: 'DuplicateKeyError', keyPattern: error.keyPattern, keyValue: error.keyValue, message: error.message }; } return null; }然后在beforeSend里:
beforeSend(event, hint) { const error = hint.originalException; if (error) { const normalized = normalizeMongooseError(error); if (normalized) { event.extra = { ...event.extra, mongoose_error: normalized }; } } return event; }这样在 Sentry 面板上,你就能看到具体的字段校验错误、类型转换错误、唯一索引冲突等信息,而不是一个笼统的报错。
4.4 部署与 Source Map 上传
在 CI/CD 流程里,构建完成后执行 Source Map 上传。以 GitHub Actions 为例:
- name: Build run: npm run build - name: Upload Source Maps to Sentry run: | npx sentry-cli releases new ${{ env.APP_VERSION }} npx sentry-cli releases files ${{ env.APP_VERSION }} upload-sourcemaps ./dist --url-prefix '~/dist' npx sentry-cli releases finalize ${{ env.APP_VERSION }} env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: your-org SENTRY_PROJECT: your-projectSENTRY_AUTH_TOKEN从 Sentry 的 Settings -> Auth Tokens 里生成,权限选project:releases和org:read就够了。
4.5 验证接入是否成功
部署完成后,写一个测试接口故意抛错:
app.get('/api/debug/error', (req, res) => { throw new Error('Sentry test error'); });访问这个接口,然后在 Sentry 面板上应该能看到这条错误。检查几个关键点:堆栈信息是否可读(Source Map 是否生效)、请求上下文是否完整(URL、method、headers)、用户信息是否附加、环境标签是否正确。如果都正常,说明接入成功。
5. 日常排查中的常见问题与解决技巧
5.1 错误没有上报到 Sentry
这是最常见的问题,原因通常有几个。第一,DSN 配置错误,检查环境变量是否加载成功。第二,中间件顺序不对,requestHandler没有放在路由之前,或者errorHandler没有放在路由之后。第三,错误被 Express 的默认错误处理器吞掉了,比如在路由里用了res.send()之后又抛错,这时候错误不会进入 Sentry 的 errorHandler。第四,beforeSend里返回了null,把错误过滤掉了。
排查方法:在beforeSend里加一行console.log('Sentry event:', event),看看有没有走到这里。如果没有,说明错误根本没被捕获;如果有但没上报,检查 DSN 和网络。
5.2 堆栈信息显示的是编译后的代码
这是 Source Map 没上传或版本号不匹配导致的。检查三个地方:构建时是否生成了.map文件、sentry-cli上传时release版本号是否和初始化时的release一致、上传的url-prefix是否和实际部署路径匹配。我遇到过url-prefix写成~/dist但实际部署在/app/dist的情况,导致 Source Map 匹配不上。
5.3 错误量太大,Sentry 被噪音淹没
生产环境跑一段时间后,Sentry 上可能积累大量重复的、不重要的错误。解决办法有几个:一是用beforeSend过滤掉已知的、不需要处理的错误(比如网络抖动导致的 ECONNRESET);二是用 Sentry 的 Inbound Filters 功能,在面板上配置过滤规则;三是调整采样率,对高频错误做降采样;四是设置 Issue 的告警阈值,只有错误量突增时才通知。
5.4 MongoDB 连接错误频繁上报
MongoDB 在连接不稳定时会频繁抛出MongoNetworkError,这类错误通常是暂时的,不需要每次都上报。我的做法是在beforeSend里判断错误类型,如果是MongoNetworkError且错误信息包含ECONNRESET或ETIMEDOUT,直接返回null过滤掉。同时,在应用层面加一个重连机制,确保连接恢复后服务能自动恢复。
5.5 敏感信息泄露到 Sentry
请求体里的密码、token、手机号等信息如果被上报到 Sentry,会造成安全隐患。必须在beforeSend里做脱敏处理。我的做法是维护一个敏感字段列表,遍历event.request.data和event.extra,把匹配的字段值替换成[Filtered]。另外,Sentry 的sendDefaultPii选项默认是false,不要轻易打开。
5.6 性能监控数据太多,存储成本高
开了 Tracing 之后,Sentry 的存储量会快速增长。控制方法:降低tracesSampleRate(生产环境 0.1 甚至 0.05)、设置tracesSampler自定义采样规则(比如只采样慢请求)、定期清理旧的 Transaction 数据。如果预算有限,可以只对关键接口开启 Tracing,其他接口关闭。
5.7 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 错误未上报 | DSN 错误、中间件顺序不对、被 beforeSend 过滤 | 检查环境变量、调整中间件顺序、检查 beforeSend 逻辑 |
| 堆栈不可读 | Source Map 未上传或版本不匹配 | 检查 release 版本号、url-prefix、上传流程 |
| 错误量过大 | 未过滤已知错误、采样率过高 | 配置 beforeSend 过滤、调整采样率、设置告警阈值 |
| MongoDB 错误频繁 | 连接不稳定、未过滤网络错误 | 过滤 MongoNetworkError、加重连机制 |
| 敏感信息泄露 | 未脱敏、sendDefaultPii 开启 | 配置 beforeSend 脱敏、关闭 sendDefaultPii |
| 性能数据过多 | tracesSampleRate 过高 | 降低采样率、自定义 tracesSampler |
5.8 几个我踩过的坑
第一个坑:在beforeSend里做了异步操作。beforeSend必须是同步的,如果你在里面await一个异步函数,Sentry 会直接忽略返回值,导致过滤失效。如果需要异步处理,用beforeSendTransaction或者提前在中间件里处理好。
第二个坑:在 Express 的异步路由里抛错,Sentry 捕获不到。Express 4.x 不会自动捕获异步错误,你需要用express-async-errors这个包,或者手动try-catch后调用next(err)。Express 5.x 已经原生支持了,但升级需谨慎。
第三个坑:Sentry 的requestHandler会读取req.body,但如果 body 解析中间件放在它后面,就读不到。所以express.json()必须放在Sentry.Handlers.requestHandler()之前。这个顺序很容易搞反。
第四个坑:在多进程(cluster)模式下,每个 worker 都会初始化 Sentry,导致重复上报。解决办法是在主进程初始化一次,或者用Sentry.Handlers.requestHandler()在每个 worker 里单独挂载,但 DSN 和配置保持一致。
6. 一些关于告警和团队协作的经验
Sentry 的告警规则配置得好,能省很多事。我的配置是:新出现的错误立即通知、错误量突增(比如 5 分钟内超过 100 次)立即通知、每天定时发送错误汇总。通知渠道用 Slack 或钉钉,直接推到开发群。
另外,Sentry 的 Issue 分配功能很实用。可以按模块或按代码负责人自动分配 Issue,避免“大家都看到了但没人处理”的情况。配合 Release 追踪,还能看到每个版本引入的新错误和修复的错误,对复盘很有帮助。
对于 MongoDB 相关的错误,我建议单独建一个 Tag,比如db: mongodb,这样在 Sentry 面板上可以快速筛选出所有数据库相关的错误,方便 DBA 或后端同学专项排查。
最后分享一个小技巧:在 Sentry 的 Issue 详情页,可以用user.id搜索特定用户的所有错误。当用户反馈“我这边一直报错”时,直接搜用户 ID,就能看到他在哪个接口、什么时间、遇到了什么错误,排查效率提升非常明显。