- 前端
- 可观测性
- 开发工具
【免费下载链接】rrweb
record and replay the web
本篇技术指南以 rrweb 官方控制台录制插件@rrweb/rrweb-plugin-console-record的 CHANGELOG 为主线,结合插件源码、测试用例与官方 console 使用文档,完整讲解如何在会话录制中采集console输出、如何配置level/lengthThreshold/stringifyOptions/logger四个核心参数,并深入到patch包装、this绑定、console.assert语义、递归防死循环、对象序列化等实现细节,帮助读者理解该插件"如何工作、为什么这样设计、版本迭代改了什么"。
一、插件定位:把开发者控制台搬进回放画面
rrweb 的核心能力是"record and replay the web"。DOM 快照与增量事件只能还原页面长什么样,却无法还原用户操作时代码抛出了什么错误、页面打了哪些日志。@rrweb/rrweb-plugin-console-record正是为了解决这个盲区:它在录制阶段劫持console的各个方法,把日志内容、日志级别、调用堆栈序列化为结构化数据并作为 rrweb 事件输出;回放阶段再由配套插件@rrweb/rrweb-plugin-console-replay在时间轴上逐条还原这些日志。
从项目结构看,该插件位于 packages/plugins/rrweb-plugin-console-record/,遵循 rrweb v2 的插件协议:导出getRecordConsolePlugin(options)工厂函数,返回一个实现了RecordPlugin接口的对象,可直接放入record({ plugins: [...] })。它的源码仅由三个文件构成,职责非常清晰:
- src/index.ts:插件入口、
initLogObserver观察器与replace劫持逻辑; - src/stringify.ts:把任意 JS 值安全序列化为字符串(处理循环引用、
bigint、Event、Node、Error等); - src/error-stack-parser.ts:fork 自 stacktracejs 的
ErrorStackParser与StackFrame,负责解析错误堆栈。
二、快速开始:用默认配置录制控制台
按照 docs/recipes/console.md 的说明,启用控制台录制只需把插件加入record的plugins数组:
import { record } from '@rrweb/record'; import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record'; record({ emit: function emit(event) { // 注意:不要在 emit 中直接调用 console.log,应使用原始方法,避免递归 const defaultLog = console.log['__rrweb_original__'] ? console.log['__rrweb_original__'] : console.log; defaultLog(event); }, plugins: [getRecordConsolePlugin()], });这里有一个官方文档明确警告的关键陷阱:插件会劫持console.log/warn/error等全部方法,如果在emit回调里直接调用console.log(event),就会触发已被包装的console.log→ 再次进入录制逻辑 → 无限递归,最终抛出Uncaught RangeError: Maximum call stack size exceeded。正确做法是通过console.log['__rrweb_original__']拿到被包装前的原始方法再调用。这个__rrweb_original__属性正是由 @rrweb/utils 的 patch 函数 注入的(详见下文第四节)。
三、配置项详解:level、lengthThreshold、stringifyOptions、logger
插件接受一个可选的LogRecordOptions配置对象,官方文档给出了完整参数表:
| key | 默认值 | 说明 |
|---|---|---|
level | 全部 console 方法名(['assert','clear','count','countReset','debug','dir','dirxml','error','group','groupCollapsed','groupEnd','info','log','table','time','timeEnd','timeLog','trace','warn']) | 需要录制的 console 级别白名单,可只保留需要的级别以减少事件体积 |
lengthThreshold | 1000 | 单次会话中最多记录的日志条数,超过后停止记录并发送一条warn提示 |
stringifyOptions | { stringLengthLimit: undefined, numOfKeysLimit: 50, depthOfLimit: 4 } | 对象序列化策略:stringLengthLimit限制单个字符串值的长度;numOfKeysLimit限制对象的键数量上限,超出则直接调用其toString();depthOfLimit限制对象嵌套深度,防止序列化过深导致浏览器 OOM |
logger | window.console | 要劫持的 console 对象。可传入其他执行环境的 console 对象(例如 iframe 的contentWindow.console)实现跨环境录制 |
带自定义配置的完整用法(来自官方文档):
import { record } from '@rrweb/record'; import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record'; record({ emit: function emit(event) { const defaultLog = console.log['__rrweb_original__'] ? console.log['__rrweb_original__'] : console.log; defaultLog(event); }, plugins: [ getRecordConsolePlugin({ level: ['info', 'log', 'warn', 'error'], lengthThreshold: 10000, stringifyOptions: { stringLengthLimit: 1000, numOfKeysLimit: 100, depthOfLimit: 1, }, logger: window.console, }), ], });3.1 配置的合并逻辑(源码层面)
从源码看,配置合并发生在 src/index.ts 的 initLogObserver 内:通过Object.assign({}, defaultLogOptions, options)将用户配置浅合并到默认值上。随后logger参数决定被劫持对象——如果是字符串'console',则取win['console'](win是IWindow,即顶层窗口或 iframe 窗口),否则直接使用传入的 logger 对象。
值得注意的是默认level列表非常完整(src/index.ts#L28-L52),几乎覆盖了console的全部方法,包括group/groupEnd/count/countReset/time/timeEnd/timeLog/table/dir/dirxml/trace/clear。配置层面只做白名单过滤,并不额外判断浏览器是否支持某方法——replace内部会通过if (!_logger[level]) return noop跳过不存在的级别(src/index.ts#L179-L184),因此在不支持某方法的旧浏览器上也不会报错。
四、工作原理:patch 劫持、this 绑定与防递归
4.1 基于 @rrweb/utils 的 patch 机制
插件对每个待录制级别调用patch(该函数原属于插件内部,2.0.0 版本将其迁移至@rrweb/utils以改善打包,见 CHANGELOG 2.0.0 条目)。patch 的实现位于 packages/utils/src/index.ts#L290-L330:
- 若
name in source不存在则返回空清理函数; - 保存
original = source[name]; - 调用
replacement(original)生成包装函数wrapped; - 通过
Object.defineProperties(wrapped, { __rrweb_original__: { value: original } })把原始方法挂到包装函数的__rrweb_original__属性上(这就是前文emit中救命的原始方法来源); - 用
wrapped覆盖source[name],并返回一个恢复函数,用于在stop()时还原原始方法。
插件侧(src/index.ts#L186-L239)把日志逻辑封装在replacement返回的包装函数内,核心流程是:
return patch(_logger, level, (original) => { return (...args) => { original.apply(_logger, args); // 先执行原始 console 方法,保证页面行为不受影响 // ... 收集 trace、payload,回调 cb }; });4.2this绑定修复(CHANGELOG 2.1.3 / 2.1.4 / 2.1.5 核心变更)
包装函数调用原始方法时使用original.apply(_logger, args),显式将this绑定为 logger 对象。这正是 CHANGELOG 2.1.3 修复的关键问题:在更早的版本中,如果包装函数以错误的接收者调用原方法,在严格上下文(例如浏览器扩展的 content script)里会抛出TypeError: Illegal invocation。
测试 test/this-binding.test.ts 精确复现了这一场景:测试构造了一个fakeLogger,其log方法会捕获调用时的this,并模拟"以错误 receiver 调用就抛异常"的原生行为;随后通过插件包装后调用fakeLogger.log('hello'),断言capturedThis === fakeLogger,从而验证包装函数始终以 logger 为this调用原方法。
4.3inStack防递归标志
日志被包装后,如果stringify序列化过程(例如触发 Vue 响应式 Proxy 的 getter)反过来调用了某个 console 方法,就会产生"录日志 → 序列化 → 又打日志 → 又录日志"的无限循环。为此包装函数引入inStack标志(src/index.ts#L114、src/index.ts#L198-L236):
- 进入日志收集逻辑前置
inStack = true; - 若在
inStack期间又有 console 方法被调用,直接return丢弃,避免无限循环; - 收集结束在
finally中复位inStack = false。
对应的测试用例 "should handle recursive console messages"(test/index.test.ts#L53-L91)构造了一个带 Proxy 的递归对象,console.log该对象时会触发 Proxy 的get并再次console.warn,测试断言最终快照中只有 1 条 console 日志,而不是多条。
4.4console.assert语义修正(CHANGELOG 2.0.0 核心变更)
原生console.assert只在断言为假(falsy)时才输出日志,第一个参数只用于判断,不参与输出。CHANGELOG 2.0.0(PR #1530)修正了插件此前的错误行为,源码中体现在两处:
- src/index.ts#L193-L196:
if (level === 'assert' && !!args[0]) return;—— 断言为真时直接跳过录制; - src/index.ts#L209-L210:
const argsForPayload = level === 'assert' ? args.slice(1) : args;—— 录制 payload 时剔除第一个断言参数。
集成测试 test/index.test.ts#L96-L101 用console.assert(0 === 0, 'should not log assert')与console.assert(false, 'should log assert')分别验证"真值不记录、假值记录"。
4.5 堆栈采集与日志条数阈值
每条日志都会通过ErrorStackParser.parse(new Error())采集当前调用堆栈,并.splice(1)去掉被劫持的 log 函数自身这一帧(src/index.ts#L205-L207)。error-stack-parser.ts 内维护了 Firefox/Safari 与 Chrome/IE 两套堆栈正则,兼容不同浏览器格式。
日志计数受lengthThreshold控制:当累计记录数小于阈值时正常回调cb;恰好等于阈值时发送一条warn级别的提示'The number of log records reached the threshold.';超过后不再记录(src/index.ts#L215-L231)。序列化或收集过程中若抛出异常,包装函数会回退到原始方法输出'rrweb logger error:',保证录制逻辑的异常不会破坏页面本身的 console 输出。
五、未被劫持的全局错误:error 与 unhandledrejection
除了劫持 console 方法,插件还以事件监听的方式捕获两类"不是 console 调用"的错误(src/index.ts#L117-L166),且仅当level配置中包含'error'时启用:
error事件:捕获全局window上的错误,通过ErrorStackParser.parse(error)解析堆栈,payload 为错误消息;unhandledrejection事件:处理未捕获的 Promise rejection。若event.reason是Error实例,payload 形如Uncaught (in promise) ${error.name}: ${error.message};否则额外序列化event.reason本身。
这两类事件在stop()时通过removeEventListener完整卸载。录制输出的数据结构为:
type LogData = { level: LogLevel; // 日志级别 trace: string[]; // 堆栈帧字符串数组 payload: string[]; // 序列化后的日志内容 };插件标识PLUGIN_NAME = 'rrweb/console@1'(src/index.ts#L243),回放端据此识别日志事件。
六、序列化策略:stringify 的安全细节
日志内容必须序列化为字符串才能进入 rrweb 事件流,src/stringify.ts 的stringify基于JSON.stringify的 replacer 实现了多种保护:
- 循环引用去环:fork 自
json-stringify-safe,遇到循环引用输出[Circular ~]或[Circular ~.path]; bigint:输出数字加n后缀,避免JSON.stringify直接抛错;Event对象:遍历其属性,对数组值(如path、composedPath等节点数组)用pathToSelector转换成tagName:eq(index)>...形式的 CSS 路径选择器(stringify.ts#L12-L47),避免把整个 DOM 节点序列化进日志;Node/HTMLElement:元素输出outerHTML,其他节点只输出nodeName;Error对象:优先输出完整stack,并附加End of stack for Error object标记;- 超限保护:对象键数超过
numOfKeysLimit、或嵌套深度超过depthOfLimit(isObjTooDeep递归检测,depth 为 0 视为超深)、或值为function时,改走toString()并受stringLengthLimit截断(超出部分以...结尾)。
这套策略同时服务于两个目标:控制事件体积(numOfKeysLimit/depthOfLimit/stringLengthLimit),以及避免序列化过程触发浏览器 OOM(depth 限制即源于 rrweb 的 issue #653)。
七、配套回放:@rrweb/rrweb-plugin-console-replay
录制只是前半段。若事件流中包含 console 类型日志,回放端会自动播放(docs/recipes/console.md):
import { Replayer } from '@rrweb/replay'; import { getReplayConsolePlugin } from '@rrweb/rrweb-plugin-console-replay'; const replayer = new Replayer(events, { plugins: [ getReplayConsolePlugin({ level: ['info', 'log', 'warn', 'error'], }), ], }); replayer.play();回放插件的两个选项:level(默认全部级别,指定要回放的日志级别)与replayLogger(默认是基于 console 的对象;可通过实现 ReplayLogger 接口 在模拟浏览器控制台中回放日志,例如渲染成开发者工具风格的日志面板)。
八、版本演进:从 CHANGELOG 看插件迭代脉络
该插件的 CHANGELOG 完整记录了自 v2.0.0-alpha 以来的演进,可归纳为三个主题:
1. 包拆分与产物格式重构(2.0.0,随 rrweb 主版本大版本发布)
- 插件从 rrweb 主包拆出为独立 npm 包
@rrweb/rrweb-plugin-console-record(PR #1497),与@rrweb/packer、rrweb-plugin-console-replay等一批插件一同独立发布; - 发布产物结构变化:所有
.js文件改为 ES Module(现代浏览器、Node.js 与支持 ESM 的打包器可用);新增.cjs与.umd.cjs产物(后者将全部依赖打成单文件,供<script>标签直接引入);额外提供/umd/输出目录以支持带.js扩展名的 UMD 文件,避免与 package.json 中"dist 下所有 .js 均为模块"的约定冲突(PR #1704)。可查看该包的 package.json 中exports/main/module/unpkg/jsdelivr字段确认各产物的入口; - 版本号与
rrweb、@rrweb/utils保持同步(2.0.0 → 2.0.1 → 2.1.x 一路对齐)。
2. 行为正确性修复(2.0.0 / 2.1.3)
console.assert语义修正:只捕获断言为假时的日志(PR #1530,详见 4.4 节);this绑定修复:包装函数以 logger 为this调用原始方法,消除严格上下文中的 "Illegal invocation"(PR #1904,详见 4.2 节)。
3. 打包与依赖优化
patch函数移入@rrweb/utils统一管理,改善各插件的打包体积与一致性(PR #1631);peerDependencies声明rrweb: ^2.1.1与@rrweb/utils: ^2.1.1,与当前版本(2.1.5)保持兼容。
九、测试与验证:插件质量如何保证
插件的自动化测试集中在 test/ 下,由 vitest + puppeteer + Vite dev server 驱动:
- index.test.ts:
should handle recursive console messages:验证 Proxy 递归对象场景下不会产生无限循环与重复日志;should record console messages:覆盖全部 console 级别(assert、count、countReset、debug、dir、dirxml、group、groupCollapsed、info、log、table、time、timeEnd、timeLog、trace、warn、clear)、Error对象参数,以及iframe 内 console 的跨环境录制(page.frames()[1].evaluate中调用console.log('from iframe')),其快照断言在 test/snapshots/index.test.ts.snap;
- this-binding.test.ts:以 jsdom 环境验证 2.1.3 的
this绑定修复(详见 4.2 节); - stringify.test.ts 与 error-stack-parser 相关测试:分别覆盖序列化边界与堆栈解析格式。
运行插件自身的测试与类型检查:
# 在 packages/plugins/rrweb-plugin-console-record 目录下 yarn test # vitest run yarn check-types # tsc -noEmit十、使用建议与注意事项
emit中务必使用__rrweb_original__:这是官方文档反复强调的坑,否则必然栈溢出;- 控制事件体积:生产环境建议按需裁剪
level(例如只保留error/warn/log),并根据业务对象复杂度设置stringifyOptions(numOfKeysLimit与depthOfLimit调小、stringLengthLimit设置上限),以降低事件存储成本; lengthThreshold兜底:默认 1000 条足以防止日志洪峰拖垮事件流,超出时会收到阈值告警warn事件;- iframe 场景:插件天然支持传入 iframe 的 console 对象,跨 iframe 的日志可通过
logger参数一并纳入录制,相关行为已由测试覆盖; - 版本对齐:该插件与
rrweb、@rrweb/utils同步发版(当前 2.1.5),升级时建议三者一同升级,并留意 CHANGELOG 中标注的破坏性变更(如 2.0.0 的产物路径调整,若直接引用 dist 文件需改为.umd.cjs等新命名)。
- 前端
- 可观测性
- 开发工具
【免费下载链接】rrweb
record and replay the web
相关推荐
rrweb Canvas WebRTC 回放插件:@rrweb/rrweb-plugin-canvas-webrtc-replay 演进与实战指南
rrweb Canvas WebRTC 回放插件:@rrweb/rrweb plugin canvas webrtc replay 演进与实战指南 本篇技术指南
前端可观测性开发工具rrweb 控制台录制与回放:console 记录/回放插件的配置、原理与实战指南
rrweb 控制台录制与回放:console 记录/回放插件的配置、原理与实战指南 本篇指南围绕 rrweb 提供的 console 录制与回放插件展开,讲解如
前端可观测性开发工具rrweb 顺序 ID 回放插件实战:@rrweb/rrweb-plugin-sequential-id-replay 原理与配置详解
rrweb 顺序 ID 回放插件实战:@rrweb/rrweb plugin sequential id replay 原理与配置详解 在基于 rrweb ht
前端可观测性开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考