diff --git a/.cache/.gitkeep b/.cache/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/.changeset/eighty-teachers-smash.md b/.changeset/eighty-teachers-smash.md
new file mode 100644
index 0000000000..e26a2157b4
--- /dev/null
+++ b/.changeset/eighty-teachers-smash.md
@@ -0,0 +1,8 @@
+---
+"@rrweb/rrweb-plugin-canvas-webrtc-replay": patch
+"@rrweb/rrweb-plugin-sequential-id-replay": patch
+"@rrweb/rrweb-plugin-console-replay": patch
+"rrweb": patch
+---
+
+Export `ReplayPlugin` from rrweb directly. Previously we had to do `import type { ReplayPlugin } from 'rrweb/dist/types';` now we can do `import type { ReplayPlugin } from 'rrweb';`
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000000..280cf719b8
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,13 @@
+.DS_Store
+node_modules
+/build
+/dist
+/package
+.env
+.env.*
+!.env.example
+
+# Ignore files for PNPM, NPM and YARN
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
diff --git a/.eslintrc.js b/.eslintrc.js
index 6172eedf77..16d975a68a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,3 +1,4 @@
+// TODO: add .eslintignore. More info: https://bobbyhadz.com/blog/typescript-parseroptions-project-has-been-set-for
module.exports = {
env: {
browser: true,
@@ -16,7 +17,7 @@ module.exports = {
ecmaVersion: 'latest',
sourceType: 'module',
tsconfigRootDir: __dirname,
- project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'],
+ project: ['./tsconfig.eslint.json', './packages/**/tsconfig.json'],
},
plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc', 'jest', 'compat'],
rules: {
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index cf64ccca07..7fe6a4bd54 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -19,7 +19,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: lts/*
-
+
- name: Install Dependencies
run: yarn install --frozen-lockfile
@@ -27,9 +27,10 @@ jobs:
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all
- name: Check types
- run: yarn turbo run check-types
+ run: yarn check-types
- name: Run tests
+ # run: PUPPETEER_EXECUTABLE_PATH=${{ steps.setup-chrome.outputs.chrome-path }} PUPPETEER_HEADLESS=true xvfb-run --server-args="-screen 0 1920x1080x24" yarn test
run: PUPPETEER_HEADLESS=true xvfb-run --server-args="-screen 0 1920x1080x24" yarn test
- name: Upload diff images to GitHub
diff --git a/.gitignore b/.gitignore
index 97ae63a201..7384b1a8ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,14 +13,25 @@ temp
.DS_Store
+# output of `yarn build`
build
dist
+# turbo cache
.turbo
+# needed to store puppeteer binaries
+.cache/*
+!.gitkeep
+
# emacs working files end in a tilde
*~
# `.yarn/install-state.gz` is an optimization file that you shouldn't ever have to commit.
# It simply stores the exact state of your project so that the next commands can boot without having to resolve your workspaces all over again.
-.yarn/install-state.gz
\ No newline at end of file
+.yarn/install-state.gz
+
+
+# for vite
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
diff --git a/.puppeteerrc.cjs b/.puppeteerrc.cjs
new file mode 100644
index 0000000000..0831229fc5
--- /dev/null
+++ b/.puppeteerrc.cjs
@@ -0,0 +1,10 @@
+const { join } = require('path');
+
+/**
+ * @type {import("puppeteer").Configuration}
+ */
+module.exports = {
+ // Changes the cache location for Puppeteer.
+ cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
+ browserRevision: '115.0.5763.0',
+};
diff --git a/.vscode/rrweb-monorepo.code-workspace b/.vscode/rrweb-monorepo.code-workspace
index ee31ea35ee..98338cf192 100644
--- a/.vscode/rrweb-monorepo.code-workspace
+++ b/.vscode/rrweb-monorepo.code-workspace
@@ -24,18 +24,76 @@
"name": "rrweb-snapshot (package)",
"path": "../packages/rrweb-snapshot"
},
+ {
+ "name": "@rrweb/all",
+ "path": "../packages/all"
+ },
+ {
+ "name": "@rrweb/record",
+ "path": "../packages/record"
+ },
+ {
+ "name": "@rrweb/replay",
+ "path": "../packages/replay"
+ },
+ {
+ "name": "@rrweb/types",
+ "path": "../packages/types"
+ },
+ {
+ "name": "@rrweb/packer",
+ "path": "../packages/packer"
+ },
{
"name": "web-extension (package)",
"path": "../packages/web-extension"
},
{ "name": "rrvideo (package)", "path": "../packages/rrvideo" },
- { "name": "@rrweb/types", "path": "../packages/types" }
+ {
+ "name": "@rrweb/rrweb-plugin-console-record",
+ "path": "../packages/plugins/rrweb-plugin-console-record"
+ },
+ {
+ "name": "@rrweb/rrweb-plugin-console-replay",
+ "path": "../packages/plugins/rrweb-plugin-console-replay"
+ },
+ {
+ "name": "@rrweb/rrweb-plugin-sequential-id-record",
+ "path": "../packages/plugins/rrweb-plugin-sequential-id-record"
+ },
+ {
+ "name": "@rrweb/rrweb-plugin-sequential-id-replay",
+ "path": "../packages/plugins/rrweb-plugin-sequential-id-replay"
+ },
+ {
+ "name": "@rrweb/rrweb-plugin-canvas-webrtc-record",
+ "path": "../packages/plugins/rrweb-plugin-canvas-webrtc-record"
+ },
+ {
+ "name": "@rrweb/rrweb-plugin-canvas-webrtc-replay",
+ "path": "../packages/plugins/rrweb-plugin-canvas-webrtc-replay"
+ }
],
"settings": {
+ "vitest.workspaceConfig": "../vitest.workspace.ts",
+ "vitest.commandLine": "yarn vitest",
"jest.disabledWorkspaceFolders": [
" rrweb monorepo",
+ "rrweb (package)",
"rrweb-player (package)",
- "@rrweb/types"
+ "rrweb-snapshot (package)",
+ "rrdom (package)",
+ "rrdom-nodejs (package)",
+ "@rrweb/all",
+ "@rrweb/record",
+ "@rrweb/replay",
+ "@rrweb/types",
+ "@rrweb/packer",
+ "@rrweb/rrweb-plugin-console-record",
+ "@rrweb/rrweb-plugin-console-replay",
+ "@rrweb/rrweb-plugin-sequential-id",
+ "@rrweb/rrweb-plugin-canvas-webrtc-record",
+ "@rrweb/rrweb-plugin-canvas-webrtc-replay"
]
}
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e6c87ea457..4883735bfe 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,8 +27,10 @@ clear and has sufficient instructions to be able to reproduce the issue.
## Run locally
- Install dependencies: `yarn`
-- Run recorder on a website: `yarn repl`
-- Run a cobrowsing/mirroring session locally: `yarn live-stream`
+- Build all packages: (in `/`) `yarn build:all` or `yarn dev`
+- Run recorder on a website: (in `/packages/rrweb`) `yarn repl`
+- Run a cobrowsing/mirroring session locally: (in `/packages/rrweb`) `yarn live-stream`
+- Build individual packages: `yarn build` or `yarn dev`
- Test: `yarn test` or `yarn test:watch`
- Lint: `yarn lint`
- Rewrite files with prettier: `yarn format` or `yarn format:head`
diff --git a/README.md b/README.md
index dc2ffef021..17e6b5591a 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,8 @@
[![Join the chat at slack](https://img.shields.io/badge/slack-@rrweb-teal.svg?logo=slack)](https://join.slack.com/t/rrweb/shared_invite/zt-siwoc6hx-uWay3s2wyG8t5GpZVb8rWg)
[![Twitter Follow](https://img.shields.io/badge/twitter-@rrweb__io-teal.svg?logo=twitter)](https://twitter.com/rrweb_io)
-![total gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js?compression=gzip&label=total%20gzip%20size)
-![recorder gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/rrweb@latest/dist/record/rrweb-record.min.js?compression=gzip&label=recorder%20gzip%20size)
+![total gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.cjs?compression=gzip&label=total%20gzip%20size)
+![recorder gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/rrweb@latest/dist/record/rrweb-record.min.cjs?compression=gzip&label=recorder%20gzip%20size)
[![](https://data.jsdelivr.com/v1/package/npm/rrweb/badge)](https://www.jsdelivr.com/package/npm/rrweb)
[中文文档](./README.zh_CN.md)
@@ -39,9 +39,7 @@ rrweb is mainly composed of 3 parts:
## Roadmap
-- rrdom: an ad-hoc DOM for rrweb session data [#419](https://github.com/rrweb-io/rrweb/issues/419)
- storage engine: do deduplication on a large number of rrweb sessions
-- more end-to-end tests
- compact mutation data in common patterns
- provide plugins via the new plugin API, including:
- XHR plugin
@@ -166,7 +164,7 @@ In addition to adding integration tests and unit tests, rrweb also provides a RE
-
-
+
+
diff --git a/README.zh_CN.md b/README.zh_CN.md
index 8e06fa8838..6e6c408aa7 100644
--- a/README.zh_CN.md
+++ b/README.zh_CN.md
@@ -10,8 +10,8 @@
**[rrweb 纪录片(中文)](https://www.bilibili.com/video/BV1wL4y1B7wN?share_source=copy_web)**
[![Join the chat at slack](https://img.shields.io/badge/slack-@rrweb-teal.svg?logo=slack)](https://join.slack.com/t/rrweb/shared_invite/zt-siwoc6hx-uWay3s2wyG8t5GpZVb8rWg)
-![total gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js?compression=gzip&label=total%20gzip%20size)
-![recorder gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/rrweb@latest/dist/record/rrweb-record.min.js?compression=gzip&label=recorder%20gzip%20size)
+![total gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.cjs?compression=gzip&label=total%20gzip%20size)
+![recorder gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/rrweb@latest/dist/record/rrweb-record.min.cjs?compression=gzip&label=recorder%20gzip%20size)
[![](https://data.jsdelivr.com/v1/package/npm/rrweb/badge)](https://www.jsdelivr.com/package/npm/rrweb)
> 我已开通 Github Sponsor, 您可以通过赞助的形式帮助 rrweb 的开发。
@@ -34,9 +34,7 @@ rrweb 主要由 3 部分组成:
## Roadmap
-- rrdom: rrweb 数据专用的 DOM 实现 [#419](https://github.com/rrweb-io/rrweb/issues/419)
- storage engine: 对大规模 rrweb 数据进行去重
-- 更多的 E2E 测试
- 在常见场景下对 mutation 数据进行压缩
- 基于新的插件 API 提供更多插件,包括:
- XHR 插件
@@ -66,6 +64,58 @@ rrweb 主要由 3 部分组成:
[使用 REPL 工具](./guide.zh_CN.md#REPL-工具)
+## Sponsors
+
+[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
+
+### Gold Sponsors 🥇
+
+
+
+### Silver Sponsors 🥈
+
+
+
+### Bronze Sponsors 🥉
+
+
+
+### Backers
+
+
+
## Core Team Members
@@ -78,16 +128,18 @@ rrweb 主要由 3 部分组成:
alt=""
/>
Yuyz0112
+
-
+
- Mark-Fenng
+ Yun Feng
+
@@ -98,6 +150,7 @@ rrweb 主要由 3 部分组成:
alt=""
/>
eoghanmurray
+
@@ -108,12 +161,13 @@ rrweb 主要由 3 部分组成:
alt=""
/>
Juice10
+ open for rrweb consulting
-## Who's using rrweb
+## Who's using rrweb?
@@ -133,15 +187,15 @@ rrweb 主要由 3 部分组成:
-
-
+
+
-
-
+
+
@@ -166,10 +220,20 @@ rrweb 主要由 3 部分组成:
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/recipes/canvas.md b/docs/recipes/canvas.md
index b8833b506e..d6676211af 100644
--- a/docs/recipes/canvas.md
+++ b/docs/recipes/canvas.md
@@ -40,5 +40,5 @@ replayer.play();
**Enable replaying Canvas will remove the sandbox, which may cause a potential security issue.**
-Alternatively you can stream canvas elements via webrtc with the canvas-webrtc plugin.
-For more information see [canvas-webrtc documentation](../../packages/rrweb/src/plugins/canvas-webrtc/Readme.md)
+Alternatively you can stream canvas elements via webrtc with the [rrweb-plugin-canvas-webrtc-record](../../packages/plugins/rrweb-plugin-canvas-webrtc-record/) & [rrweb-plugin-canvas-webrtc-replay](../../packages/plugins/rrweb-plugin-canvas-webrtc-replay) plugins.
+For more information see [canvas-webrtc documentation](../../packages/plugins/rrweb-plugin-canvas-webrtc-record/Readme.md)
diff --git a/docs/recipes/canvas.zh_CN.md b/docs/recipes/canvas.zh_CN.md
index 1f522c9bcf..ae8902a44b 100644
--- a/docs/recipes/canvas.zh_CN.md
+++ b/docs/recipes/canvas.zh_CN.md
@@ -40,5 +40,5 @@ replayer.play();
**回放 Canvas 将会关闭沙盒策略,导致一定风险**。
-另外,您可以使用 canvas-webrtc 插件通过 WEBRTC 流式传输 Canvas 元素。
-有关更多信息,请参考[canvas-webrtc 文档](../../packages/rrweb/src/plugins/canvas-webrtc/Readme.md)
+另外,您可以使用 [rrweb-plugin-canvas-webrtc-record](../../packages/plugins/rrweb-plugin-canvas-webrtc-record/) 和 [rrweb-plugin-canvas-webrtc-replay](../../packages/plugins/rrweb-plugin-canvas-webrtc-replay) 插件通过 WebRTC 流式传输 Canvas 元素。
+有关更多信息,请参考 [canvas-webrtc 文档](../../packages/plugins/rrweb-plugin-canvas-webrtc-record/Readme.md)。
diff --git a/docs/recipes/console.md b/docs/recipes/console.md
index ffeb55f18d..eb7b9a15e1 100644
--- a/docs/recipes/console.md
+++ b/docs/recipes/console.md
@@ -8,6 +8,9 @@ This feature aims to provide developers with more information about the bug scen
You can enable the logger using default option like this:
```js
+import rrweb from 'rrweb';
+import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record';
+
rrweb.record({
emit: function emit(event) {
// you should use console.log in this way to avoid errors.
@@ -17,7 +20,7 @@ rrweb.record({
defaultLog(event);
},
// to use default record option
- plugins: [rrweb.getRecordConsolePlugin()],
+ plugins: [getRecordConsolePlugin()],
});
```
@@ -27,6 +30,9 @@ You should call console.log.\_\_rrweb_original\_\_() instead.
You can also customize the behavior of logger like this:
```js
+import rrweb from 'rrweb';
+import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record';
+
rrweb.record({
emit: function emit(event) {
// you should use console.log in this way to avoid errors.
@@ -37,7 +43,7 @@ rrweb.record({
},
// customized options
plugins: [
- rrweb.getRecordConsolePlugin({
+ getRecordConsolePlugin({
level: ['info', 'log', 'warn', 'error'],
lengthThreshold: 10000,
stringifyOptions: {
@@ -64,9 +70,12 @@ All options are described below:
If recorded events include data of console log type, we will automatically play them.
```js
+import rrweb from 'rrweb';
+import { getReplayConsolePlugin } from '@rrweb/rrweb-plugin-console-replay';
+
const replayer = new rrweb.Replayer(events, {
plugins: [
- rrweb.getReplayConsolePlugin({
+ getReplayConsolePlugin({
level: ['info', 'log', 'warn', 'error'],
}),
],
diff --git a/docs/recipes/console.zh_CN.md b/docs/recipes/console.zh_CN.md
index 3171548427..42d0c08c92 100644
--- a/docs/recipes/console.zh_CN.md
+++ b/docs/recipes/console.zh_CN.md
@@ -7,7 +7,10 @@
可以通过如下代码使用默认的配置选项
```js
-rrweb.record({
+import rrweb from 'rrweb';
+import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record';
+
+rweb.record({
emit: function emit(event) {
// 如果要使用console来输出信息,请使用如下的写法
const defaultLog = console.log['__rrweb_original__']
@@ -16,7 +19,7 @@ rrweb.record({
defaultLog(event);
},
// 使用默认的配置选项
- plugins: [rrweb.getRecordConsolePlugin()],
+ plugins: [getRecordConsolePlugin()],
});
```
@@ -26,6 +29,9 @@ rrweb.record({
你也可以定制录制 console 的选项
```js
+import rrweb from 'rrweb';
+import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record';
+
rrweb.record({
emit: function emit(event) {
// 如果要使用console来输出信息,请使用如下的写法
@@ -36,7 +42,7 @@ rrweb.record({
},
// 定制的选项
plugins: [
- rrweb.getRecordConsolePlugin({
+ getRecordConsolePlugin({
level: ['info', 'log', 'warn', 'error'],
lengthThreshold: 10000,
stringifyOptions: {
@@ -63,9 +69,12 @@ rrweb.record({
如果 replayer 传入的 events 中包含了 console 类型的数据,我们将自动播放这些数据。
```js
+import rrweb from 'rrweb';
+import { getReplayConsolePlugin } from '@rrweb/rrweb-plugin-console-replay';
+
const replayer = new rrweb.Replayer(events, {
plugins: [
- rrweb.getReplayConsolePlugin({
+ getReplayConsolePlugin({
level: ['info', 'log', 'warn', 'error'],
}),
],
diff --git a/docs/recipes/customize-replayer.md b/docs/recipes/customize-replayer.md
index d30462326f..218ced2432 100644
--- a/docs/recipes/customize-replayer.md
+++ b/docs/recipes/customize-replayer.md
@@ -1,11 +1,11 @@
# Customize the Replayer
-When rrweb's Replayer and the rrweb-player UI do not fit your need, you can customize your replayer UI.
+When rrweb's Replayer and the [rrweb-player](../../packages/rrweb-player/) UI do not fit your need, you can customize your replayer UI.
There are several ways to do this:
-1. Use rrweb-player, and customize its CSS.
-2. Use rrweb-player, and set `showController: false` to hide the controller UI. With this config, you can implement your controller UI.
+1. Use [rrweb-player](../../packages/rrweb-player/), and customize its CSS.
+2. Use [rrweb-player](../../packages/rrweb-player/), and set `showController: false` to hide the controller UI. With this config, you can implement your controller UI.
3. Use the `insertStyleRules` options to inject some CSS into the replay iframe.
4. Develop a new replayer UI with rrweb's Replayer.
@@ -14,6 +14,8 @@ There are several ways to do this:
When using rrweb-player, you can hide its controller UI:
```js
+import rrwebPlayer from 'rrweb-player';
+
new rrwebPlayer({
target: document.body,
props: {
diff --git a/docs/recipes/customize-replayer.zh_CN.md b/docs/recipes/customize-replayer.zh_CN.md
index 58f6553fc0..70a297303f 100644
--- a/docs/recipes/customize-replayer.zh_CN.md
+++ b/docs/recipes/customize-replayer.zh_CN.md
@@ -1,11 +1,11 @@
# 自定义回放 UI
-当 rrweb Replayer 和 rrweb-player 的 UI 不能满足需求时,可以通过自定义回放 UI 制作属于你自己的回放器。
+当 rrweb Replayer 和 [rrweb-player](../../packages/rrweb-player/) 的 UI 不能满足需求时,可以通过自定义回放 UI 制作属于你自己的回放器。
你可以通过以下几种方式从不同角度自定义回放 UI:
-1. 使用 rrweb-player 时,通过覆盖 CSS 样式表定制 UI。
-2. 使用 rrweb-player 时,通过 `showController: false` 隐藏控制器 UI,重新实现控制器 UI。
+1. 使用 [rrweb-player](../../packages/rrweb-player/) 时,通过覆盖 CSS 样式表定制 UI。
+2. 使用 [rrweb-player](../../packages/rrweb-player/) 时,通过 `showController: false` 隐藏控制器 UI,重新实现控制器 UI。
3. 通过 `insertStyleRules` 在回放页面(iframe)内定制 CSS 样式。
4. 基于 rrweb Replayer 开发自己的回放器 UI。
@@ -14,6 +14,8 @@
使用 rrweb-player 时,可以隐藏其控制器 UI:
```js
+import rrwebPlayer from 'rrweb-player';
+
new rrwebPlayer({
target: document.body,
props: {
diff --git a/docs/recipes/optimize-storage.md b/docs/recipes/optimize-storage.md
index cd5152593a..a50118bf89 100644
--- a/docs/recipes/optimize-storage.md
+++ b/docs/recipes/optimize-storage.md
@@ -69,20 +69,24 @@ rrweb.record({
### Use packFn to compress every event
-rrweb provides an fflate-based simple compress function rrweb.pack.
+rrweb provides an fflate-based simple compress function in [@rrweb/packer](../../packages/packer/).
You can use it by passing it as the `packFn` in the recording.
```js
+import { pack } from '@rrweb/packer';
+
rrweb.record({
emit(event) {},
packFn: rrweb.pack,
});
```
-And you need to pass rrweb.unpack as the `unpackFn` in replaying.
+And you need to pass packer.unpack as the `unpackFn` in replaying.
```js
+import { unpack } from '@rrweb/packer';
+
const replayer = new rrweb.Replayer(events, {
unpackFn: rrweb.unpack,
});
diff --git a/docs/recipes/optimize-storage.zh_CN.md b/docs/recipes/optimize-storage.zh_CN.md
index c180d57ad5..92bbdbdbc1 100644
--- a/docs/recipes/optimize-storage.zh_CN.md
+++ b/docs/recipes/optimize-storage.zh_CN.md
@@ -69,18 +69,22 @@ rrweb.record({
### 基于 packFn 的单数据压缩
-rrweb 内包含了基于 fflate 的简单压缩 rrweb.pack,在录制时可以作为 `packFn` 传入。
+rrweb 提供了一个基于 fflate 的简单压缩函数,在 [@rrweb/packer](../../packages/packer/) 中可以作为 `packFn` 传入使用。
```js
+import { pack } from '@rrweb/packer';
+
rrweb.record({
emit(event) {},
packFn: rrweb.pack,
});
```
-回放时通用需要传入 rrweb.unpack 作为 `unpackFn` 传入。
+回放时通用需要传入 packer.unpack 作为 `unpackFn` 传入。
```js
+import { unpack } from '@rrweb/packer';
+
const replayer = new rrweb.Replayer(events, {
unpackFn: rrweb.unpack,
});
diff --git a/docs/recipes/plugin.md b/docs/recipes/plugin.md
index e4c6bfcae7..d322bc1eaf 100644
--- a/docs/recipes/plugin.md
+++ b/docs/recipes/plugin.md
@@ -4,7 +4,12 @@ The plugin API is designed to extend the function of rrweb without bump the size
# Available plugins
-- [console](./console.md)
+- [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record): A plugin for recording console logs.
+- [@rrweb/rrweb-plugin-console-replay](packages/plugins/rrweb-plugin-console-replay): A plugin for replaying console logs.
+- [@rrweb/rrweb-plugin-sequential-id-record](packages/plugins/rrweb-plugin-sequential-id-record): A plugin for recording sequential IDs.
+- [@rrweb/rrweb-plugin-sequential-id-replay](packages/plugins/rrweb-plugin-sequential-id-replay): A plugin for replaying sequential IDs.
+- [@rrweb/rrweb-plugin-canvas-webrtc-record](packages/plugins/rrweb-plugin-canvas-webrtc-record): A plugin for stream `` via WebRTC.
+- [@rrweb/rrweb-plugin-canvas-webrtc-replay](packages/plugins/rrweb-plugin-canvas-webrtc-replay): A plugin for playing streamed `` via WebRTC.
## Interface
diff --git a/docs/recipes/plugin.zh_CN.md b/docs/recipes/plugin.zh_CN.md
index 530f48bc92..806e4e228c 100644
--- a/docs/recipes/plugin.zh_CN.md
+++ b/docs/recipes/plugin.zh_CN.md
@@ -4,7 +4,12 @@
# 可用插件
-- [console](./console.zh_CN.md)
+- [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record):一个用于记录控制台日志的插件。
+- [@rrweb/rrweb-plugin-console-replay](packages/plugins/rrweb-plugin-console-replay):一个用于回放控制台日志的插件。
+- [@rrweb/rrweb-plugin-sequential-id-record](packages/plugins/rrweb-plugin-sequential-id-record):一个用于记录顺序 ID 的插件。
+- [@rrweb/rrweb-plugin-sequential-id-replay](packages/plugins/rrweb-plugin-sequential-id-replay):一个用于回放顺序 ID 的插件。
+- [@rrweb/rrweb-plugin-canvas-webrtc-record](packages/plugins/rrweb-plugin-canvas-webrtc-record):一个用于通过 WebRTC 流式传输 `` 的插件。
+- [@rrweb/rrweb-plugin-canvas-webrtc-replay](packages/plugins/rrweb-plugin-canvas-webrtc-replay):一个用于通过 WebRTC 播放流式 `` 的插件。
## 接口
diff --git a/docs/sandbox.md b/docs/sandbox.md
index 4e7f396d44..8c01fe1317 100644
--- a/docs/sandbox.md
+++ b/docs/sandbox.md
@@ -20,7 +20,7 @@ When you click the a element link, the default event is to jump to the URL corre
Usually we will capture all an elements click events through the event handler proxy and disable the default event via `event.preventDefault()`. But when we put the replay page in the sandbox, all the event handlers will not be executed, and we will not be able to implement the event delegation.
-When replaying interactive events, note that replaying the JS `click` event is not nessecary because click events do not have any impact when JS is disabled. However, in order to optimize the replay effect, we can add special animation effects to visualize elements being clicked with the mouse, to clearly show the viewer that a click has occurred.
+When replaying interactive events, note that replaying the JS `click` event is not necessary because click events do not have any impact when JS is disabled. However, in order to optimize the replay effect, we can add special animation effects to visualize elements being clicked with the mouse, to clearly show the viewer that a click has occurred.
## iframe style settings
diff --git a/guide.md b/guide.md
index 5693b26d29..bfdb2d14fd 100644
--- a/guide.md
+++ b/guide.md
@@ -13,52 +13,48 @@ You are recommended to install rrweb via jsdelivr's CDN service:
```html
-
+
```
Also, you can link to a specific version number that you can update manually:
```html
-
+
```
#### Only include the recorder code
rrweb's code includes both the record and the replay parts. Most of the time you only need to include the record part into your targeted web Apps.
-This also can be done by using the CDN service:
+This also can be done by using the `@rrweb/record` package and the CDN service:
```html
-
+
```
-#### Other bundles
-
-Besides the `record/rrweb-record.min.js` entry, rrweb also provides other bundles for different usage.
-
-```shell
-# Include record, replay, compression, and decompression.
-rrweb-all.js
-rrweb-all.min.js
-# Include record and replay.
-rrweb.js
-rrweb.min.js
-# Include the styles for replay.
-rrweb.min.css
-# Record
-record/rrweb-record.js
-record/rrweb-record.min.js
-# Data compression.
-record/rrweb-record-pack.js
-record/rrweb-record-pack.min.js
-# Replay
-replay/rrweb-replay.js
-replay/rrweb-replay.min.js
-# Data decompression.
-replay/rrweb-replay-unpack.js
-replay/rrweb-replay-unpack.min.js
-```
+#### Other packages
+
+Besides the `rrweb` and `@rrweb/record` packages, rrweb also provides other packages for different usage.
+
+- [rrweb](packages/rrweb): The core package of rrweb, including record and replay functions.
+- [rrweb-player](packages/rrweb-player): A GUI for rrweb, providing a timeline and buttons for things like pause, fast-forward, and speedup.
+- [rrweb-snapshot](packages/rrweb-snapshot): Handles snapshot and rebuilding features, converting the DOM and its state into a serializable data structure.
+- [rrdom](packages/rrdom): A virtual dom package rrweb.
+- [rrdom-nodejs](packages/rrdom-nodejs): The Node.js version of rrdom for server-side DOM operations.
+- [@rrweb/all](packages/all): A package that includes `rrweb` and `@rrweb/packer` for easy install.
+- [@rrweb/record](packages/record): A package for recording rrweb sessions.
+- [@rrweb/replay](packages/replay): A package for replaying rrweb sessions.
+- [@rrweb/packer](packages/packer): A package for packing and unpacking rrweb data.
+- [@rrweb/types](packages/types): Contains types shared across rrweb packages.
+- [web-extension](packages/web-extension): A web extension for rrweb.
+- [rrvideo](packages/rrvideo): A package for handling video operations in rrweb.
+- [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record): A plugin for recording console logs.
+- [@rrweb/rrweb-plugin-console-replay](packages/plugins/rrweb-plugin-console-replay): A plugin for replaying console logs.
+- [@rrweb/rrweb-plugin-sequential-id-record](packages/plugins/rrweb-plugin-sequential-id-record): A plugin for recording sequential IDs.
+- [@rrweb/rrweb-plugin-sequential-id-replay](packages/plugins/rrweb-plugin-sequential-id-replay): A plugin for replaying sequential IDs.
+- [@rrweb/rrweb-plugin-canvas-webrtc-record](packages/plugins/rrweb-plugin-canvas-webrtc-record): A plugin for stream `` via WebRTC.
+- [@rrweb/rrweb-plugin-canvas-webrtc-replay](packages/plugins/rrweb-plugin-canvas-webrtc-replay): A plugin for playing streamed `` via WebRTC.
### NPM
@@ -76,7 +72,6 @@ rrweb does **not** support IE11 and below because it uses the `MutationObserver`
### Record
-**If you only included the record code with `
+
```
也可以在 URL 中指定具体的版本号,例如:
```html
-
+
```
#### 仅引入录制部分
-rrweb 代码分为录制和回放两部分,大多数时候用户在被录制的应用中只需要引入录制部分代码,同样可以通过 CDN 安装:
+rrweb 代码分为录制和回放两部分,大多数时候用户在被录制的应用中只需要引入录制部分代码。同样可以通过使用 @rrweb/record 包和 CDN 服务来实现:
```html
-
+
```
-#### 其它按需引入方式
-
-除了仅包含录制代码的 `record/rrweb-record-min.js` 之外,rrweb 还提供了其它多种可选的打包文件。所有包含 `.min` 的文件为同名文件的压缩版。
-
-```shell
-# 包含录制、回放、压缩数据、解压缩数据
-rrweb-all.js
-rrweb-all.min.js
-# 包含录制、回放
-rrweb.js
-rrweb.min.js
-# 回放所需的样式文件
-rrweb.min.css
-# 录制
-record/rrweb-record.js
-record/rrweb-record.min.js
-# 压缩数据
-record/rrweb-record-pack.js
-record/rrweb-record-pack.min.js
-# 回放
-replay/rrweb-replay.js
-replay/rrweb-replay.min.js
-# 解压缩数据
-replay/rrweb-replay-unpack.js
-replay/rrweb-replay-unpack.min.js
-```
+#### 其他包
+
+除了 `rrweb` 和 `@rrweb/record` 包之外,rrweb 还提供了其他不同用途的包。
+
+- [rrweb](packages/rrweb):rrweb 的核心包,包括录制和回放功能。
+- [rrweb-player](packages/rrweb-player):rrweb 的图形用户界面,提供时间线和暂停、快进、加速等按钮。
+- [rrweb-snapshot](packages/rrweb-snapshot):处理快照和重建功能,将 DOM 及其状态转换为可序列化的数据结构。
+- [rrdom](packages/rrdom):rrweb 的虚拟 dom 包。
+- [rrdom-nodejs](packages/rrdom-nodejs):用于服务器端 DOM 操作的 rrdom 的 Node.js 版本。
+- [@rrweb/all](packages/all):一个包含 `rrweb` 和 `@rrweb/packer`,便于安装的包。
+- [@rrweb/record](packages/record):一个用于录制 rrweb 会话的包。
+- [@rrweb/replay](packages/replay):一个用于回放 rrweb 会话的包。
+- [@rrweb/packer](packages/packer):一个用于打包和解包 rrweb 数据的包。
+- [@rrweb/types](packages/types):包含 rrweb 中使用的类型定义。
+- [web-extension](packages/web-extension):rrweb 的网页扩展。
+- [rrvideo](packages/rrvideo):一个用于处理 rrweb 中视频操作的包。
+- [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record):一个用于记录控制台日志的插件。
+- [@rrweb/rrweb-plugin-console-replay](packages/plugins/rrweb-plugin-console-replay):一个用于回放控制台日志的插件。
+- [@rrweb/rrweb-plugin-sequential-id-record](packages/plugins/rrweb-plugin-sequential-id-record):一个用于记录顺序 ID 的插件。
+- [@rrweb/rrweb-plugin-sequential-id-replay](packages/plugins/rrweb-plugin-sequential-id-replay):一个用于回放顺序 ID 的插件。
+- [@rrweb/rrweb-plugin-canvas-webrtc-record](packages/plugins/rrweb-plugin-canvas-webrtc-record):一个用于通过 WebRTC 流式传输 `` 的插件。
+- [@rrweb/rrweb-plugin-canvas-webrtc-replay](packages/plugins/rrweb-plugin-canvas-webrtc-replay):一个用于通过 WebRTC 播放流式 `` 的插件。
### 通过 npm 引入
@@ -252,7 +248,7 @@ window.onerror = function () {
```html
```
diff --git a/package.json b/package.json
index 4534a0204a..7cd9d3ec47 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,8 @@
"private": true,
"homepage": "https://github.com/amplitude/rrweb#readme",
"workspaces": [
- "packages/*"
+ "packages/*",
+ "packages/plugins/*"
],
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
@@ -25,6 +26,7 @@
"@typescript-eslint/parser": "^5.62.0",
"browserslist": "^4.22.1",
"concurrently": "^7.1.0",
+ "esbuild-plugin-umd-wrapper": "^2.0.0",
"eslint": "^8.53.0",
"eslint-plugin-compat": "^4.2.0",
"eslint-plugin-jest": "^27.6.0",
@@ -37,9 +39,10 @@
},
"scripts": {
"build:all": "NODE_OPTIONS='--max-old-space-size=4096' yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references' 'yarn turbo run prepublish'",
- "test": "yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references --check' 'yarn turbo run test --concurrency=1'",
+ "test": "yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references --check' 'yarn turbo run test --concurrency=1 --continue'",
"test:watch": "yarn turbo run test:watch",
"test:update": "yarn turbo run test:update",
+ "check-types": "yarn turbo run check-types --continue",
"format": "yarn prettier --write '**/*.{ts,md}'",
"format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write",
"dev": "yarn turbo run dev",
@@ -50,7 +53,7 @@
"release": "yarn build:all && changeset publish"
},
"resolutions": {
- "**/jsdom/cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
+ "**/cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
"**/@types/dom-webcodecs": "0.1.5"
},
"browserslist": [
diff --git a/packages/all/.gitignore b/packages/all/.gitignore
new file mode 100644
index 0000000000..d29f9959b9
--- /dev/null
+++ b/packages/all/.gitignore
@@ -0,0 +1,4 @@
+.turbo
+dist
+node_modules
+yarn-error.log
\ No newline at end of file
diff --git a/packages/all/README.md b/packages/all/README.md
new file mode 100644
index 0000000000..d71a565313
--- /dev/null
+++ b/packages/all/README.md
@@ -0,0 +1,200 @@
+# @rrweb/all
+
+Convenience package that includes a bundle of rrweb packages.
+
+Includes the following packages:
+
+- [rrweb](../rrweb)
+- [@rrweb/record](../record)
+- [@rrweb/replay](../replay)
+- [@rrweb/packer](../packer)
+
+## Installation
+
+```bash
+npm install @rrweb/all
+```
+
+## Usage
+
+```js
+import { record, replay, pack, unpack } from '@rrweb/all';
+
+// use record, replay, pack, unpack as you would with the individual packages.
+```
+
+See the [guide](../../guide.md) for more info on rrweb.
+
+## Sponsors
+
+[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
+
+### Gold Sponsors 🥇
+
+
+
+### Silver Sponsors 🥈
+
+
+
+### Bronze Sponsors 🥉
+
+
+
+### Backers
+
+
+
+## Core Team Members
+
+
+
+## Who's using rrweb?
+
+
diff --git a/packages/all/package.json b/packages/all/package.json
new file mode 100644
index 0000000000..4dff234b20
--- /dev/null
+++ b/packages/all/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "@rrweb/all",
+ "version": "2.0.0-alpha.14",
+ "publishConfig": {
+ "access": "public"
+ },
+ "keywords": [
+ "rrweb",
+ "@rrweb/all"
+ ],
+ "scripts": {
+ "dev": "vite build --watch",
+ "build": "tsc -noEmit && vite build",
+ "test": "vitest run",
+ "test:watch": "vitest watch",
+ "check-types": "tsc -noEmit",
+ "prepublish": "npm run build",
+ "lint": "yarn eslint src/**/*.ts"
+ },
+ "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/all#readme",
+ "bugs": {
+ "url": "https://github.com/rrweb-io/rrweb/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rrweb-io/rrweb.git"
+ },
+ "license": "MIT",
+ "type": "module",
+ "main": "./dist/all.cjs",
+ "module": "./dist/all.js",
+ "unpkg": "./dist/all.umd.cjs",
+ "typings": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/all.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/all.cjs"
+ }
+ }
+ },
+ "files": [
+ "build",
+ "dist",
+ "package.json"
+ ],
+ "devDependencies": {
+ "puppeteer": "^20.9.0",
+ "vite": "^5.2.8",
+ "vite-plugin-dts": "^3.8.1",
+ "vitest": "^1.4.0",
+ "typescript": "^4.7.3"
+ },
+ "dependencies": {
+ "@rrweb/types": "^2.0.0-alpha.14",
+ "@rrweb/packer": "^2.0.0-alpha.14",
+ "rrweb": "^2.0.0-alpha.14"
+ },
+ "browserslist": [
+ "supports es6-class"
+ ]
+}
diff --git a/packages/all/src/index.ts b/packages/all/src/index.ts
new file mode 100644
index 0000000000..aa01eeaba9
--- /dev/null
+++ b/packages/all/src/index.ts
@@ -0,0 +1,4 @@
+export * from 'rrweb';
+export * from '@rrweb/packer';
+// export * from '@rrweb/rrweb-plugin-console-record';
+// export * from '@rrweb/rrweb-plugin-console-replay';
diff --git a/packages/all/test/__snapshots__/cross-origin-iframe-packer.test.ts.snap b/packages/all/test/__snapshots__/cross-origin-iframe-packer.test.ts.snap
new file mode 100644
index 0000000000..b550cf9bde
--- /dev/null
+++ b/packages/all/test/__snapshots__/cross-origin-iframe-packer.test.ts.snap
@@ -0,0 +1,170 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`cross origin iframes & packer > blank.html > should support packFn option in record() > 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ },
+ \\"v\\": \\"v1\\"
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {
+ \\"type\\": \\"text/javascript\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 6
+ }
+ ],
+ \\"id\\": 5
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"iframe\\",
+ \\"attributes\\": {
+ \\"rr_src\\": \\"http://localhost:3030/html/blank.html\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
+ \\"id\\": 10
+ }
+ ],
+ \\"id\\": 7
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ },
+ \\"v\\": \\"v1\\"
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"adds\\": [
+ {
+ \\"parentId\\": 9,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {
+ \\"type\\": \\"text/javascript\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"rootId\\": 11,
+ \\"id\\": 15
+ }
+ ],
+ \\"rootId\\": 11,
+ \\"id\\": 14
+ }
+ ],
+ \\"rootId\\": 11,
+ \\"id\\": 13
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n\\\\n\\",
+ \\"rootId\\": 11,
+ \\"id\\": 17
+ }
+ ],
+ \\"rootId\\": 11,
+ \\"id\\": 16
+ }
+ ],
+ \\"rootId\\": 11,
+ \\"id\\": 12
+ }
+ ],
+ \\"compatMode\\": \\"BackCompat\\",
+ \\"id\\": 11
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"isAttachIframe\\": true
+ },
+ \\"v\\": \\"v1\\"
+ }
+]"
+`;
diff --git a/packages/all/test/cross-origin-iframe-packer.test.ts b/packages/all/test/cross-origin-iframe-packer.test.ts
new file mode 100644
index 0000000000..9ae56b31bb
--- /dev/null
+++ b/packages/all/test/cross-origin-iframe-packer.test.ts
@@ -0,0 +1,156 @@
+import {
+ describe,
+ it,
+ vi,
+ beforeAll,
+ beforeEach,
+ afterEach,
+ afterAll,
+} from 'vitest';
+import type {
+ eventWithTime,
+ listenerHandler,
+ mutationData,
+} from '@rrweb/types';
+import { unpack } from '@rrweb/packer';
+import * as fs from 'fs';
+import * as path from 'path';
+import type * as puppeteer from 'puppeteer';
+import type { recordOptions } from 'rrweb';
+import type {} from '@rrweb/types';
+import { EventType } from '@rrweb/types';
+import {
+ assertSnapshot,
+ getServerURL,
+ launchPuppeteer,
+ startServer,
+ waitForRAF,
+} from './utils';
+import type * as http from 'http';
+
+interface ISuite {
+ code: string;
+ browser: puppeteer.Browser;
+ page: puppeteer.Page;
+ events: eventWithTime[];
+ server: http.Server;
+ serverURL: string;
+}
+
+interface IWindow extends Window {
+ rrweb: {
+ record: (
+ options: recordOptions,
+ ) => listenerHandler | undefined;
+ addCustomEvent(tag: string, payload: T): void;
+ pack: (e: eventWithTime) => string;
+ };
+ emit: (e: eventWithTime) => undefined;
+ snapshots: eventWithTime[];
+}
+type ExtraOptions = {
+ usePackFn?: boolean;
+};
+
+async function injectRecordScript(
+ frame: puppeteer.Frame,
+ options?: ExtraOptions,
+) {
+ await frame.addScriptTag({
+ path: path.resolve(__dirname, '../dist/all.umd.cjs'),
+ });
+ options = options || {};
+ await frame.evaluate((options) => {
+ (window as unknown as IWindow).snapshots = [];
+ const { record, pack } = (window as unknown as IWindow).rrweb;
+ const config: recordOptions = {
+ recordCrossOriginIframes: true,
+ recordCanvas: true,
+ emit(event) {
+ (window as unknown as IWindow).snapshots.push(event);
+ (window as unknown as IWindow).emit(event);
+ },
+ };
+ if (options.usePackFn) {
+ config.packFn = pack;
+ }
+ record(config);
+ }, options);
+
+ for (const child of frame.childFrames()) {
+ await injectRecordScript(child, options);
+ }
+}
+
+const setup = function (
+ this: ISuite,
+ content: string,
+ options?: ExtraOptions,
+): ISuite {
+ const ctx = {} as ISuite;
+
+ beforeAll(async () => {
+ ctx.browser = await launchPuppeteer();
+ ctx.server = await startServer();
+ ctx.serverURL = getServerURL(ctx.server);
+ });
+
+ beforeEach(async () => {
+ ctx.page = await ctx.browser.newPage();
+ await ctx.page.goto('about:blank');
+ await ctx.page.setContent(
+ content.replace(/\{SERVER_URL\}/g, ctx.serverURL),
+ );
+ ctx.events = [];
+ await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
+ if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) {
+ return;
+ }
+ ctx.events.push(e);
+ });
+
+ ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
+ await injectRecordScript(ctx.page.mainFrame(), options);
+ });
+
+ afterEach(async () => {
+ await ctx.page.close();
+ });
+
+ afterAll(async () => {
+ await ctx.browser.close();
+ ctx.server.close();
+ });
+
+ return ctx;
+};
+
+describe('cross origin iframes & packer', function (this: ISuite) {
+ vi.setConfig({ testTimeout: 100_000 });
+
+ describe('blank.html', function (this: ISuite) {
+ const content = `
+
+
+
+
+
+
+ `;
+ const ctx = setup.call(this, content, { usePackFn: true });
+
+ describe('should support packFn option in record()', () => {
+ it('', async () => {
+ const frame = ctx.page.mainFrame().childFrames()[0];
+ await waitForRAF(frame);
+ const packedSnapshots = (await ctx.page.evaluate(
+ 'window.snapshots',
+ )) as string[];
+ const unpackedSnapshots = packedSnapshots.map((packed) =>
+ unpack(packed),
+ ) as eventWithTime[];
+ assertSnapshot(unpackedSnapshots);
+ });
+ });
+ });
+});
diff --git a/packages/all/test/html/blank.html b/packages/all/test/html/blank.html
new file mode 100644
index 0000000000..80e6028fd4
--- /dev/null
+++ b/packages/all/test/html/blank.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/all/test/utils.ts b/packages/all/test/utils.ts
new file mode 100644
index 0000000000..5f8aaab932
--- /dev/null
+++ b/packages/all/test/utils.ts
@@ -0,0 +1,278 @@
+import { NodeType } from 'rrweb-snapshot';
+import { expect } from 'vitest';
+import {
+ EventType,
+ IncrementalSource,
+ eventWithTime,
+ Optional,
+ mouseInteractionData,
+ event,
+ pluginEvent,
+} from '@rrweb/types';
+import * as puppeteer from 'puppeteer';
+import * as path from 'path';
+import * as http from 'http';
+import * as url from 'url';
+import * as fs from 'fs';
+
+export async function launchPuppeteer(
+ options?: Parameters<(typeof puppeteer)['launch']>[0],
+) {
+ return await puppeteer.launch({
+ headless: process.env.PUPPETEER_HEADLESS ? 'new' : false,
+ defaultViewport: {
+ width: 1920,
+ height: 1080,
+ },
+ args: ['--no-sandbox'],
+ ...options,
+ });
+}
+
+interface IMimeType {
+ [key: string]: string;
+}
+export const startServer = (defaultPort = 3030) =>
+ new Promise((resolve) => {
+ const mimeType: IMimeType = {
+ '.html': 'text/html',
+ '.js': 'text/javascript',
+ '.css': 'text/css',
+ };
+ const s = http.createServer((req, res) => {
+ const parsedUrl = url.parse(req.url!);
+ const sanitizePath = path
+ .normalize(parsedUrl.pathname!)
+ .replace(/^(\.\.[\/\\])+/, '');
+
+ let pathname = path.join(__dirname, sanitizePath);
+ if (/^\/rrweb.*\.c?js.*/.test(sanitizePath)) {
+ pathname = path.join(__dirname, `../dist`, sanitizePath);
+ }
+
+ try {
+ const data = fs.readFileSync(pathname);
+ const ext = path.parse(pathname).ext;
+ res.setHeader('Content-type', mimeType[ext] || 'text/plain');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-type');
+ setTimeout(() => {
+ res.end(data);
+ // mock delay
+ }, 100);
+ } catch (error) {
+ res.end();
+ }
+ });
+ s.listen(defaultPort)
+ .on('listening', () => {
+ resolve(s);
+ })
+ .on('error', (e) => {
+ s.listen().on('listening', () => {
+ resolve(s);
+ });
+ });
+ });
+
+export function getServerURL(server: http.Server): string {
+ const address = server.address();
+ if (address && typeof address !== 'string') {
+ return `http://localhost:${address.port}`;
+ } else {
+ return `${address}`;
+ }
+}
+
+/**
+ * Puppeteer may cast random mouse move which make our tests flaky.
+ * So we only do snapshot test with filtered events.
+ * Also remove timestamp from event.
+ * @param snapshots incrementalSnapshotEvent[]
+ */
+function stringifySnapshots(snapshots: eventWithTime[]): string {
+ return JSON.stringify(
+ snapshots
+ .filter((s) => {
+ if (
+ s.type === EventType.IncrementalSnapshot &&
+ (s.data.source === IncrementalSource.MouseMove ||
+ s.data.source === IncrementalSource.ViewportResize)
+ ) {
+ return false;
+ }
+ return true;
+ })
+ .map((s) => {
+ if (s.type === EventType.Meta) {
+ s.data.href = 'about:blank';
+ }
+ // FIXME: travis coordinates seems different with my laptop
+ const coordinatesReg =
+ /(bottom|top|left|right|width|height): \d+(\.\d+)?px/g;
+ if (
+ s.type === EventType.IncrementalSnapshot &&
+ s.data.source === IncrementalSource.MouseInteraction
+ ) {
+ delete (s.data as Optional).x;
+ delete (s.data as Optional).y;
+ }
+ if (
+ s.type === EventType.IncrementalSnapshot &&
+ s.data.source === IncrementalSource.Mutation
+ ) {
+ s.data.attributes.forEach((a) => {
+ if ('style' in a.attributes && a.attributes.style) {
+ if (typeof a.attributes.style === 'object') {
+ for (const [k, v] of Object.entries(a.attributes.style)) {
+ if (Array.isArray(v)) {
+ if (coordinatesReg.test(k + ': ' + v[0])) {
+ // TODO: could round the number here instead depending on what's coming out of various test envs
+ a.attributes.style[k] = ['Npx', v[1]];
+ }
+ } else if (typeof v === 'string') {
+ if (coordinatesReg.test(k + ': ' + v)) {
+ a.attributes.style[k] = 'Npx';
+ }
+ }
+ coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
+ }
+ } else if (coordinatesReg.test(a.attributes.style)) {
+ a.attributes.style = a.attributes.style.replace(
+ coordinatesReg,
+ '$1: Npx',
+ );
+ }
+ }
+
+ // strip blob:urls as they are different every time
+ stripBlobURLsFromAttributes(a);
+ });
+ s.data.adds.forEach((add) => {
+ if (add.node.type === NodeType.Element) {
+ if (
+ 'style' in add.node.attributes &&
+ typeof add.node.attributes.style === 'string' &&
+ coordinatesReg.test(add.node.attributes.style)
+ ) {
+ add.node.attributes.style = add.node.attributes.style.replace(
+ coordinatesReg,
+ '$1: Npx',
+ );
+ }
+ coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
+
+ // strip blob:urls as they are different every time
+ stripBlobURLsFromAttributes(add.node);
+
+ // strip rr_dataURL as they are not consistent
+ if (
+ 'rr_dataURL' in add.node.attributes &&
+ add.node.attributes.rr_dataURL &&
+ typeof add.node.attributes.rr_dataURL === 'string'
+ ) {
+ add.node.attributes.rr_dataURL =
+ add.node.attributes.rr_dataURL.replace(/,.+$/, ',...');
+ }
+ }
+ });
+ } else if (
+ s.type === EventType.IncrementalSnapshot &&
+ s.data.source === IncrementalSource.MediaInteraction
+ ) {
+ // round the currentTime to 1 decimal place
+ if (s.data.currentTime) {
+ s.data.currentTime = Math.round(s.data.currentTime * 10) / 10;
+ }
+ } else if (
+ s.type === EventType.Plugin &&
+ s.data.plugin === 'rrweb/console@1'
+ ) {
+ const pluginPayload = (
+ s as pluginEvent<{
+ trace: string[];
+ payload: string[];
+ }>
+ ).data.payload;
+
+ if (pluginPayload?.trace.length) {
+ pluginPayload.trace = pluginPayload.trace.map((trace) => {
+ return trace.replace(
+ /^pptr:evaluate;.*?:(\d+:\d+)/,
+ '__puppeteer_evaluation_script__:$1',
+ );
+ });
+ }
+ if (pluginPayload?.payload.length) {
+ pluginPayload.payload = pluginPayload.payload.map((payload) => {
+ return payload.replace(
+ /pptr:evaluate;.*?:(\d+:\d+)/g,
+ '__puppeteer_evaluation_script__:$1',
+ );
+ });
+ }
+ }
+ delete (s as Optional).timestamp;
+ return s as event;
+ }),
+ null,
+ 2,
+ ).replace(
+ // servers might get run on a random port,
+ // so we need to normalize the port number
+ /http:\/\/localhost:\d+/g,
+ 'http://localhost:3030',
+ );
+}
+
+function stripBlobURLsFromAttributes(node: {
+ attributes: {
+ src?: string;
+ };
+}) {
+ if (
+ 'src' in node.attributes &&
+ node.attributes.src &&
+ typeof node.attributes.src === 'string' &&
+ node.attributes.src.startsWith('blob:')
+ ) {
+ node.attributes.src = node.attributes.src
+ .replace(/[\w-]+$/, '...')
+ .replace(/:[0-9]+\//, ':xxxx/');
+ }
+}
+
+export async function assertSnapshot(
+ snapshotsOrPage: eventWithTime[] | puppeteer.Page,
+) {
+ let snapshots: eventWithTime[];
+ if (!Array.isArray(snapshotsOrPage)) {
+ // make sure page has finished executing js
+ await waitForRAF(snapshotsOrPage);
+ await snapshotsOrPage.waitForFunction(
+ 'window.snapshots && window.snapshots.length > 0',
+ );
+
+ snapshots = (await snapshotsOrPage.evaluate(
+ 'window.snapshots',
+ )) as eventWithTime[];
+ } else {
+ snapshots = snapshotsOrPage;
+ }
+
+ expect(snapshots).toBeDefined();
+ expect(stringifySnapshots(snapshots)).toMatchSnapshot();
+}
+
+export async function waitForRAF(
+ pageOrFrame: puppeteer.Page | puppeteer.Frame,
+) {
+ return await pageOrFrame.evaluate(() => {
+ return new Promise((resolve) => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ });
+ });
+ });
+}
diff --git a/packages/all/tsconfig.json b/packages/all/tsconfig.json
new file mode 100644
index 0000000000..5de77cef36
--- /dev/null
+++ b/packages/all/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "rootDir": "src",
+ "tsBuildInfoFile": "./tsconfig.tsbuildinfo"
+ },
+ "references": [
+ {
+ "path": "../types"
+ },
+ {
+ "path": "../packer"
+ },
+ {
+ "path": "../rrweb"
+ }
+ ]
+}
diff --git a/packages/all/vite.config.ts b/packages/all/vite.config.ts
new file mode 100644
index 0000000000..cf4366a01d
--- /dev/null
+++ b/packages/all/vite.config.ts
@@ -0,0 +1,4 @@
+import path from 'path';
+import config from '../../vite.config.default';
+
+export default config(path.resolve(__dirname, 'src/index.ts'), 'rrweb');
diff --git a/packages/all/vitest.config.ts b/packages/all/vitest.config.ts
new file mode 100644
index 0000000000..b168428dbf
--- /dev/null
+++ b/packages/all/vitest.config.ts
@@ -0,0 +1,5 @@
+///
+import { defineProject, mergeConfig } from 'vitest/config';
+import configShared from '../../vitest.config';
+
+export default mergeConfig(configShared, defineProject({}));
diff --git a/packages/packer/.gitignore b/packages/packer/.gitignore
new file mode 100644
index 0000000000..d29f9959b9
--- /dev/null
+++ b/packages/packer/.gitignore
@@ -0,0 +1,4 @@
+.turbo
+dist
+node_modules
+yarn-error.log
\ No newline at end of file
diff --git a/packages/packer/README.md b/packages/packer/README.md
new file mode 100644
index 0000000000..7c649605fa
--- /dev/null
+++ b/packages/packer/README.md
@@ -0,0 +1,180 @@
+# @rrweb/packer
+
+`@rrweb/packer` is a tool to compress rrweb events into a smaller size.
+
+See the [storage recipe](../../docs/recipes/optimize-storage.md#compression) for more info on how this works.
+And the [guide](../../guide.md) for more info on rrweb.
+
+## Sponsors
+
+[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
+
+### Gold Sponsors 🥇
+
+
+
+### Silver Sponsors 🥈
+
+
+
+### Bronze Sponsors 🥉
+
+
+
+### Backers
+
+
+
+## Core Team Members
+
+
+
+## Who's using rrweb?
+
+
diff --git a/packages/packer/pack/package.json b/packages/packer/pack/package.json
new file mode 100644
index 0000000000..89e4e8ba31
--- /dev/null
+++ b/packages/packer/pack/package.json
@@ -0,0 +1,4 @@
+{
+ "main": "../dist/pack.cjs",
+ "types": "../dist/pack.d.ts"
+}
diff --git a/packages/packer/package.json b/packages/packer/package.json
new file mode 100644
index 0000000000..25e7919129
--- /dev/null
+++ b/packages/packer/package.json
@@ -0,0 +1,86 @@
+{
+ "name": "@rrweb/packer",
+ "version": "2.0.0-alpha.14",
+ "publishConfig": {
+ "access": "public"
+ },
+ "keywords": [
+ "rrweb",
+ "@rrweb/packer"
+ ],
+ "scripts": {
+ "dev": "vite build --watch",
+ "build": "tsc -noEmit && vite build",
+ "test": "vitest run",
+ "test:watch": "vitest watch",
+ "check-types": "tsc -noEmit",
+ "prepublish": "npm run build",
+ "lint": "yarn eslint src/**/*.ts"
+ },
+ "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/packer#readme",
+ "bugs": {
+ "url": "https://github.com/rrweb-io/rrweb/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rrweb-io/rrweb.git"
+ },
+ "license": "MIT",
+ "type": "module",
+ "main": "./dist/packer.cjs",
+ "module": "./dist/packer.js",
+ "unpkg": "./dist/packer.js",
+ "typings": "dist/packer.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/packer.d.ts",
+ "default": "./dist/packer.js"
+ },
+ "require": {
+ "types": "./dist/packer.d.cts",
+ "default": "./dist/packer.cjs"
+ }
+ },
+ "./pack": {
+ "import": {
+ "types": "./dist/pack.d.ts",
+ "default": "./dist/pack.js"
+ },
+ "require": {
+ "types": "./dist/pack.d.cts",
+ "default": "./dist/pack.cjs"
+ }
+ },
+ "./unpack": {
+ "import": {
+ "types": "./dist/unpack.d.ts",
+ "default": "./dist/unpack.js"
+ },
+ "require": {
+ "types": "./dist/unpack.d.cts",
+ "default": "./dist/unpack.cjs"
+ }
+ }
+ },
+ "files": [
+ "pack",
+ "unpack",
+ "build",
+ "dist",
+ "package.json"
+ ],
+ "devDependencies": {
+ "vite": "^5.2.8",
+ "vite-plugin-dts": "^3.8.1",
+ "vitest": "^1.4.0",
+ "typescript": "^4.7.3"
+ },
+ "dependencies": {
+ "fflate": "^0.4.4",
+ "@rrweb/types": "^2.0.0-alpha.14"
+ },
+ "browserslist": [
+ "supports es6-class"
+ ]
+}
diff --git a/packages/rrweb/src/packer/base.ts b/packages/packer/src/base.ts
similarity index 59%
rename from packages/rrweb/src/packer/base.ts
rename to packages/packer/src/base.ts
index 88c3122cae..fc9435c6ed 100644
--- a/packages/rrweb/src/packer/base.ts
+++ b/packages/packer/src/base.ts
@@ -1,8 +1,5 @@
import type { eventWithTime } from '@amplitude/rrweb-types';
-export type PackFn = (event: eventWithTime) => string;
-export type UnpackFn = (raw: string) => eventWithTime;
-
export type eventWithTimeAndPacker = eventWithTime & {
v: string;
};
diff --git a/packages/rrweb/src/packer/index.ts b/packages/packer/src/index.ts
similarity index 100%
rename from packages/rrweb/src/packer/index.ts
rename to packages/packer/src/index.ts
diff --git a/packages/rrweb/src/packer/pack.ts b/packages/packer/src/pack.ts
similarity index 70%
rename from packages/rrweb/src/packer/pack.ts
rename to packages/packer/src/pack.ts
index 5fce47ccbb..dfe5bc2590 100644
--- a/packages/rrweb/src/packer/pack.ts
+++ b/packages/packer/src/pack.ts
@@ -1,5 +1,6 @@
import { strFromU8, strToU8, zlibSync } from 'fflate';
-import { PackFn, MARK, eventWithTimeAndPacker } from './base';
+import type { PackFn } from '@rrweb/types';
+import { eventWithTimeAndPacker, MARK } from './base';
export const pack: PackFn = (event) => {
const _e: eventWithTimeAndPacker = {
diff --git a/packages/rrweb/src/packer/unpack.ts b/packages/packer/src/unpack.ts
similarity index 85%
rename from packages/rrweb/src/packer/unpack.ts
rename to packages/packer/src/unpack.ts
index a4710fa707..0e0327e343 100644
--- a/packages/rrweb/src/packer/unpack.ts
+++ b/packages/packer/src/unpack.ts
@@ -1,6 +1,6 @@
-import type { eventWithTime } from '@amplitude/rrweb-types';
+import type { UnpackFn, eventWithTime } from '@amplitude/rrweb-types';
import { strFromU8, strToU8, unzlibSync } from 'fflate';
-import { MARK, UnpackFn, eventWithTimeAndPacker } from './base';
+import { eventWithTimeAndPacker, MARK } from './base';
export const unpack: UnpackFn = (raw: string) => {
if (typeof raw !== 'string') {
diff --git a/packages/packer/test/__snapshots__/packer.test.ts.snap b/packages/packer/test/__snapshots__/packer.test.ts.snap
new file mode 100644
index 0000000000..fbd879662c
Binary files /dev/null and b/packages/packer/test/__snapshots__/packer.test.ts.snap differ
diff --git a/packages/rrweb/test/packer.test.ts b/packages/packer/test/packer.test.ts
similarity index 75%
rename from packages/rrweb/test/packer.test.ts
rename to packages/packer/test/packer.test.ts
index 54c4026129..9ef43508d6 100644
--- a/packages/rrweb/test/packer.test.ts
+++ b/packages/packer/test/packer.test.ts
@@ -1,6 +1,7 @@
-import { EventType, eventWithTime } from '@amplitude/rrweb-types';
-import { pack, unpack } from '../src/packer';
-import { MARK } from '../src/packer/base';
+import { type eventWithTime, EventType } from '@amplitude/rrweb-types';
+import { pack, unpack } from '../src';
+import { MARK } from '../src/base';
+import { describe, it, expect, vi } from 'vitest';
const event: eventWithTime = {
type: EventType.DomContentLoaded,
@@ -27,14 +28,12 @@ describe('unpack', () => {
});
it('stop on unknown data format', () => {
- const consoleSpy = jest
- .spyOn(console, 'error')
- .mockImplementation(() => {});
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(() => unpack('[""]')).toThrow('');
expect(consoleSpy).toHaveBeenCalled();
- jest.resetAllMocks();
+ vi.resetAllMocks();
});
it('can unpack packed data', () => {
diff --git a/packages/packer/tsconfig.json b/packages/packer/tsconfig.json
new file mode 100644
index 0000000000..262aaa39da
--- /dev/null
+++ b/packages/packer/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src"],
+ "exclude": ["vite.config.ts", "test"],
+ "compilerOptions": {
+ "rootDir": "src",
+ "tsBuildInfoFile": "./tsconfig.tsbuildinfo"
+ },
+ "references": [
+ {
+ "path": "../types"
+ }
+ ]
+}
diff --git a/packages/packer/unpack/package.json b/packages/packer/unpack/package.json
new file mode 100644
index 0000000000..96eb397ef3
--- /dev/null
+++ b/packages/packer/unpack/package.json
@@ -0,0 +1,4 @@
+{
+ "main": "../dist/unpack.cjs",
+ "types": "../dist/unpack.d.ts"
+}
diff --git a/packages/packer/vite.config.ts b/packages/packer/vite.config.ts
new file mode 100644
index 0000000000..3a5c09212f
--- /dev/null
+++ b/packages/packer/vite.config.ts
@@ -0,0 +1,11 @@
+import path from 'path';
+import config from '../../vite.config.default';
+
+export default config(
+ {
+ packer: path.resolve(__dirname, 'src/index.ts'),
+ pack: path.resolve(__dirname, 'src/pack.ts'),
+ unpack: path.resolve(__dirname, 'src/unpack.ts'),
+ },
+ 'rrwebPacker',
+);
diff --git a/packages/rrweb/src/plugins/canvas-webrtc/Readme.md b/packages/plugins/rrweb-plugin-canvas-webrtc-record/Readme.md
similarity index 100%
rename from packages/rrweb/src/plugins/canvas-webrtc/Readme.md
rename to packages/plugins/rrweb-plugin-canvas-webrtc-record/Readme.md
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json b/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json
new file mode 100644
index 0000000000..de391f878d
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@rrweb/rrweb-plugin-canvas-webrtc-record",
+ "version": "2.0.0-alpha.14",
+ "description": "",
+ "type": "module",
+ "main": "./dist/rrweb-plugin-canvas-webrtc-record.umd.cjs",
+ "module": "./dist/rrweb-plugin-canvas-webrtc-record.js",
+ "unpkg": "./dist/rrweb-plugin-canvas-webrtc-record.umd.cjs",
+ "typings": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/rrweb-plugin-canvas-webrtc-record.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/rrweb-plugin-canvas-webrtc-record.cjs"
+ }
+ }
+ },
+ "files": [
+ "dist",
+ "package.json"
+ ],
+ "scripts": {
+ "dev": "vite build --watch",
+ "build": "tsc -noEmit && vite build",
+ "check-types": "tsc -noEmit",
+ "prepublish": "npm run build"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rrweb-io/rrweb.git"
+ },
+ "keywords": [
+ "rrweb"
+ ],
+ "author": "justin@recordonce.com",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/rrweb-io/rrweb/issues"
+ },
+ "homepage": "https://github.com/rrweb-io/rrweb#readme",
+ "devDependencies": {
+ "rrweb": "^2.0.0-alpha.14",
+ "typescript": "^4.7.3",
+ "vite": "^5.2.8",
+ "vite-plugin-dts": "^3.8.1"
+ },
+ "peerDependencies": {
+ "rrweb": "^2.0.0-alpha.14"
+ }
+}
diff --git a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts
similarity index 95%
rename from packages/rrweb/src/plugins/canvas-webrtc/record/index.ts
rename to packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts
index 6a71315804..45b779ba81 100644
--- a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts
@@ -4,7 +4,7 @@ import type {
RecordPlugin,
} from '@amplitude/rrweb-types';
import SimplePeer from 'simple-peer-light';
-import type { WebRTCDataChannel } from '../types';
+import type { WebRTCDataChannel } from './types';
export const PLUGIN_NAME = 'rrweb/canvas-webrtc@1';
@@ -28,8 +28,8 @@ export type CrossOriginIframeMessageEventContent = {
export class RRWebPluginCanvasWebRTCRecord {
private peer: SimplePeer.Instance | null = null;
- private mirror: Mirror;
- private crossOriginIframeMirror: ICrossOriginIframeMirror;
+ private mirror: Mirror | undefined;
+ private crossOriginIframeMirror: ICrossOriginIframeMirror | undefined;
private streamMap: Map = new Map();
private incomingStreams = new Set();
private outgoingStreams = new Set();
@@ -47,13 +47,11 @@ export class RRWebPluginCanvasWebRTCRecord {
peer?: SimplePeer.Instance;
}) {
this.signalSendCallback = signalSendCallback;
- window.addEventListener(
- 'message',
- this.windowPostMessageHandler.bind(this),
+ window.addEventListener('message', (event: MessageEvent) =>
+ this.windowPostMessageHandler(event),
);
if (peer) this.peer = peer;
}
-
public initPlugin(): RecordPlugin {
return {
name: PLUGIN_NAME,
@@ -196,7 +194,7 @@ export class RRWebPluginCanvasWebRTCRecord {
}
public setupStream(id: number, rootId?: number): boolean | MediaStream {
- if (id === -1) return false;
+ if (id === -1 || !this.mirror) return false;
let stream: MediaStream | undefined = this.streamMap.get(rootId || id);
if (stream) return stream;
@@ -244,6 +242,7 @@ export class RRWebPluginCanvasWebRTCRecord {
document.querySelectorAll('iframe').forEach((iframe) => {
if (found) return;
+ if (!this.crossOriginIframeMirror) return;
const remoteId = this.crossOriginIframeMirror.getRemoteId(iframe, id);
if (remoteId === -1) return;
diff --git a/packages/rrweb/src/plugins/canvas-webrtc/simple-peer-light.d.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-record/src/simple-peer-light.d.ts
similarity index 100%
rename from packages/rrweb/src/plugins/canvas-webrtc/simple-peer-light.d.ts
rename to packages/plugins/rrweb-plugin-canvas-webrtc-record/src/simple-peer-light.d.ts
diff --git a/packages/rrweb/src/plugins/canvas-webrtc/types.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-record/src/types.ts
similarity index 100%
rename from packages/rrweb/src/plugins/canvas-webrtc/types.ts
rename to packages/plugins/rrweb-plugin-canvas-webrtc-record/src/types.ts
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/tsconfig.json b/packages/plugins/rrweb-plugin-canvas-webrtc-record/tsconfig.json
new file mode 100644
index 0000000000..8ffb27ccca
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "exclude": ["vite.config.ts"],
+ "compilerOptions": {
+ "rootDir": "src",
+ "tsBuildInfoFile": "./tsconfig.tsbuildinfo"
+ },
+ "references": [
+ {
+ "path": "../../rrweb"
+ }
+ ]
+}
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/vite.config.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-record/vite.config.ts
new file mode 100644
index 0000000000..202a314ffd
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/vite.config.ts
@@ -0,0 +1,3 @@
+import config from '../../../vite.config.default';
+
+export default config('src/index.ts', 'rrwebPluginCanvasWebRTCRecord');
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/Readme.md b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/Readme.md
new file mode 100644
index 0000000000..fcc6a725f7
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/Readme.md
@@ -0,0 +1,248 @@
+# rrweb canvas webrtc plugin
+
+Plugin that live streams contents of canvas elements via webrtc
+
+## Example of live streaming via `yarn live-stream`
+
+https://user-images.githubusercontent.com/4106/186701616-fd71a107-5d53-423c-ba09-0395a3a0252f.mov
+
+## Instructions
+
+### Record side
+
+```js
+// Record side
+
+import rrweb from 'rrweb';
+import { RRWebPluginCanvasWebRTCRecord } from '@rrweb/rrweb-plugin-canvas-webrtc-record';
+
+const webRTCRecordPlugin = new RRWebPluginCanvasWebRTCRecord({
+ signalSendCallback: (msg) => {
+ // provides webrtc sdp offer signal & connect message
+ // make sure you send this to the replayer's `webRTCReplayPlugin.signalReceive(signal)`
+ sendSignalToReplayer(msg); // example of function that sends the signal to the replayer
+ },
+});
+
+rrweb.record({
+ emit: (event) => {
+ // send these events to the `replayer.addEvent(event)`, how you do that is up to you
+ // you can send them to a server for example which can then send them to the replayer
+ sendEventToReplayer(event); // example of function that sends the event to the replayer
+ },
+ plugins: [
+ // add the plugin to the list of plugins, and initialize it via `.initPlugin()`
+ webRTCRecordPlugin.initPlugin(),
+ ],
+ recordCanvas: false, // we don't want canvas recording turned on, we're going to do that via the plugin
+});
+```
+
+### Replay Side
+
+```js
+// Replay side
+import rrweb from 'rrweb';
+import { RRWebPluginCanvasWebRTCReplay } from '@rrweb/rrweb-plugin-canvas-webrtc-replay';
+
+const webRTCReplayPlugin = new RRWebPluginCanvasWebRTCReplay({
+ canvasFoundCallback(canvas, context) {
+ console.log('canvas', canvas);
+ // send the canvas id to `webRTCRecordPlugin.setupStream(id)`, how you do that is up to you
+ // you can send them to a server for example which can then send them to the replayer
+ sendCanvasIdToRecordScript(context.id); // example of function that sends the id to the record script
+ },
+ signalSendCallback(signal) {
+ // provides webrtc sdp offer signal & connect message
+ // make sure you send this to the record script's `webRTCRecordPlugin.signalReceive(signal)`
+ sendSignalToRecordScript(signal); // example of function that sends the signal to the record script
+ },
+});
+
+const replayer = new rrweb.Replayer([], {
+ UNSAFE_replayCanvas: true, // turn canvas replay on!
+ liveMode: true, // live mode is needed to stream events to the replayer
+ plugins: [webRTCReplayPlugin.initPlugin()],
+});
+replayer.startLive(); // start the replayer in live mode
+
+replayer.addEvent(event); // call this whenever an event is received from the record script
+```
+
+## More info
+
+https://github.com/rrweb-io/rrweb/pull/976
+
+## Sponsors
+
+[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
+
+### Gold Sponsors 🥇
+
+
+
+### Silver Sponsors 🥈
+
+
+
+### Bronze Sponsors 🥉
+
+
+
+### Backers
+
+
+
+## Core Team Members
+
+
+
+## Who's using rrweb?
+
+
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json
new file mode 100644
index 0000000000..61d7cb6e6d
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@rrweb/rrweb-plugin-canvas-webrtc-replay",
+ "version": "2.0.0-alpha.14",
+ "description": "",
+ "type": "module",
+ "main": "./dist/rrweb-plugin-canvas-webrtc-replay.umd.cjs",
+ "module": "./dist/rrweb-plugin-canvas-webrtc-replay.js",
+ "unpkg": "./dist/rrweb-plugin-canvas-webrtc-replay.umd.cjs",
+ "typings": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/rrweb-plugin-canvas-webrtc-replay.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/rrweb-plugin-canvas-webrtc-replay.cjs"
+ }
+ }
+ },
+ "files": [
+ "dist",
+ "package.json"
+ ],
+ "scripts": {
+ "dev": "vite build --watch",
+ "build": "tsc -noEmit && vite build",
+ "check-types": "tsc -noEmit",
+ "prepublish": "npm run build"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rrweb-io/rrweb.git"
+ },
+ "keywords": [
+ "rrweb"
+ ],
+ "author": "justin@recordonce.com",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/rrweb-io/rrweb/issues"
+ },
+ "homepage": "https://github.com/rrweb-io/rrweb#readme",
+ "devDependencies": {
+ "rrweb": "^2.0.0-alpha.14",
+ "typescript": "^4.7.3",
+ "vite": "^5.2.8",
+ "vite-plugin-dts": "^3.8.1"
+ },
+ "peerDependencies": {
+ "rrweb": "^2.0.0-alpha.14"
+ }
+}
diff --git a/packages/rrweb/src/plugins/canvas-webrtc/replay/index.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts
similarity index 95%
rename from packages/rrweb/src/plugins/canvas-webrtc/replay/index.ts
rename to packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts
index 79144cab48..f6604ec4aa 100644
--- a/packages/rrweb/src/plugins/canvas-webrtc/replay/index.ts
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts
@@ -1,9 +1,8 @@
import type { RRNode } from '@amplitude/rrdom';
import type { Mirror } from '@amplitude/rrweb-snapshot';
import SimplePeer from 'simple-peer-light';
-import type { Replayer } from '../../../replay';
-import type { ReplayPlugin } from '../../../types';
-import type { WebRTCDataChannel } from '../types';
+import type { ReplayPlugin, Replayer } from 'rrweb';
+import type { WebRTCDataChannel } from './types';
// TODO: restrict callback to real nodes only, or make sure callback gets called when real node gets added to dom as well
@@ -13,7 +12,7 @@ export class RRWebPluginCanvasWebRTCReplay {
context: { id: number; replayer: Replayer },
) => void;
private signalSendCallback: (signal: RTCSessionDescriptionInit) => void;
- private mirror: Mirror;
+ private mirror: Mirror | undefined;
constructor({
canvasFoundCallback,
@@ -156,7 +155,7 @@ export class RRWebPluginCanvasWebRTCReplay {
this.streams.forEach((stream) => {
const nodeId = this.streamNodeMap.get(stream.id);
if (!nodeId) return;
- const target = this.mirror.getNode(nodeId) as
+ const target = this.mirror?.getNode(nodeId) as
| HTMLCanvasElement
| HTMLVideoElement
| null;
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/simple-peer-light.d.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/simple-peer-light.d.ts
new file mode 100644
index 0000000000..9f7fc8687f
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/simple-peer-light.d.ts
@@ -0,0 +1,309 @@
+/* eslint-disable */
+///
+declare module 'simple-peer-light' {
+ import * as stream from 'stream';
+
+ const SimplePeer: SimplePeer.SimplePeer;
+
+ namespace SimplePeer {
+ interface Options {
+ /** set to `true` if this is the initiating peer */
+ initiator?: boolean | undefined;
+ /** custom webrtc data channel configuration (used by [`createDataChannel`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel)) */
+ channelConfig?: RTCDataChannelInit | undefined;
+ /** custom webrtc data channel name */
+ channelName?: string | undefined;
+ /** custom webrtc configuration (used by [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) constructor) */
+ config?: RTCConfiguration | undefined;
+ /** custom offer options (used by [`createOffer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer) method) */
+ offerOptions?: RTCOfferOptions | undefined;
+ /** custom answer options (used by [`createAnswer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer) method) */
+ answerOptions?: RTCAnswerOptions | undefined;
+ /** function to transform the generated SDP signaling data (for advanced users) */
+ sdpTransform?: ((this: Instance, sdp: string) => string) | undefined;
+ /** if video/voice is desired, pass stream returned from [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) */
+ stream?: MediaStream | undefined;
+ /** an array of MediaStreams returned from [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) */
+ streams?: MediaStream[] | undefined;
+ /** set to `false` to disable [trickle ICE](http://webrtchacks.com/trickle-ice/) and get a single 'signal' event (slower) */
+ trickle?: boolean | undefined;
+ /** similar to `trickle`, needs to be set to `false` to disable trickling, defaults to `false` */
+ allowHalfTrickle?: boolean | undefined;
+ /** if `trickle` is set to `false`, determines how long to wait before providing an offer or answer; default value is 5000 milliseconds */
+ iceCompleteTimeout?: number | undefined;
+ /** custom webrtc implementation, mainly useful in node to specify in the [wrtc](https://npmjs.com/package/wrtc) package. */
+ wrtc?:
+ | {
+ RTCPeerConnection: typeof RTCPeerConnection;
+ RTCSessionDescription: typeof RTCSessionDescription;
+ RTCIceCandidate: typeof RTCIceCandidate;
+ }
+ | undefined;
+ /** set to true to create the stream in Object Mode. In this mode, incoming string data is not automatically converted to Buffer objects. */
+ objectMode?: boolean | undefined;
+ }
+ interface SimplePeer {
+ prototype: Instance;
+ /**
+ * Create a new WebRTC peer connection.
+ *
+ * A "data channel" for text/binary communication is always established, because it's cheap and often useful. For video/voice communication, pass the stream option.
+ *
+ * If opts is specified, then the default options (see ) will be overridden.
+ */
+ new (opts?: Options): Instance;
+
+ /** Detect native WebRTC support in the javascript environment. */
+ readonly WEBRTC_SUPPORT: boolean;
+
+ // ********************************
+ // methods which are not documented
+ // ********************************
+
+ /**
+ * Expose peer and data channel config for overriding all Peer
+ * instances. Otherwise, just set opts.config or opts.channelConfig
+ * when constructing a Peer.
+ */
+ config: RTCConfiguration;
+ /**
+ * Expose peer and data channel config for overriding all Peer
+ * instances. Otherwise, just set opts.config or opts.channelConfig
+ * when constructing a Peer.
+ */
+ channelConfig: RTCDataChannelInit;
+ }
+
+ type TypedArray =
+ | Int8Array
+ | Uint8Array
+ | Uint8ClampedArray
+ | Int16Array
+ | Uint16Array
+ | Int32Array
+ | Uint32Array
+ | Float32Array
+ | Float64Array;
+
+ type SimplePeerData = string | Buffer | TypedArray | ArrayBuffer | Blob;
+
+ type SignalData =
+ | {
+ type: 'transceiverRequest';
+ transceiverRequest: {
+ kind: string;
+ init?: RTCRtpTransceiverInit | undefined;
+ };
+ }
+ | {
+ type: 'renegotiate';
+ renegotiate: true;
+ }
+ | {
+ type: 'candidate';
+ candidate: RTCIceCandidate;
+ }
+ | RTCSessionDescriptionInit;
+
+ interface Instance extends stream.Duplex {
+ /**
+ * Call this method whenever the remote peer emits a `peer.on('signal')` event.
+ *
+ * The `data` will encapsulate a webrtc offer, answer, or ice candidate. These messages help
+ * the peers to eventually establish a direct connection to each other. The contents of these
+ * strings are an implementation detail that can be ignored by the user of this module;
+ * simply pass the data from 'signal' events to the remote peer and call `peer.signal(data)`
+ * to get connected.
+ */
+ signal(data: string | SignalData): void;
+
+ /**
+ * Send text/binary data to the remote peer. `data` can be any of several types: `String`,
+ * `Buffer` (see [buffer](https://github.com/feross/buffer)), `ArrayBufferView` (`Uint8Array`,
+ * etc.), `ArrayBuffer`, or `Blob` (in browsers that support it).
+ *
+ * Note: If this method is called before the `peer.on('connect')` event has fired,
+ * then an exception will be thrown. Use `peer.write(data)`
+ * (which is inherited from the node.js [duplex stream](https://nodejs.org/api/stream.html) interface)
+ * if you want this data to be buffered instead.
+ */
+ send(data: SimplePeerData): void;
+
+ /** Add a `MediaStream` to the connection. */
+ addStream(stream: MediaStream): void;
+
+ /** Remove a `MediaStream` from the connection. */
+ removeStream(stream: MediaStream): void;
+
+ /** Add a `MediaStreamTrack` to the connection. Must also pass the `MediaStream` you want to attach it to. */
+ addTrack(track: MediaStreamTrack, stream: MediaStream): void;
+
+ /** Remove a `MediaStreamTrack` from the connection. Must also pass the `MediaStream` that it was attached to. */
+ removeTrack(track: MediaStreamTrack, stream: MediaStream): void;
+
+ /** Replace a `MediaStreamTrack` with another track. Must also pass the `MediaStream` that the old track was attached to. */
+ replaceTrack(
+ oldTrack: MediaStreamTrack,
+ newTrack: MediaStreamTrack,
+ stream: MediaStream,
+ ): void;
+
+ /** Add a `RTCRtpTransceiver` to the connection. Can be used to add transceivers before adding tracks. Automatically called as necessary by `addTrack`. */
+ addTransceiver(kind: string, init?: RTCRtpTransceiverInit): void;
+
+ // TODO: https://github.com/feross/simple-peer/blob/d972548299a50f836ca91c36e39304ef0f9474b7/index.js#L427
+ // destroy(onclose?: () => void): void;
+ /**
+ * Destroy and cleanup this peer connection.
+ *
+ * If the optional `err` parameter is passed, then it will be emitted as an `'error'`
+ * event on the stream.
+ */
+ destroy(error?: Error): any;
+
+ // ********************************
+ // methods which are not documented
+ // ********************************
+
+ readonly bufferSize: number;
+ readonly connected: boolean;
+ address():
+ | { port: undefined; family: undefined; address: undefined }
+ | { port: number; family: 'IPv6' | 'IPv4'; address: string };
+
+ // used for debug logging
+ _debug(message?: any, ...optionalParams: any[]): void;
+
+ // ******
+ // events
+ // ******
+ addListener(
+ event: 'connect' | 'close' | 'end' | 'pause' | 'readable' | 'resume',
+ listener: () => void,
+ ): this;
+ addListener(event: 'signal', listener: (data: SignalData) => void): this;
+ addListener(
+ event: 'stream',
+ listener: (stream: MediaStream) => void,
+ ): this;
+ addListener(
+ event: 'track',
+ listener: (track: MediaStreamTrack, stream: MediaStream) => void,
+ ): this;
+ addListener(event: 'data', listener: (chunk: any) => void): this;
+ addListener(event: 'error', listener: (err: Error) => void): this;
+ addListener(
+ event: string | symbol,
+ listener: (...args: any[]) => void,
+ ): this;
+
+ emit(
+ event: 'connect' | 'close' | 'end' | 'pause' | 'readable' | 'resume',
+ ): boolean;
+ emit(event: 'signal', data: SignalData): this;
+ emit(event: 'stream', stream: MediaStream): this;
+ emit(event: 'track', track: MediaStreamTrack, stream: MediaStream): this;
+ emit(event: 'data', chunk: any): boolean;
+ emit(event: 'error', err: Error): boolean;
+ emit(event: string | symbol, ...args: any[]): boolean;
+
+ on(
+ event: 'connect' | 'close' | 'end' | 'pause' | 'readable' | 'resume',
+ listener: () => void,
+ ): this;
+ on(event: 'signal', listener: (data: SignalData) => void): this;
+ on(event: 'stream', listener: (stream: MediaStream) => void): this;
+ on(
+ event: 'track',
+ listener: (track: MediaStreamTrack, stream: MediaStream) => void,
+ ): this;
+ on(event: 'data', listener: (chunk: any) => void): this;
+ on(event: 'error', listener: (err: Error) => void): this;
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
+
+ once(
+ event: 'connect' | 'close' | 'end' | 'pause' | 'readable' | 'resume',
+ listener: () => void,
+ ): this;
+ once(event: 'signal', listener: (data: SignalData) => void): this;
+ once(event: 'stream', listener: (stream: MediaStream) => void): this;
+ once(
+ event: 'track',
+ listener: (track: MediaStreamTrack, stream: MediaStream) => void,
+ ): this;
+ once(event: 'data', listener: (chunk: any) => void): this;
+ once(event: 'error', listener: (err: Error) => void): this;
+ once(event: string | symbol, listener: (...args: any[]) => void): this;
+
+ prependListener(
+ event: 'connect' | 'close' | 'end' | 'pause' | 'readable' | 'resume',
+ listener: () => void,
+ ): this;
+ prependListener(
+ event: 'signal',
+ listener: (data: SignalData) => void,
+ ): this;
+ prependListener(
+ event: 'stream',
+ listener: (stream: MediaStream) => void,
+ ): this;
+ prependListener(
+ event: 'track',
+ listener: (track: MediaStreamTrack, stream: MediaStream) => void,
+ ): this;
+ prependListener(event: 'data', listener: (chunk: any) => void): this;
+ prependListener(event: 'error', listener: (err: Error) => void): this;
+ prependListener(
+ event: string | symbol,
+ listener: (...args: any[]) => void,
+ ): this;
+
+ prependOnceListener(
+ event: 'connect' | 'close' | 'end' | 'pause' | 'readable' | 'resume',
+ listener: () => void,
+ ): this;
+ prependOnceListener(
+ event: 'signal',
+ listener: (data: SignalData) => void,
+ ): this;
+ prependOnceListener(
+ event: 'stream',
+ listener: (stream: MediaStream) => void,
+ ): this;
+ prependOnceListener(
+ event: 'track',
+ listener: (track: MediaStreamTrack, stream: MediaStream) => void,
+ ): this;
+ prependOnceListener(event: 'data', listener: (chunk: any) => void): this;
+ prependOnceListener(event: 'error', listener: (err: Error) => void): this;
+ prependOnceListener(
+ event: string | symbol,
+ listener: (...args: any[]) => void,
+ ): this;
+
+ removeListener(
+ event: 'connect' | 'close' | 'end' | 'pause' | 'readable' | 'resume',
+ listener: () => void,
+ ): this;
+ removeListener(
+ event: 'signal',
+ listener: (data: SignalData) => void,
+ ): this;
+ removeListener(
+ event: 'stream',
+ listener: (stream: MediaStream) => void,
+ ): this;
+ removeListener(
+ event: 'track',
+ listener: (track: MediaStreamTrack, stream: MediaStream) => void,
+ ): this;
+ removeListener(event: 'data', listener: (chunk: any) => void): this;
+ removeListener(event: 'error', listener: (err: Error) => void): this;
+ removeListener(
+ event: string | symbol,
+ listener: (...args: any[]) => void,
+ ): this;
+ }
+ }
+ export default SimplePeer;
+}
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/types.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/types.ts
new file mode 100644
index 0000000000..a20f82e5b9
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/types.ts
@@ -0,0 +1,4 @@
+export interface WebRTCDataChannel {
+ nodeId: number;
+ streamId: string;
+}
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/tsconfig.json b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/tsconfig.json
new file mode 100644
index 0000000000..8ffb27ccca
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "exclude": ["vite.config.ts"],
+ "compilerOptions": {
+ "rootDir": "src",
+ "tsBuildInfoFile": "./tsconfig.tsbuildinfo"
+ },
+ "references": [
+ {
+ "path": "../../rrweb"
+ }
+ ]
+}
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/vite.config.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/vite.config.ts
new file mode 100644
index 0000000000..0f0928d2d3
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/vite.config.ts
@@ -0,0 +1,3 @@
+import config from '../../../vite.config.default';
+
+export default config('src/index.ts', 'rrwebPluginCanvasWebRTCReplay');
diff --git a/packages/plugins/rrweb-plugin-console-record/README.md b/packages/plugins/rrweb-plugin-console-record/README.md
new file mode 100644
index 0000000000..7455fb0658
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-console-record/README.md
@@ -0,0 +1,178 @@
+# @rrweb/rrweb-plugin-console-record
+
+Please refer to the [console recipe](../../../docs/recipes/console.md) on how to use this plugin.
+See the [guide](../../../guide.md) for more info on rrweb.
+
+## Sponsors
+
+[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
+
+### Gold Sponsors 🥇
+
+
+
+### Silver Sponsors 🥈
+
+
+
+### Bronze Sponsors 🥉
+
+
+
+### Backers
+
+
+
+## Core Team Members
+
+
+
+## Who's using rrweb?
+
+
diff --git a/packages/plugins/rrweb-plugin-console-record/package.json b/packages/plugins/rrweb-plugin-console-record/package.json
new file mode 100644
index 0000000000..4e5bc78bd3
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-console-record/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "@rrweb/rrweb-plugin-console-record",
+ "version": "2.0.0-alpha.14",
+ "description": "",
+ "type": "module",
+ "main": "./dist/rrweb-plugin-console-record.umd.cjs",
+ "module": "./dist/rrweb-plugin-console-record.js",
+ "unpkg": "./dist/rrweb-plugin-console-record.umd.cjs",
+ "typings": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/rrweb-plugin-console-record.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/rrweb-plugin-console-record.umd.cjs"
+ }
+ }
+ },
+ "files": [
+ "dist",
+ "package.json"
+ ],
+ "scripts": {
+ "dev": "vite build --watch",
+ "test": "vitest run",
+ "test:watch": "vitest watch",
+ "build": "tsc -noEmit && vite build",
+ "check-types": "tsc -noEmit",
+ "prepublish": "npm run build"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rrweb-io/rrweb.git"
+ },
+ "keywords": [
+ "rrweb"
+ ],
+ "author": "yanzhen@smartx.com",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/rrweb-io/rrweb/issues"
+ },
+ "homepage": "https://github.com/rrweb-io/rrweb#readme",
+ "devDependencies": {
+ "rrweb": "^2.0.0-alpha.14",
+ "typescript": "^4.7.3",
+ "vite": "^5.2.8",
+ "vite-plugin-dts": "^3.8.1",
+ "vitest": "^1.4.0",
+ "puppeteer": "^20.9.0"
+ },
+ "peerDependencies": {
+ "rrweb": "^2.0.0-alpha.14"
+ }
+}
diff --git a/packages/rrweb/src/plugins/console/record/error-stack-parser.ts b/packages/plugins/rrweb-plugin-console-record/src/error-stack-parser.ts
similarity index 100%
rename from packages/rrweb/src/plugins/console/record/error-stack-parser.ts
rename to packages/plugins/rrweb-plugin-console-record/src/error-stack-parser.ts
diff --git a/packages/rrweb/src/plugins/console/record/index.ts b/packages/plugins/rrweb-plugin-console-record/src/index.ts
similarity index 99%
rename from packages/rrweb/src/plugins/console/record/index.ts
rename to packages/plugins/rrweb-plugin-console-record/src/index.ts
index c3ec2e4455..9918ffa74d 100644
--- a/packages/rrweb/src/plugins/console/record/index.ts
+++ b/packages/plugins/rrweb-plugin-console-record/src/index.ts
@@ -3,7 +3,7 @@ import type {
RecordPlugin,
listenerHandler,
} from '@amplitude/rrweb-types';
-import { patch } from '../../../utils';
+import { utils } from 'rrweb';
import { ErrorStackParser, StackFrame } from './error-stack-parser';
import { stringify } from './stringify';
@@ -187,7 +187,7 @@ function initLogObserver(
};
}
// replace the logger.{level}. return a restore function
- return patch(
+ return utils.patch(
_logger,
level,
(original: (...args: Array) => void) => {
diff --git a/packages/rrweb/src/plugins/console/record/stringify.ts b/packages/plugins/rrweb-plugin-console-record/src/stringify.ts
similarity index 100%
rename from packages/rrweb/src/plugins/console/record/stringify.ts
rename to packages/plugins/rrweb-plugin-console-record/src/stringify.ts
diff --git a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap
new file mode 100644
index 0000000000..ad900ed916
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap
@@ -0,0 +1,697 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`rrweb-plugin-console-record > should handle recursive console messages 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {
+ \\"type\\": \\"module\\",
+ \\"src\\": \\"http://localhost:3030/@vite/client\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 11
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"http-equiv\\": \\"X-UA-Compatible\\",
+ \\"content\\": \\"ie=edge\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 12
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 13
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Log record\\",
+ \\"id\\": 15
+ }
+ ],
+ \\"id\\": 14
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 16
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {
+ \\"src\\": \\"http://localhost:3030/test/html/index.ts\\",
+ \\"type\\": \\"module\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 17
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 18
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 19
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n\\\\n\\",
+ \\"id\\": 21
+ }
+ ],
+ \\"id\\": 20
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"log\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:21:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"Proxied object:\\\\\\"\\",
+ \\"\\\\\\"[object Object]\\\\\\"\\"
+ ]
+ }
+ }
+ }
+]"
+`;
+
+exports[`rrweb-plugin-console-record > should record console messages 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {
+ \\"type\\": \\"module\\",
+ \\"src\\": \\"http://localhost:3030/@vite/client\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 11
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"http-equiv\\": \\"X-UA-Compatible\\",
+ \\"content\\": \\"ie=edge\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 12
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 13
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Log record\\",
+ \\"id\\": 15
+ }
+ ],
+ \\"id\\": 14
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 16
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {
+ \\"src\\": \\"http://localhost:3030/test/html/index.ts\\",
+ \\"type\\": \\"module\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 17
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 18
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 19
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n\\\\n\\",
+ \\"id\\": 21
+ }
+ ],
+ \\"id\\": 20
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"assert\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:2:15\\"
+ ],
+ \\"payload\\": [
+ \\"true\\",
+ \\"\\\\\\"assert\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"count\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:3:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"count\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"countReset\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:4:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"count\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"debug\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:5:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"debug\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"dir\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:6:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"dir\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"dirxml\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:7:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"dirxml\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"group\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:8:15\\"
+ ],
+ \\"payload\\": []
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"groupCollapsed\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:9:15\\"
+ ],
+ \\"payload\\": []
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"info\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:10:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"info\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"log\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:11:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"log\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"table\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:12:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"table\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"time\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:13:15\\"
+ ],
+ \\"payload\\": []
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"timeEnd\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:14:15\\"
+ ],
+ \\"payload\\": []
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"timeLog\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:15:15\\"
+ ],
+ \\"payload\\": []
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"trace\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:16:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"trace\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"warn\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:17:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"warn\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"clear\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:18:15\\"
+ ],
+ \\"payload\\": []
+ }
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"log\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:19:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:19:19\\\\\\\\nEnd of stack for Error object\\\\\\"\\"
+ ]
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 20,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"iframe\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 22
+ }
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"adds\\": [
+ {
+ \\"parentId\\": 22,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 23,
+ \\"id\\": 25
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 23,
+ \\"id\\": 26
+ }
+ ],
+ \\"rootId\\": 23,
+ \\"id\\": 24
+ }
+ ],
+ \\"compatMode\\": \\"BackCompat\\",
+ \\"id\\": 23
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"isAttachIframe\\": true
+ }
+ },
+ {
+ \\"type\\": 6,
+ \\"data\\": {
+ \\"plugin\\": \\"rrweb/console@1\\",
+ \\"payload\\": {
+ \\"level\\": \\"log\\",
+ \\"trace\\": [
+ \\"__puppeteer_evaluation_script__:2:15\\"
+ ],
+ \\"payload\\": [
+ \\"\\\\\\"from iframe\\\\\\"\\"
+ ]
+ }
+ }
+ }
+]"
+`;
diff --git a/packages/plugins/rrweb-plugin-console-record/test/html/index.ts b/packages/plugins/rrweb-plugin-console-record/test/html/index.ts
new file mode 100644
index 0000000000..d701b7991e
--- /dev/null
+++ b/packages/plugins/rrweb-plugin-console-record/test/html/index.ts
@@ -0,0 +1,19 @@
+import type { eventWithTime } from '@rrweb/types';
+import { record } from 'rrweb';
+import { getRecordConsolePlugin } from '../../src/index';
+
+window.Date.now = () => new Date(Date.UTC(2018, 10, 15, 8)).valueOf();
+const snapshots: eventWithTime[] = ((window as any).snapshots = []);
+record({
+ emit: (event) => {
+ snapshots.push(event);
+ },
+ // maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
+ // maskAllInputs: ${options.maskAllInputs},
+ // maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
+ // userTriggeredOnInput: ${options.userTriggeredOnInput},
+ // maskTextFn: ${options.maskTextFn},
+ // recordCanvas: ${options.recordCanvas},
+ // inlineImages: ${options.inlineImages},
+ plugins: [getRecordConsolePlugin()],
+});
diff --git a/packages/rrweb/test/html/log.html b/packages/plugins/rrweb-plugin-console-record/test/html/log.html
similarity index 83%
rename from packages/rrweb/test/html/log.html
rename to packages/plugins/rrweb-plugin-console-record/test/html/log.html
index ba60e26e13..2e9c1baf0f 100644
--- a/packages/rrweb/test/html/log.html
+++ b/packages/plugins/rrweb-plugin-console-record/test/html/log.html
@@ -5,6 +5,7 @@
Log record
+