监控平台接入 rrweb:从周会到落地的记录
开周会时,有小伙伴提到一些用户反馈的「奇奇怪怪」的问题很难排查——可能是用户做了某些特殊操作,或者环境、网络有差异,光靠日志和录屏描述很难复现。我想到前几个月了解过的 rrweb(Web 页面录制与回放),正好适合「用户操作可回放、便于事后排查」这类场景。周会后和 LD、组内小伙伴沟通了一轮,大家一致觉得可以尝试在监控平台里接入 rrweb,把录制能力作为现有性能与异常监控的补充。接入过程中我们遇到并整理了五类问题:数据存在敏感信息需与法务确认、数据量大如何裁剪及如何完整上报最有效部分、数据如何压缩(通过 Web Worker)、数据过期机制、为与其他客户端一致使用 ECC 加密时 JS 与 Go/C++ 的坑。本文记录背景和这五类问题的应对与踩坑。
一、为什么在监控平台接 rrweb
监控平台已有性能指标、异常上报,能知道「哪一页、什么时间、报了什么错」,但用户具体做了什么操作、页面当时长什么样,往往只能靠用户描述或额外录屏,排查效率低。rrweb 能在用户侧录制 DOM 变更、鼠标移动、滚动、输入等事件,序列化后上报,在监控后台按 session 回放,便于复现「奇怪」问题。因此我们把 rrweb 作为监控 SDK 的一个可选能力接入:在已上报异常或性能问题的 session 上,可关联同一次访问的 rrweb 录制数据,实现「先看指标与报错,再看回放」的动线。
二、接入遇到的问题与应对
1. 数据存在敏感信息,需与法务确认
录制内容会包含页面上的文案、输入框内容、部分接口返回等,可能涉及用户隐私或商业敏感信息,必须先和法务/合规同事确认:哪些数据可以落库、保留多久、是否需要在采集端就做脱敏。
在技术侧,rrweb 提供了多种脱敏与屏蔽能力,便于在「能回放操作路径」的前提下尽量少录敏感内容:
- 屏蔽整块 DOM:
blockClass(默认rr-block)、blockSelector,被标记的元素在回放时显示为占位块,不记录内部结构和文案。 - 只屏蔽文案:
maskTextClass、maskTextSelector、maskTextFn,保留布局便于排查,但文字以占位或星号替代。 - 输入框:
maskAllInputs可将所有输入框内容录成*;或使用maskInputOptions、maskInputFn只对密码等类型脱敏,其它输入由业务决定是否脱敏。 - 不录制某类元素:
ignoreClass(默认rr-ignore),完全不纳入 events,进一步减小体积。
我们根据法务给出的范围,在 record 配置里统一加了 blockSelector(如敏感区域的选择器)、maskAllInputs: true(或按页按表单单开),并对动态插入的敏感区域通过「手动触发全量快照」使新规则生效。合规通过后再开启全量录制与上报。
2. 数据量大,如何做裁剪与最有效部分上报
rrweb 的 events 体积较大:一次简单操作就可能产生 2–4MB 的 JSON,复杂页面或长时间操作甚至到 10MB+(社区里也有反馈单次录制 JSON 字符串达 20 万~110 万字符)。若全量上报、全量存储,带宽、存储成本和回放加载都会有问题,需要做裁剪和只上报最有效的部分。
采集侧裁剪
- 采样:降低录制频率(如每 N 毫秒取一帧或只在有 DOM 变更时记),在「能还原操作」和「体积」之间折中。
- 屏蔽不重要 DOM:用
blockClass/blockSelector忽略大块且与排查无关的区域(如推荐列表、广告),减少单次快照体积。 - 只录关键时段:与现有监控联动,例如仅在「发生异常的前后 N 分钟」或「性能指标异常」的 session 开启录制,或录制时长上限(如最多 2 分钟),避免所有访问都录。
上报与存储侧
- 全量快照复用:多段录制间可复用同一份全量快照,只上报增量 events,由后端去重或按 session 组装,减少重复存储。
- 按需拉取:列表只展示 session 元信息(时间、页面、是否关联异常),回放时再按 session 拉取完整 events,避免一次加载大量数据。
这样既能保留「有问题的那段」的完整回放,又控制住存储和带宽。
3. 数据如何压缩,以及通过 Web Worker 不阻塞主线程
events 序列化后体积大,必须先压缩再上报,否则耗时耗流量。常见做法是:在浏览器端用压缩库(如 fflate、pako)对整段 events 做压缩,上报压缩后的 blob;回放端再解压后喂给 rrweb 的 replay。
- 压缩策略:推荐整包压缩(整个 events 数组或按 session 打成一块再压),比单条 event 分别压缩率更高,因为算法能利用事件之间的重复结构;rrweb 自带的 pack 配合 gzip 等,整体体积可降到原来的约 1/4~1/10,社区反馈 gzip 可带来约 90% 的体积下降。
- Web Worker:压缩/解压是 CPU 密集操作,放在主线程会卡页面。我们把压缩放在 Web Worker 里:主线程把序列化好的 events 发给 worker,worker 内用 fflate/pako 等压缩后 postMessage 回主线程,再由主线程上报。这样录制和上传阶段主线程不阻塞,用户无感。解压一般在监控后台(Node 或浏览器回放页)做,若在回放页解压且数据很大,可以用时间分片或 worker 解压后再把结果传回主线程,避免长时间阻塞。
我们接入时采用了「record 端 Worker 压缩 → 上报压缩后数据 → 后台/回放页解压」的链路,并配合上面的裁剪与按需拉取,控制单次上报体积和回放加载时间。
4. 数据过期机制
录制数据按 session 存到后端后,若不做过期清理,会随时间和访问量线性增长,存储与合规压力都会上来。需要数据过期机制:约定录制数据保留时长(如 7 天、30 天),由定时任务或 TTL 策略在到期后删除或归档。
我们做法是:在存储层为每条录制记录打上「过期时间」(例如 created_at + 保留天数),由后端定时任务按天扫描并删除已过期的记录;若使用对象存储,可配置 bucket 生命周期规则,按前缀或标签自动删除。同时在前端/配置里明确展示「录制数据保留 X 天」,与法务/合规对「数据保留期限」的约定一致,避免合规风险。
5. 为与其他客户端一致使用 ECC 加密遇到的坑(JS / Go / C++)
监控平台其它客户端(如 iOS/Android、桌面端)对上报数据使用了 ECC(椭圆曲线) 加密,服务端用 Go 解密,其它端用 C++ 实现。为与现有链路一致,Web 端也需要对录制数据做 ECC 加密后再上报,由同一套 Go 服务解密入库。JS 与 Go、C++ 的 ECC 互通成了接入中的一大坑。
主要问题集中在以下几方面:
- 曲线与格式一致:JS 端(如 Web Crypto API 或
@noble/curves)与 Go(crypto/ecdsa/crypto/elliptic)、C++ 端必须选用同一条曲线(如 P-256)和同一套编码约定(公钥/私钥/密文的格式:裸字节、DER、ASN.1、hex、base64 等)。我们一开始 JS 用某库默认的 hex 公钥,Go 端按 DER 解析,导致解密失败,后来统一为「公钥用裸坐标或统一格式(如 04||x||y)在接口里传递」,并在三端文档里写死格式说明。 - 签名/加密用途区分:ECDSA 常用于签名,若要做「加密后上报、服务端解密」,更常见的是 ECDH 协商出共享密钥再 AES-GCM 加密,或使用国密 SM2 等「公钥加密、私钥解密」。我们最终采用「JS 用 ECDH 与后端约定好的公钥算出共享密钥,再用 AES-GCM 加密 payload,Go 端用对应私钥做 ECDH 再 AES-GCM 解密」,与 C++ 客户端的流程对齐。
- 字节序与长度:不同语言对多字节整型、大数的序列化方式不同(大端/小端、是否补零),密钥或 IV 的字节长度必须一致(如 AES-128 用 16 字节 key),否则 Go 解密会报错或得到乱码。我们通过写单测:JS 加密固定明文 → 用 Go 脚本解密,反复对齐格式和长度,才把互通跑通。
建议:在方案选型时就把「JS / Go / C++ 三端加解密格式」写成文档,公钥/密文/IV 的编码方式、曲线名称、库版本都固定下来,避免后期只改一端导致对不上。
三、小结
- 接入过程中我们整理并解决了五类问题:(1)敏感信息——与法务确认范围,用 rrweb 的 block/mask/ignore 做脱敏与屏蔽;(2)数据量大——通过采样、屏蔽无关 DOM、只录关键时段、全量快照复用与按需拉取做裁剪与有效上报(rrweb 单次录制常达 2–10MB 级,需严格控制);(3)压缩与主线程——整包压缩(pack + gzip 可显著减体积)+ 在 Web Worker 中做压缩再上报,避免阻塞主线程;(4)数据过期——约定保留天数,定时任务或存储 TTL 清理过期录制;(5)ECC 加密与 Go/C++ 一致——统一曲线与编码格式,JS 用 ECDH + AES-GCM 与后端/其它客户端对齐,注意字节序与长度,并通过 JS→Go 解密单测反复验证。
如果你也在监控或排查场景里接 rrweb,希望这份「合规 → 裁剪 → 压缩 → 过期 → 加密互通」的整理能少踩一点坑。