rxdb-plugin-replay
@aiao/rxdb 的会话录制回放插件,默认不录,由应用显式开始。它用 rrweb 录下页面 DOM,事件写进一个独立的录制库,被录的应用库里不多一行数据。回放时间轴上会标出工作树的 commit,点一下标记,就能把工作树恢复到那次提交。
rrweb 钉在精确版本 2.1.7。回放组件按需 import('rrweb'),所以不打开回放视图,rrweb 回放端就不会被加载。
安装
pnpm add @aiao/rxdb @aiao/rxdb-plugin-replay rxjs
# 需要 commit 标记与恢复时再装(可选 peer)
pnpm add @aiao/rxdb-plugin-working-tree
用法
import { RxDB, SyncType } from '@aiao/rxdb';
import { RxDBAdapterSqlite } from '@aiao/rxdb-adapter-sqlite-wasm';
import { rxDBPluginReplay } from '@aiao/rxdb-plugin-replay';
// 录制库工厂:返回另一个 RxDB,不要返回被录的应用库
const createRecordingDb = entities =>
new RxDB({
dbName: 'my-app-replay',
entities: [...entities],
multiInstance: false,
sync: `{ local: { adapter: 'sqlite-wasm' }, type: SyncType.None }`
}).adapter('sqlite-wasm', async db => new RxDBAdapterSqlite(db, sqliteOptions));
db.use(rxDBPluginReplay, {
createRecordingDb,
limits: { sessionBytes: 16 * 1024 * 1024, storeBytes: 128 * 1024 * 1024 },
record: `{ maskTextSelector: '.secret' }`
});
await db.connect('sqlite-wasm');
const sessionId = await db.replay.start();
// …用户操作…
await db.replay.stop();
const sessions = await db.replay.listSessions(); // 状态 / 事件数 / 体积
const events = await db.replay.readEvents(sessionId);
录制库工厂在每个连接纪元里只调用一次,时机是第一次需要存储时。返回的库如果还没 init(),插件会先替它 init(),再 connect(),作用域释放时一并 destroy()。工厂抛错或返回的库连不上时,插件抛 RxDBReplayError('recording_db_unavailable'),下一次调用会重新调用工厂。
db.replay 是 ReplayManager,成员如下:
- 录制:
state$、start()、stop() - 读取:
listSessions()、readEvents(id, range?)、listCommitMarkers(id)、exportSession(id) - 空间:
deleteSession(id)、usage() - 恢复:
restoreToCommit(commitId)
页面刷新时,录制会话会通过 sessionStorage 暂存续上同一个 id:还没冲刷的事件不会丢,暂存不完整时写一条 gap 标记。
体积上限
上限按事件 JSON.stringify 后的 UTF-8 字节计,两项都可配置:
| 选项 | 默认 | 超出时 |
|---|---|---|
limits.sessionBytes | 16 MiB | 该会话停录,写一条 truncated 标记(session_limit),会话状态变为 truncated |
limits.storeBytes | 128 MiB | start() 抛 RxDBReplayError('store_limit') |
插件绝不自动删除旧录像,释放空间只能调用 deleteSession()。
脱敏
maskAllInputs默认为true,输入框里的值只录成同样长度的*。- 给元素加上
data-rxdb-replay-block属性(REPLAY_BLOCK_SELECTOR),它在录像里就只剩一个同尺寸的占位块。这个选择器总会并入你传的record.blockSelector。 record透传 rrweb 的maskInputOptions/maskTextSelector/maskTextClass/blockSelector/blockClass/ignoreSelector,含义以 rrweb 文档为准。
commit 标记与恢复
被录的库装了 @aiao/rxdb-plugin-working-tree 时,录制中的每次提交都会写一条 commit 标记(rrweb Custom 事件,tag 为 rxdb-replay:commit)。标记来自工作树门面的 commits$。
restoreToCommit(commitId) 每次调用时都现取工作树凭据,再调 workingTree.restore()。被拒绝时用 replayRestoreHint(reason) 取提示文案,可能的 reason:
conflictdirty_working_treeincompatible_schemaunreachable_target
没装工作树插件时抛 working_tree_unavailable。
回放
mountReplayer(host, { replay, sessionId, initialTime, onTimeChange, onCommitRestore }) 会在 host 里挂出以下内容:
- rrweb 回放 iframe
- 播放 / 暂停按钮
- 时间轴
- commit 标记列表
它返回 play / pause / seek / update / destroy 句柄。三个框架的封装 API 对称:
- Angular:
@aiao/rxdb-plugin-replay-angular - React:
@aiao/rxdb-plugin-replay-react - Vue:
@aiao/rxdb-plugin-replay-vue
错误
插件的业务错误统一抛 RxDBReplayError,请按 code 分支处理,不要匹配 message。可能的 code:
no_domalready_recordingstore_limitrecording_db_unavailablesession_not_foundsession_recordingworking_tree_unavailableinvalid_markernot_installed
测试
以下命令在仓库根目录执行;pnpm 的 preinstall 门禁要求 Node 26。
包内测试
pnpm nx test rxdb-plugin-replay # node 环境:上限、续录、commit 标记、恢复、选项校验、三框架 parity 契约
pnpm nx run rxdb-plugin-replay:test-browser # 真实 chromium:*.browser.spec.ts(rrweb 录制 / 回放、脱敏)
REPLAY_BENCH=1 pnpm nx test rxdb-plugin-replay -- store.bench # 写入基准,中位数 < 100 ms;默认不跑
test 只跑 node 环境,*.browser.spec.ts 只有 test-browser 会跑:改了录制、回放或脱敏,记得跑后者。
在 demo 里手动验证
pnpm nx serve dev-rxdb-angular,打开http://localhost:4200/replay,打开录制开关(页面会重新加载)。- 在
/todo建两条 Todo,各提交一次,回到/replay停止录制。 - 在会话列表里点「回放」:时间轴可以拖动,两个 commit 标记按提交顺序排列。
- 点第一个标记:回放跳到第一次提交那一刻,状态区显示
Restored …,/working-tree上工作树变脏。 - 在「脱敏演示」的输入框里打字再回放:输入只剩同样长度的
*,data-rxdb-replay-block区块只剩占位块。 - 关掉开关后重新加载:DevTools Network 里不出现
rrweb与rxdb-plugin-replay的 chunk,IndexedDB 里不新建*-replay库。
demo e2e
pnpm nx e2e dev-rxdb-angular-e2e -- replay.spec
apps/dev-rxdb-angular-e2e/src/replay.spec.ts 覆盖:刷新后续录同一会话、录制不进应用库、commit 标记与恢复、输入遮蔽与 block 占位。
与 e2e 失败现场的关系
Angular e2e 默认不打开录制,e2e 失败时留下的现场里没有 rrweb 录像:
| 产物 | 何时产生 | 怎么看 |
|---|---|---|
Playwright trace(trace.zip) | 用例失败时保留,通过的用例不留 | npx playwright show-trace <trace.zip>,或在 HTML 报告里点 Trace |
数据归档(附件 rxdb-failure-archive) | 用例失败时自动导出;failure-archive.spec.ts 主动导出 | demo 的 /failure-archive 页导入 |
两者都在 apps/dev-rxdb-angular-e2e/test-output/playwright/ 下:trace 在 output/<用例>/,附件在 report/data/*.bin(以 RXDBBAK 开头)。用
npx playwright show-report apps/dev-rxdb-angular-e2e/test-output/playwright/report 打开报告,能看出每个附件属于哪个用例。每次跑 e2e 都会覆盖这个目录。
导入页的验证(http://localhost:4200/failure-archive,页面根节点的 data-phase 反映状态):
| 场景 | 操作 | 预期 |
|---|---|---|
| 解析 | 选择归档文件 | idle → parsed,显示库名、创建时间、格式版本 |
| 导入 | 点「导入并打开」 | importing 后整页重载,顶部出现导入库提示条 |
| 非法文件 | 选一个非归档文件 | error,显示错误码,没有「打开该库」 |
| 重复导入 | 「回到默认库」后再导入同一份归档 | target_not_empty,「打开该库」打开之前导入的库 |
| 目标被占用 | 导入库还在别的标签页开着时再导入 | target_busy,同样提供「打开该库」 |
- 归档恢复到原库名:在生成归档的同一个浏览器 profile 里导入会直接
target_not_empty,手动测请用无痕窗口或另一个 profile。 - 导入后应用一直打开导入库(localStorage 键
rxdb-demo-imported-db-name),点提示条上的「回到默认库」恢复。 - 在导入库上同样可以打开
/replay的录制开关,录下自己复现的过程;commit 标记照常和工作树联动。
想看一个完整的失败现场,临时让某条 Angular e2e 用例失败(例如断言一个不存在的 test id),跑完后同一个用例下会同时有
trace、rxdb-failure-archive 与 rxdb-failure-summary:先看 trace 了解「做了什么」,再导入归档查看「数据当时的状态」。
文档
- 仓库主页:https://github.com/aiao-io/rxdb
- 插件指南见项目文档站
License
Classes
| Class | Description |
|---|---|
| ReplayEventRecord | 一条 rrweb 事件(表 replay_event),只存在于录制库。 |
| ReplaySessionRecord | 录制会话(表 replay_session),只存在于录制库。 |
| RxDBPluginReplay | @aiao/rxdb-plugin-replay 主类:每个连接纪元建一个 ReplayRuntime,纪元释放时停录制并销毁录制库。 |
| RxDBReplayError | @aiao/rxdb-plugin-replay 抛出的全部业务错误。 |
Interfaces
| Interface | Description |
|---|---|
| ReplayCommitMarker | 录像里的一个 commit 标记(listCommitMarkers())。 |
| ReplayerCommitRestoreEvent | 点 commit 标记恢复的结果;restoreToCommit() 抛错时 reason 为 'error'。 |
| ReplayerHandle | mountReplayer 返回的句柄。非就绪态 play / pause / seek 是空操作,destroy() 之后一切都是空操作。 |
| ReplayerOptions | 回放器的输入(git show 2e820521:specs/005-us-909-session-replay/contracts/replayer-component.md §1)。 |
| ReplayEventRange | readEvents() 的时间区间(闭区间,epoch ms)。 |
| ReplayManager | 录制回放入口(rxdb.replay)。 |
| ReplayMarkerPayloads | 各标记 tag 对应的 payload 形状。 |
| ReplaySessionExport | exportSession() 的结果:会话概要 + 按 seq 排好的全部事件。 |
| ReplaySessionInfo | 一个录制会话的概要(listSessions() / exportSession())。 |
| ReplayUsage | 录制库用量(usage())。 |
| ResolvedReplayOptions | 补齐默认值、校验过的选项。 |
| RxDBReplayOptions | rxdb.use(rxDBPluginReplay, options) 的选项。 |
Type Aliases
| Type Alias | Description |
|---|---|
| ReplayMarker | parseReplayMarker 的结果。 |
| ReplayMarkerTag | 本插件的标记 tag。 |
| ReplayRecordingDbFactory | 录制库工厂:收到本插件的实体清单,返回一个独立的 RxDB(不要是被录的应用库)。 |
| ReplayRestoreRejection | restoreToCommit() 被拒的四种原因(与工作树 restore() 的 reason 同值)。 |
| ReplayRestoreResult | restoreToCommit() 的结果:工作树 restore() 结果里回放用得上的那部分。 |
| ReplaySessionStatus | 会话行的状态:录制中 → 已停止 / 已截断(终态)。 |
| ReplayState | rxdb.replay.state$ 的值。 |
| ReplayTruncatedCode | 会话被截断的原因:单会话超限或录制库总量超限。 |
| RxDBReplayErrorCode | RxDBReplayError 的可判别原因。 |
| RxDBReplayRecordOptions | 透传给 rrweb record() 的脱敏 / 屏蔽选项子集;含义照 rrweb 文档。 |
Variables
| Variable | Description |
|---|---|
| REPLAY_BLOCK_SELECTOR | 永远并入 record.blockSelector 的选择器:给元素加上 data-rxdb-replay-block 属性, 它在录像里就只剩一个同尺寸的占位块。应用不需要懂 rrweb 也能标出敏感区块。 |
| REPLAY_ENTITIES | 录制库需要的全部实体;createRecordingDb 工厂收到的就是这一组,原样交给 new RxDB({ entities })。 |
| REPLAY_MARKER_TAGS | 本插件写进录像的三种标记 tag。 |
| rxDBPluginReplay | 录制回放插件工厂:rxdb.use(rxDBPluginReplay, { createRecordingDb })。 |
Functions
| Function | Description |
|---|---|
| mountReplayer | 在 host 里渲染一个会话的回放器:rrweb 回放、播放 / 暂停、时间轴、commit 标记(点了恢复工作树)与恢复结果。 |
| parseReplayMarker | 把一条 rrweb 事件解析成本插件的标记。 |
| replayRestoreHint | restoreToCommit() 被拒时给用户看的一句英文提示(git show 2e820521:specs/005-us-909-session-replay/research.md D8)。 |