1. 为什么 Node.js 后端绕不开 Sentry 这件事做 Node.js 后端的朋友尤其是用 Express 搭服务的大概都有过这种经历线上接口突然 500用户反馈过来你打开服务器日志满屏的堆栈信息但就是定位不到是哪一行代码、哪个请求参数触发的。更难受的是有些错误是间歇性的等你连上服务器去看的时候它又不出现了。这种“幽灵 Bug”消耗的排查时间往往比写代码本身还多。Sentry 就是来解决这个问题的。它是一套开源的错误追踪与性能监控平台支持 Node.js、Python、Java、Go 等主流语言。对于 Node.js 后端来说Sentry 能做的事情很具体自动捕获未处理的异常、记录请求上下文、还原错误堆栈、聚合相同错误、在错误发生时第一时间推送告警。你不需要在代码里到处写console.log和try-catchSentry 的 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 性能监控的取舍什么时候开 TracingSentry 的性能监控Tracing能记录每个请求的耗时、数据库查询时间、外部 API 调用时间。听起来很美好但它有成本一是数据量会显著增加二是对性能有轻微影响通常 1%-3%。我的建议是项目初期先只开错误监控等业务稳定了、确实需要优化性能时再开 Tracing。如果开了采样率tracesSampleRate不要设太高生产环境 0.1 到 0.2 就够了开发环境可以设 1.0 方便调试。3. 核心细节解析与实操要点3.1 SDK 初始化那些文档里没写的参数Sentry 的初始化看起来简单就一个Sentry.init()但有几个参数直接决定了你后续排查的体验。首先是dsn这是 Sentry 项目的唯一标识格式是https://xxxxxx.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 手动捕获什么时候该用 captureExceptionSentry 的自动捕获能覆盖大部分场景但有些错误是“预期内”的比如第三方 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 SDKnpm install sentry/node sentry/tracing如果你用 TypeScript还需要安装类型定义通常 SDK 自带。然后在项目根目录创建一个.env文件写入 Sentry 的 DSNSENTRY_DSNhttps://your-dsnsentry.io/your-project-id APP_VERSION1.0.0 NODE_ENVproductionDSN 从 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 过高降低采样率、自定义 tracesSampler5.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就能看到他在哪个接口、什么时间、遇到了什么错误排查效率提升非常明显。
