使用 rrweb 还原用户的操作,监听线上 BUG
哥们最害怕的时刻莫过于:测试环境一切正常,一上线用户就报错。
更糟糕的是,用户反馈往往只有一句:“页面打不开了”或者“点击没反应”。当我们试图复现时,却发现自己无论怎么操作都无法触发 Bug。用户不愿意提供详细步骤,客服也传达不清楚,最后只能对着日志干瞪眼。
如果有这样一种技术,能像“时光倒流”一样,完整还原用户出错前的每一步操作,那该多好?
一、为什么我们需要监控与回放?
1.1 沉默的流失
在产品运营中,愿意主动上报 Bug 的用户是极少数。 绝大多数用户遇到体验问题或 Bug 时,选择是直接关闭页面,卸载应用,然后永远不再回来。我们失去了挽留他们的机会,甚至不知道他们为什么离开。
1.2 “在我这里没问题”
前端开发的口头禅:“我没复现这个问题啊,tmd用户怎么操作的”。 因为线上环境太复杂了:
- 用户的网络波动
- 特定的浏览器版本
- 特殊的操作顺序
- 并发请求的竞争条件
1.3 解决方案:从“猜”到“看”
传统的日志监控(如 Sentry)只能告诉我们哪里错了(Error Stack),但很难告诉我们用户是怎么操作才导致错的。 会话回放技术填补了这个空白。
二、什么是 rrweb?
rrweb (record and replay web) 是一个开源的 Web 会话录制与回放库。
2.1 它不是视频录屏
很多人以为 rrweb 是像 OBS 一样录制视频流。这么想你就错了
- 视频录屏: 录制像素点,文件大,无法检索,无法分析 DOM 结构。
- rrweb 回放: 录制 DOM 变更事件(Mutation)。它记录的是“在什么时间,哪个节点,发生了什么变化”。
2.2 核心优势
- 体积极小: 传输的是 JSON 文本,经过压缩后,几分钟的操作可能只有几十 KB。
- 可检索: 因为记录的是 DOM 结构,你可以搜索页面上的文字来定位片段。
- 隐私可控: 可以在录制时配置忽略敏感输入框(如密码、银行卡号)。
- 跨平台: 录制的文件可以在任何地方回放,甚至离线回放。
三、核心原理:DOM 的序列化与反序列化
rrweb 的工作原理可以概括为:“把页面上的状态变成 JSON 数据,然后拿着这个 JSON 数据再转换成页面”。
3.1 录制阶段(Record)
rrweb 在用户浏览器中运行,主要依赖以下技术:
- MutationObserver: 监听 DOM 树的变化(节点新增、删除、属性修改、文本变化)。
- 事件监听: 监听鼠标移动、点击、滚动、输入等交互事件。
- 快照(Snapshot): 在录制开始时,通过遍历 DOM 树生成一份初始 HTML 结构的 JSON 快照。
- 增量数据: 之后只记录变化的部分(Incremental Data)。
数据流示例:
// 初始快照 { "type": 2, "data": { "node": { "tagName": "html", ... } } } // 增量操作:用户点击了按钮 { "type": 3, "data": { "source": 1, "positions": [{ "x": 100, "y": 200 }] } } // 增量操作:DOM 发生变化 { "type": 3, "data": { "source": 2, "adds": [...], "removes": [...] } }3.2 回放阶段(Replay)
- 创建一个干净的 iframe 或容器。
- 读取初始快照,重建 DOM 树。
- 按照时间戳顺序,依次重放增量事件(模拟鼠标移动、触发 DOM 变更)。
- 我们排查问题的时候看到的就像是在看视频一样。
四、架构设计与实施步骤
要实现一套完整的回放系统,我们需要三部分组成:客户端 SDK、数据存储服务、回放管理平台。
4.1 基本流程
- 用户端: 用户访问页面 -> SDK 自动录制 -> 触发异常或会话结束 -> 数据上报。
- 服务端: 接收数据 -> 压缩存储(如 MongoDB 或 OSS)。
- 管理端: 开发人员登录后台 -> 查看错误列表 -> 点击“查看回放” -> 加载数据播放。
4.2 代码实现 Demo
npm 地址 : https://www.npmjs.com/package/rrweb
1. 安装 rrweb
npm install rrweb2. 前端录制与上报
import rrweb from 'rrweb'; // 开始录制 rrweb.record({ emit(event) { // 将事件数据发送到你的服务器 // 建议:不要每个事件都发,要在本地缓存,攒够一定数量或时间后批量发送 fetch('/api/record', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ events: [event], sessionId: getSessionId() }) }); }, // 隐私保护:忽略包含 .sensitive 类的元素 blockClass: 'sensitive', // 忽略脚本标签 ignoreClass: 'ignore', }); // 模拟获取会话 ID,用于关联错误日志 function getSessionId() { return localStorage.getItem('session_id') || generateUUID(); }3. 后端接收(Node.js 示例)
app.post('/api/record', (req, res) => { const { events, sessionId } = req.body; // 将 events 存入数据库,key 为 sessionId db.save(sessionId, events); res.send({ success: true }); });4. 管理端回放
import rrweb from 'rrweb'; // 从服务器获取录制数据 const events = await fetch(`/api/replay?sessionId=${id}`).then(r => r.json()); new rrweb.Player({ target: document.getElementById('replay-container'), data: events, });五、关键局限与解决方案(避坑指南)
虽然 rrweb 很强大,但它不是万能的。“它只记录了用户的界面操作,没法监控到具体出了什么问题,它无法录制 js 里面的代码。”
5.1 局限一:无法直接捕获 JS 错误堆栈
rrweb 记录的是“现象”,不是“病因”。如果页面白屏了,回放可能只看到页面加载了一半然后不动了,但不知道是哪个变量 undefined 导致的。
解决方案
- rrweb + 错误监控 (把报错信息一起传到日志中):
- 当 JS 报错时,错误监控 SDK 捕获错误堆栈。
- 将当前的
sessionId附带在错误日志中一起上报。 - 在查看错误详情时,提供一个“查看回放”的按钮,通过
sessionId拉起 rrweb 回放。 这样你就既知道了*为什么错(堆栈),又知道了怎么错的(操作路径)。*
5.2 局限二:性能开销
全量录制所有 DOM 变化和鼠标移动,可能会占用用户主线程,导致页面卡顿。
解决方案:
- 采样录制: 只对 10% 的用户开启录制,或者仅在检测到错误时开启(需配合预加载)。
- 节流(Throttle): 限制鼠标移动事件的录制频率(如每 500ms 记录一次)。
- 暂停录制: 当页面切到后台(visibilitychange)时暂停录制。
5.3 局限三:隐私安全
如果不小心录下了用户的密码、身份证号或聊天记录,会引发严重的法律合规问题(GDPR/个人信息保护法)。
解决方案:
- 配置
blockClass: 强制将敏感输入框标记为不录制(rrweb 默认会隐藏input[type="password"],但其他敏感信息需手动配置)。 - 数据脱敏: 在上传前,对 JSON 数据中的特定字段进行清洗。
- 访问控制: 回放平台必须权限严格,只有授权人员可查看,且查看日志需留痕。
5.4 局限四:数据量与存储
高频操作会产生大量 JSON 数据。
解决方案:
- 压缩: 前端使用
pako(gzip) 压缩后再上传。 - 生命周期: 设置数据保留期限(如只保留 7 天),过期自动删除。
- 触发式上传: 平时只存在本地 IndexedDB,只有发生 Error 或用户主动反馈时,才上传之前的录制数据。
六、总结
前端监控体系的建设,是衡量一个团队工程化成熟度的重要标志。
- 错误监控(Sentry 等) 告诉我们 What(出了什么错)。
- 会话回放(rrweb) 告诉我们 How(用户怎么操作的)。
- 性能监控 告诉我们 Where(哪里慢)。
将 rrweb 集成到我们的监控体系中,它能极大地缩短 Bug 排查时间,减少开发与测试的沟通成本,更重要的是,它能让我们真正站在用户的视角去理解产品体验。
最后提醒: 技术虽好,隐私先行。在上线回放功能前,请务必更新隐私协议,并获得用户的同意。