Sentry+LogRocket 前端监控实战:从错误追踪到会话回放

发布时间:2026/10/3 21:40:02
Sentry+LogRocket 前端监控实战:从错误追踪到会话回放 先说个我印象特别深的场景线上用户反馈说“订单提交后就白屏了”业务方在群里连环你自己在测试环境怎么点都复现不出来。这时候你手里只有一条“用户反馈”的通知没有报错堆栈没有用户操作路径连是哪个接口挂了都不确定——这种感觉做过前端的人应该都懂。后来我在团队里把前端监控体系搭起来用的就是标题里的组合Sentry负责错误追踪和聚合LogRocket负责会话回放和现场还原。两个工具配合起来才真正把“用户报障靠猜”变成了“打开后台看回放”。这篇文章就把我当时从选型、接入、配置到实际排障的完整经验整理出来包括一些踩过的坑和沉淀下来的排查套路适合正在建设前端监控体系、或者想从“只接一个Sentry”升级到“可还原现场”的团队参考。1. 前端监控体系整体设计与思路拆解1.1 监控体系到底要解决什么问题很多团队对前端监控的理解停留在“统计报错数量”接个Sentry能收到错误日志就完事了。但真正被线上问题折磨过的人会知道一个错误日志的价值远远不够你需要回答的是三个递进的问题系统到底哪里坏了用户是在什么操作下触发的这个问题影响了多少人、影响有多大这三个问题分别对应稳定性监控、行为追踪和影响面评估。传统的错误上报只能回答第一个问题或部分第三个问题第二个问题几乎是盲区——同样的代码分支用户点击路径不同、浏览器环境不同、登录状态不同触发出来的表现可能截然不同。Sentry能告诉你报错的位置和堆栈但没法告诉你用户是不是先快速点击了三次提交按钮也没法告诉你用户是不是在弱网环境下等了五秒才开始操作。所以我在设计整个监控体系的时候把目标拆成了三层第一层是基础的错误采集和告警第二层是用户行为的可视化还原第三层是基于这些数据的趋势分析和复盘。Sentry扛起第一层和第三层的大部分工作LogRocket专注第二层。两者各管一段中间通过用户标识和会话ID打通形成一条完整的排查链路。1.2 为什么是Sentry和LogRocket的组合Sentry在这个领域的地位不用多说开源、生态成熟、支持前后端多种语言社区对各种框架都有现成的SDK。接入一个React应用初始化代码不到十行source map上传配置好之后线上压缩代码的报错也能直接映射回源码行列号。它更像一个“故障记录仪”把每次异常的结构化信息完整记录下来包括环境、浏览器、版本、用户行为上下文然后按照相似度聚合成Issue方便持续追踪。LogRocket则是另一个维度的工具。它不做错误聚合而是把用户在页面上的完整会话录下来包括鼠标移动、点击、滚动、控制台输出、网络请求和响应、Redux或Vuex里的状态变更等。它解决的正是Sentry解决不了的问题——当报错信息本身不足以定位根因时你需要看到用户当时到底做了什么。拿开车来类比Sentry像行车电脑的故障码告诉你哪个传感器或者哪个气缸工作异常LogRocket像行车记录仪还原了事故前几秒方向盘的转动轨迹和刹车时机。没有故障码你排查起来像大海捞针没有行车记录仪你很难判断是车的问题还是人的操作问题。只有两者结合才能在几十分钟内完成从告警到定位的闭环。1.3 从采集到消费监控数据链路的整体设计一个完整的前端监控链路并不复杂大致是四个环节采集、上报、存储展示、告警与排查。采集端是嵌入在业务代码里的各个SDK。Sentry SDK捕获JavaScript运行时错误、未处理的Promise异常、资源加载失败、以及手动上报的业务异常LogRocket SDK则持续采集DOM事件、网络请求、控制台日志和状态快照。两边的SDK各自独立工作互不阻塞但会共享一个会话标识方便后面对接。上报环节Sentry会把错误事件包装成带有结构化字段的JSON经过内置的传输层发送到Sentry服务端LogRocket则走的是流式压缩上传把录制数据分片传回它的云端服务。服务端负责解压、索引、存储然后分别在各自的控制台里呈现错误列表或会话列表。告警与排查是最上层的消费环节。Sentry的告警规则在满足条件时通过邮件、Slack、Webhook等渠道推给相关人员而LogRocket的会话回放链接可以作为上下文信息挂到Sentry的Issue详情页。换句话说一条告警到达手里的瞬间你同时能看到报错堆栈和对应的用户回放这比打开两个页面手动关联要高效得多。2. Sentry落地实操从接入到告警2.1 初始化参数与Source Map上传Sentry的接入流程网上文档很多我挑几个容易被忽略但影响深远的参数讲。第一步是初始化我的React项目里大致是这样import * as Sentry from sentry/react; Sentry.init({ dsn: https://your-dsnsentry.example.com/123, environment: process.env.NODE_ENV, release: demo-app${process.env.REACT_APP_VERSION}, tracesSampleRate: 0.2, beforeSend(event) { if (event.exception?.[0]?.type ResizeObserver loop limit exceeded) { return null; } return event; } });dsn是事件上报的地址每个项目一个environment用来区分生产、预发、测试环境否则生产环境的报错和本地开发的报错混在一起会让人崩溃release对应构建版本号这个字段直接决定你后面能不能把压缩代码映射回源码tracesSampleRate是性能追踪的采样率设成0.2代表只采样20%的会话控制性能开销。真正容易被忽略的是Source Map。线上运行的JavaScript都是压缩和混淆过的如果Sentry拿不到对应的source map报错堆栈里全是e.map is not a function这种看完等于没看的字段。所以在CI流程里我用sentry-cli把构建产物里的source map上传到对应的release下sentry-cli releases new demo-app1.0.0 sentry-cli releases set-commits --auto demo-app1.0.0 sentry-cli sourcemaps upload --releasedemo-app1.0.0-demo --orgmy-org --projectmy-project ./build/static/js这里有个注意事项source map包含源码内容属于敏感产物绝对不能直接部署到公网静态资源目录里让人随便下载。正确做法是只在构建机或CI环境里用sentry-cli上传然后从部署包里删掉source map文件或者至少确保生产环境不通过URL直接访问到它们。2.2 错误分组机制与指纹规则Sentry会把相似的错误自动聚合成一个Issue默认的分组规则包含错误消息、异常类型、堆栈帧等。这套规则大多数情况下表现不错但线上总会遇到分组不准的情况。比如同一个页面里多个不同的接口都返回了网络错误因为错误类型都是TypeError: Failed to fetch堆栈也长得一样Sentry就会把它们并成一个Issue导致排查时看不出到底是哪个接口的故障。这时候就需要自定义fingerprint重写分组逻辑。在beforeSend里可以根据业务字段修改事件指纹beforeSend(event, hint) { const originalException hint.originalException; if ( originalException originalException.message originalException.message.includes(Failed to fetch) ) { const url event.request?.url || unknown; event.fingerprint [network-error, url]; } return event; }这样每个接口的网络错误就能按URL拆分开各自成为一个独立Issue。经验是分组规则宁可拆细也不要过粗。一个Issue里面混了好几个原因比十几个Issue每人看一眼要难处理得多因为团队做周会复盘时根本没法从噪音里找到真正的高频问题。2.3 告警规则配置与通知渠道告警是监控体系触达人的关键一环配置不合理就会变成“狼来了”。我当时的第一版规则就是死亡案例把所有错误都设成即时告警结果一天收到几百条通知三天后群里的开发把通知直接折叠了。建议把告警分成两级。一级是紧急告警对应白屏、脚本执行错误、未捕获异常等直接影响核心流程的问题条件设置为“5分钟内错误次数超过20影响用户数超过10”走即时通知渠道。二级是聚合告警对应非核心页面的错误和低频问题按小时或按天汇总成一条摘要消息发送。通知渠道上Sentry官方支持邮件、Slack、PagerDuty国内团队可以用Webhook推到钉钉或企业微信。我在Webhook的Payload里通过tags动态带上release、environment和issue_url让接到告警的人不用登录Sentry就能看到关键信息。2.4 用Context信息记录业务现场只上报一个堆栈远远不够很多问题必须结合业务上下文才能定位。Sentry提供了setContext、setTag、setUser这几个API可以把用户ID、订单号、当前路由、操作步骤等维度挂到事件上Sentry.setUser({ id: userId, email: email }); Sentry.setTag(page_route, window.location.pathname); Sentry.setTag(order_id, orderId); Sentry.setContext(checkout, { step: payment, itemsCount: cart.items.length, totalPrice: cart.total });这些上下文信息在Issue详情页里一目了然。比如用户报障“支付成功但页面没跳转”你打开Sentry一看上下文里记录了step: payment、后端返回的订单状态很快就能判断问题出在“前端轮询接口后没有正确更新路由”而不是支付网关的问题。养成在关键业务分支里主动设置上下文的习惯比事后猜要强一百倍。3. LogRocket会话回放定位“幽灵Bug”的关键3.1 回放是什么它记录了哪些东西LogRocket接入方式同样简单初始化一行代码搞定import LogRocket from logrocket; LogRocket.init(your-app-id);它录制的内容比我预想的多用户鼠标的移动轨迹、点击、滚动控制台的console.log、console.warn、console.error所有网络请求的URL、请求头、请求体、响应体Redux或Vuex里的状态变化还有JavaScript运行时错误。把这些数据在播放器里串起来你就像坐在用户身后看他操作一样每一步都清清楚楚。我第一次用LogRocket排查问题遇到的Bug很诡异用户组件偶尔渲染空白刷新后恢复但错误上报里什么都没收到。打开回放才发现这个用户是从某个分享链接点进来的页面请求初始数据时用了缓存里的旧结构接口更新后字段对不上渲染函数直接返回了空节点。整个过程只看代码根本没想到因为本地环境的数据都是新的根本不会有旧缓存。这种“幽灵Bug”正是回放工具的主场。它不需要用户配合描述不需要开发去猜复现路径只要回放里有那一段操作记录事实就摆在眼前。3.2 数据脱敏隐私安全是第一道红线回放工具最大的优势是“看得全”但这也是最大的风险。用户可能在表单里输入手机号、银行卡号、身份证号如果这些内容被原样录下来一旦发生数据泄露就是严重的合规事故。所以接入LogRocket的第一步不是写代码而是设计脱敏策略。我当时的配置大致是这样LogRocket.init(your-app-id, { dom: { inputSanitizer: true, textSanitizer: true }, network: { requestSanitizer: request { if (request.url.includes(/api/user/password)) { request.body undefined; } return request; }, responseSanitizer: response { if (response.url.includes(/api/order/list)) { response.body undefined; } return response; } }, samplingRate: 0.5 });inputSanitizer: true会自动对输入框内容打码textSanitizer会把页面上的文本节点脱敏。网络请求体里如果包含密码、验证码之类的字段用requestSanitizer把对应请求的body清掉响应体里如果有用户敏感信息同样在responseSanitizer里处理。有一点特别提醒凡是涉及金融、医疗、未成年人数据的项目接入回放之前一定要做一版隐私合规评审并且在产品侧明确告知用户会话录制策略。技术上能做脱敏但业务上是否允许录制、录制多久、谁能看到回放这些规则也要早定否则后面出了事无从追溯。3.3 把回放和Sentry错误事件打通LogRocket和Sentry官方本身就提供了集成方案核心思路其实是用一个Context把回放链接塞进Sentry事件里。只要你在Sentry初始化之前完成LogRocket的初始化然后在Sentry事件里加入如下代码Sentry.setContext(Session Replay, { url: LogRocket.sessionURL });之后每个Sentry Issue的详情页里都会出现一个回放链接点开就是当时出错的会话录像。这样排障流程就从“打开Sentry看堆栈”直接变成了“打开Sentry看堆栈一边看回放一边复现现场”。我实际排障中这个组合的使用频率相当高。告警进来以后先看回放开头用户从哪个页面进入、做了什么操作再对照Sentry堆栈判断是状态异常还是接口返回结构变动。很多时候光是回放里用户的操作路径就能排除掉一批假设比如用户根本不是从正常入口进来的而是直接通过深链跳转那登录态和初始化逻辑就可能存在盲区。3.4 采样策略与成本控制LogRocket是按录制会话量计费的服务全量录制很快就烧穿预算而且无差别录制大量低价值会话也没有意义。我当时设了一套采样策略核心原则是“核心用户全量录、普通用户抽着录、异常路径优先录”。具体落地时通过URL和用户特征动态判断const isVip getUserLevel() vip; const isHighRiskPage /checkout|payment/.test(window.location.pathname); LogRocket.init(your-app-id, { samplingRate: isVip || isHighRiskPage ? 1.0 : 0.3 });简单说VIP用户和支付、订单这类高风险页面全量录制其他普通页面的访问只按30%比例抽样。这样做既保住了最需要回放能力的场景又控制了成本上涨幅度。当然采样比例的设定要结合业务实际情况调整不能照搬关键是意识到“全量录制”在成本和合规上都不可持续。4. 从告警到定位一套完整的排查流程4.1 收到告警后先做这三件事很多开发收到告警的第一反应是打开IDE开始看代码这个习惯要改。我自己的标准动作是固定的三步先看Sentry Issue的影响范围再看事件自带的上下文信息最后按需打开LogRocket回放。影响范围看什么看这个Issue关联了多少个事件、影响多少用户、集中于哪个发布版本、哪个浏览器环境。如果影响用户数多且集中在最新版本那大概率是新版本回归第一时间回滚或者修复如果只影响某个特定浏览器那方向就变成兼容性问题排查如果用户数很少可以先评估紧急程度再安排处理。上下文信息看什么看事件里挂载的tags和contexts包括页面路由、用户ID、订单号、操作步骤等。这些字段往往直接指出问题发生的业务场景比如“配置了order_id的报错”基本就能锁定是订单流程不需要从堆栈一层层反推。4.2 利用上下文信息过滤“套壳”错误线上最常遇到的一类问题是堆栈看起来一模一样但根因完全不同的错误。比如常见的Cannot read properties of undefined (reading map)可能是接口数据为空可能是字段被改名可能是权限拦截后返回了空对象光靠堆栈根本区分不了。这时候Sentry的上下文数据就派上用场了。每个事件挂的请求URL、页面路由、用户操作阶段能帮你把同一个堆栈下的不同根因快速分流。我遇到过的一个案例首页和详情页都报了同样的Cannot read properties of undefined但因为我在初始化时把页面路由打成了tag一筛就发现首页的报错来自新版推荐位组件详情页的报错来自后端返回的skuInfo为空。两个问题需要在各自组件里单独修如果混在一个Issue里排查成本会高很多。如果Sentry里没有足够的上下文就要直接在代码里补上报把关键变量塞进Sentry.setContext里。排障意识的养成很重要每当遇到一次“查了半天最后发现是某个字段r结构不对”的问题就该回去想一想为什么报错时没有把这个字段的值一起带上来4.3 一个线上问题的完整排查复盘拿一个真实的排查案例来完整走一遍流程。某次活动页上线后告警群弹出一条Sentry通知“TypeError: Cannot read property length of null”影响用户数从个位数快速攀升到两百多发布版本是当天上午的v2.3.1。我先看Sentry的Issue详情发现集中发生在WeChat内嵌浏览器系统版本iOS 15以上占比90%页面路由全部是/activity/detail。再点开一个事件的Contexts看到挂了user_id和share_code两个字段。接着打开关联的LogRocket回放发现用户都是从微信好友分享的卡片点进来的有人正常进入有人是分享链接直接进入。直接通过分享链接进入的会话里页面先请求了一个初始化接口因为登录态是异步获取的初始化接口返回401前端把响应数据缓存成了null后续组件渲染毫不知情地直接调用data.list.length于是白屏。到这里修复方案就很清晰了要么在分享链接进入时强制等待登录态完成再发起初始化请求要么在接口返回状态不是200时抛出一个明确的业务错误而不是返回null。同时为了避免这类问题再静默出现我在初始化请求的Promise链里加了一个结构校验和Sentry主动上报。这个案例里Sentry负责告诉我“哪里坏了”LogRocket则告诉了我“用户是怎么走到的”两个信息一拼十分钟就完成了从告警到定位的闭环。没有这套组合估计又要靠用户描述加远程调试搞一个下午。5. 常见问题与排查技巧实录5.1 Sentry噪音治理与分组修正用了几个月Sentry之后最大的感受是“最需要治理的不是错误的产生而是告警的噪音”。噪音来源主要是三类浏览器扩展注入的脚本错误、应用内已知但暂不修复的低优先级错误、以及分组不合理导致的高频合并。我的做法是三层过滤第一层在beforeSend里主动丢弃一批已知的浏览器噪音比如ResizeObserver loop limit exceeded、Script error.这类跨域且无堆栈信息的事件第二层对已知问题在Sentry后台标记为Ignore或Resolved避免反复触发告警第三层定期做Issue清理长尾问题要么转成技术债务记录要么关闭归档。分组修正方面除了前面说的自定义fingerprint我还养成了一个习惯每周花二十分钟浏览一遍Sentry的Issue列表看到明显分错组的事件就立刻在后台调整合并规则或拆分规则。监控体系不是搭完就完事的它需要持续运营。5.2 LogRocket性能开销与SDK体积LogRocket的SDK体积不小全量在所有路由上加载会影响首屏性能。我当时用动态加载的方式只在核心业务模块里异步引入LogRocket非核心页面不加载这样既保住了关键路径的录制能力又避免了对首屏的拖累。网络开销也要计算。初步算过一笔账一个用户在一个页面上停留五分钟LogRocket录制产生并上报的数据量可能在几百KB到几MB之间这还没有算上网络请求体和响应体。如果responseSanitizer不做限制数据量会暴涨用户体验差且费用高。所以network的脱敏和裁剪不是合规问题也是性能问题。5.3 自建还是使用SaaS服务Sentry有开源版本可以自建我见过很多团队用Docker Compose或者Kubernetes在私有环境里部署Sentry完整掌控数据不用受SaaS配额限制。但自建的代价是运维复杂度Sentry依赖PostgreSQL、Redis、ClickHouse、Kafka等一堆组件升级和调优都需要专门投入。如果你的团队没有专职的基础设施工程师建议直接用SaaS。LogRocket目前没有成熟的开源自建方案国内有类似的竞品比如一些提供“用户行为录制”的平台功能和接入方式大同小异。如果想完全自研回放能力可以关注rrweb这类开源录制工具但要把数据存储、播放器、网络请求关联、以及大规模采样的工程问题解决完成本相当高不是一般团队能扛得住的。5.4 团队落地的建议路径监控体系的建设不建议追求一步到位。我的建议是分三步走。第一步先接Sentry把错误采集和告警跑通团队养成“每次报障先去Sentry查Issue”的习惯解决“从无到有”的问题。第二步接入LogRocket并完成Sentry和回放链接的联动让团队在Issue详情页里能直接点开现场录像解决“可追溯”的问题。第三步做数据消费的深化比如每周错误趋势分析、Top问题清单、性能指标的看板让监控数据真正反哺研发流程解决“可度量”的问题。这套体系刚上线的时候我自己也有点嫌信息源太多觉得维护成本高。直到有一次凌晨三点被告警叫醒在Sentry里看到堆栈、在LogRocket回放里看到用户从分享链接进入的完整路径只用了十来分钟就定位到登录态竞态条件的问题那一刻我才确定——这套投入是值的。最后再分享一个小技巧无论用Sentry还是LogRocket一定要让“错误可读”。错误消息、上下文数据、回放分段的命名都按团队统一的约定来打让任何一个人接到告警都能快速看懂。监控体系真正发挥价值不是靠某一个大神精通工具而是靠整个团队都能顺畅地消费这些信息。