diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 6f98b26d8a..4a0355b07a 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -1,6 +1,6 @@
name: Tests
-on: [push, pull_request_target]
+on: [push, pull_request]
concurrency: ${{ github.workflow }}-${{ github.ref }}
diff --git a/docs/recipes/cross-origin-iframes.md b/docs/recipes/cross-origin-iframes.md
new file mode 100644
index 0000000000..89c01dee6e
--- /dev/null
+++ b/docs/recipes/cross-origin-iframes.md
@@ -0,0 +1,116 @@
+# Cross origin iframes
+
+By default browsers make it difficult to access the contents of an iframe that is hosted on a different domain. This is a security feature to prevent malicious sites from accessing sensitive information on other sites. It is possible to work around this security feature, but it is not recommended unless you [are very strict](https://stackoverflow.com/a/21629575) about allowing only the sites you trust to embed your website inside of an iframe.
+Since if you allow recording cross origin iframes, any malicious website can embed your website and as long as they have rrweb running they can record all the contents of your website.
+
+## How to record cross origin iframes
+
+Enable recording cross-origin iframes in your parent page:
+
+```js
+rrweb.record({
+ emit(event) {}, // all events will be emitted here, including events from cross origin iframes
+ recordCrossOriginIframes: true,
+});
+```
+
+Enable replaying cross-origin iframes in your child page:
+
+```js
+rrweb.record({
+ emit(event) {}, // this is required for rrweb, but the child page will not emit any events
+ recordCrossOriginIframes: true,
+});
+```
+
+## Considerations
+
+When cross origin iframe recording is turned on rrweb will check to see if it is being run in a top level window.
+If it isn't it'll send the events to the parent window via `postMessage`.
+
+If you don't have rrweb running in the top level window, the events will be lost when `recordCrossOriginIframes` is turned on.
+
+If the top level window is a malicious website it can listen to the events and send them to a server of its choosing.
+
+Or if a malicious script is running in on your page they can listen in on `postMessage` and as communication between the child and parent window is not encrypted. And they can see the events.
+
+## Options for injecting rrweb into cross origin iframes
+
+### 1. Website owners, add rrweb in the iframes
+
+If you own the website that with the iframe and the website that is being embedded in an iframe, you can add rrweb to both pages via a script tag.
+
+### 2. Browser extension
+
+See https://developer.chrome.com/docs/extensions/mv3/content_scripts/#functionality
+
+### 3. Puppeteer script
+
+```js
+import puppeteer from 'puppeteer';
+
+async function injectRecording(frame) {
+ await frame.evaluate((rrwebCode) => {
+ if (window.__IS_RECORDING__) return;
+ window.__IS_RECORDING__ = true;
+
+ (async () => {
+ function loadScript(code) {
+ const s = document.createElement('script');
+ s.type = 'text/javascript';
+ s.innerHTML = code;
+ if (document.head) {
+ document.head.append(s);
+ } else {
+ requestAnimationFrame(() => {
+ document.head.append(s);
+ });
+ }
+ }
+ loadScript(rrwebCode);
+
+ window.rrweb.record({
+ emit: (event) => {
+ window._captureEvent(event);
+ },
+ recordCrossOriginIframes: true,
+ });
+ })();
+ }, code);
+}
+
+const browser = await puppeteer.launch();
+const page = (await browser.pages())[0];
+
+const events = []; // contains all events from all frames
+
+await page.exposeFunction('_captureEvent', (event) => {
+ events.push(event);
+});
+
+page.on('framenavigated', async (frame) => {
+ await injectRecording(frame); // injects rrweb into the iframe
+});
+
+await page.goto('https://example.com');
+
+// your events are in the events array
+```
+
+### 4. Electron
+
+```ts
+const win = new BrowserWindow({
+ width: 800,
+ height: 600,
+ webPreferences: {
+ preload: path.join(__dirname, 'rrweb-recording-script.js'),
+ // this turns on preload inside iframes, but disables node integration
+ nodeIntegrationInSubFrames: true,
+ nodeIntegration: false,
+ },
+});
+```
+
+See https://www.electronjs.org/docs/latest/tutorial/tutorial-preload
+And https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
diff --git a/docs/recipes/cross-origin-iframes.zh_CN.md b/docs/recipes/cross-origin-iframes.zh_CN.md
new file mode 100644
index 0000000000..472f6b25c1
--- /dev/null
+++ b/docs/recipes/cross-origin-iframes.zh_CN.md
@@ -0,0 +1,117 @@
+# Cross origin iframes
+
+默认情况下,浏览器很难访问托管在不同域上的 iframe 的内容。 这是一项安全功能,可防止恶意站点访问其他站点上的敏感信息。 可以解决此安全功能。
+但不建议这样做,除非您非常严格 https://stackoverflow.com/a/21629575 只允许您信任的网站将您的网站嵌入 iframe 中。
+因为如果您允许记录跨源 iframe,任何恶意网站都可以嵌入您的网站,并且只要它们运行 rrweb,它们就可以记录您网站的所有内容。
+
+## 如何记录跨源 iframe
+
+在父页面中启用录制跨域 iframe:
+
+```js
+rrweb.record({
+ emit(event) {}, // 所有事件都将在此处发出,包括来自跨源 iframe 的事件
+ recordCrossOriginIframes: true,
+});
+```
+
+在您的子页面中启用重放跨域 iframe:
+
+```js
+rrweb.record({
+ emit(event) {}, // 这是 rrweb 所必需的,但子页面不会发出任何事件
+ recordCrossOriginIframes: true,
+});
+```
+
+## 注意事项
+
+当跨源 iframe 录制打开时,rrweb 将检查它是否正在顶级窗口中运行。
+如果不是,它将通过 `postMessage` 将事件发送到父窗口。
+
+如果您没有在顶层窗口中运行 rrweb,则打开 `recordCrossOriginIframes` 时事件将丢失。
+
+如果顶层窗口是一个恶意网站,它可以监听事件并将它们发送到它选择的服务器。
+
+或者,如果您的页面上正在运行恶意脚本,他们可以监听“postMessage”,并且子窗口和父窗口之间的通信未加密。 他们可以看到事件。
+
+## 将 rrweb 注入跨源 iframe 的选项
+
+### 1. 网站所有者,在 iframe 中添加 rrweb
+
+如果您拥有使用 iframe 的网站和嵌入在 iframe 中的网站,您可以通过脚本标签将 rrweb 添加到这两个页面。
+
+### 2. 浏览器扩展
+
+See https://developer.chrome.com/docs/extensions/mv3/content_scripts/#functionality
+
+### 3. Puppeteer script
+
+```js
+import puppeteer from 'puppeteer';
+
+async function injectRecording(frame) {
+ await frame.evaluate((rrwebCode) => {
+ if (window.__IS_RECORDING__) return;
+ window.__IS_RECORDING__ = true;
+
+ (async () => {
+ function loadScript(code) {
+ const s = document.createElement('script');
+ s.type = 'text/javascript';
+ s.innerHTML = code;
+ if (document.head) {
+ document.head.append(s);
+ } else {
+ requestAnimationFrame(() => {
+ document.head.append(s);
+ });
+ }
+ }
+ loadScript(rrwebCode);
+
+ window.rrweb.record({
+ emit: (event) => {
+ window._captureEvent(event);
+ },
+ recordCrossOriginIframes: true,
+ });
+ })();
+ }, code);
+}
+
+const browser = await puppeteer.launch();
+const page = (await browser.pages())[0];
+
+const events = []; // 包含来自所有帧的所有事件
+
+await page.exposeFunction('_captureEvent', (event) => {
+ events.push(event);
+});
+
+page.on('framenavigated', async (frame) => {
+ await injectRecording(frame); // 将 rrweb 注入 iframe
+});
+
+await page.goto('https://example.com');
+
+// 您的事件在事件数组中
+```
+
+### 4. Electron
+
+```ts
+const win = new BrowserWindow({
+ width: 800,
+ height: 600,
+ webPreferences: {
+ preload: path.join(__dirname, 'rrweb-recording-script.js'),
+ // 这会打开 iframe 内的预加载,但会禁用节点集成
+ nodeIntegrationInSubFrames: true,
+ nodeIntegration: false,
+ },
+});
+```
+
+See https://www.electronjs.org/docs/latest/tutorial/tutorial-preload
+And https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
diff --git a/docs/recipes/dive-into-event.md b/docs/recipes/dive-into-event.md
index 4df9152c19..be63972a68 100644
--- a/docs/recipes/dive-into-event.md
+++ b/docs/recipes/dive-into-event.md
@@ -67,4 +67,4 @@ source -> IncrementalSource.Font
data -> fontData
```
-enum IncrementalSource's definition can be found in this [list](https://github.com/rrweb-io/rrweb/blob/master/packages/rrweb/typings/types.d.ts#L62).
+enum IncrementalSource's definition can be found in this [list](https://github.com/rrweb-io/rrweb/blob/98e71cd0d23628cd1fbdbe47664a65748084c4a4/packages/types/src/index.ts#L69).
diff --git a/guide.md b/guide.md
index 206fa053ab..76f781d096 100644
--- a/guide.md
+++ b/guide.md
@@ -135,32 +135,33 @@ setInterval(save, 10 * 1000);
The parameter of `rrweb.record` accepts the following options.
-| key | default | description |
-| -------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| emit | required | the callback function to get emitted events |
-| checkoutEveryNth | - | take a full snapshot after every N events
refer to the [checkout](#checkout) chapter |
-| checkoutEveryNms | - | take a full snapshot after every N ms
refer to the [checkout](#checkout) chapter |
-| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter |
-| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter |
-| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter |
-| ignoreCSSAttributes | null | array of CSS attributes that should be ignored |
-| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter |
-| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter |
-| maskAllInputs | false | mask all input content as \* |
-| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
-| maskInputFn | - | customize mask input content recording logic |
-| maskTextFn | - | customize mask text content recording logic |
-| slimDOMOptions | {} | remove unnecessary parts of the DOM
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
-| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data |
-| inlineStylesheet | true | whether to inline the stylesheet in the events |
-| hooks | {} | hooks for events
refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
-| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
-| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
-| recordCanvas | false | Whether to record the canvas element. Available options:
`false`,
`true` |
-| inlineImages | false | whether to record the image content |
-| collectFonts | false | whether to collect fonts in the website |
-| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) |
-| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) |
+| key | default | description |
+| ------------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| emit | required | the callback function to get emitted events |
+| checkoutEveryNth | - | take a full snapshot after every N events
refer to the [checkout](#checkout) chapter |
+| checkoutEveryNms | - | take a full snapshot after every N ms
refer to the [checkout](#checkout) chapter |
+| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter |
+| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter |
+| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter |
+| ignoreCSSAttributes | null | array of CSS attributes that should be ignored |
+| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter |
+| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter |
+| maskAllInputs | false | mask all input content as \* |
+| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
+| maskInputFn | - | customize mask input content recording logic |
+| maskTextFn | - | customize mask text content recording logic |
+| slimDOMOptions | {} | remove unnecessary parts of the DOM
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
+| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data |
+| inlineStylesheet | true | whether to inline the stylesheet in the events |
+| hooks | {} | hooks for events
refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
+| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
+| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
+| recordCanvas | false | Whether to record the canvas element. Available options:
`false`,
`true` |
+| recordCrossOriginIframes | false | Whether to record cross origin iframes. rrweb has to be injected in each child iframe for this to work. Available options:
`false`,
`true` |
+| inlineImages | false | whether to record the image content |
+| collectFonts | false | whether to collect fonts in the website |
+| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) |
+| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) |
#### Privacy
diff --git a/guide.zh_CN.md b/guide.zh_CN.md
index e867c93de6..1093dbb386 100644
--- a/guide.zh_CN.md
+++ b/guide.zh_CN.md
@@ -131,32 +131,33 @@ setInterval(save, 10 * 1000);
`rrweb.record(config)` 的 config 部分接受以下参数
-| key | 默认值 | 功能 |
-| -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| emit | 必填 | 获取当前录制的数据 |
-| checkoutEveryNth | - | 每 N 次事件重新制作一次全量快照
详见[“重新制作快照”](#重新制作快照)章节 |
-| checkoutEveryNms | - | 每 N 毫秒重新制作一次全量快照
详见[“重新制作快照”](#重新制作快照)章节 |
-| blockClass | 'rr-block' | 字符串或正则表达式,可用于自定义屏蔽元素的类名,详见[“隐私”](#隐私)章节 |
-| blockSelector | null | 所有 element.matches(blockSelector)为 true 的元素都不会被录制,回放时取而代之的是一个同等宽高的占位元素 |
-| ignoreClass | 'rr-ignore' | 字符串或正则表达式,可用于自定义忽略元素的类名,详见[“隐私”](#隐私)章节 |
-| ignoreCSSAttributes | null | 应该被忽略的 CSS 属性数组 |
-| maskTextClass | 'rr-mask' | 字符串或正则表达式,可用于自定义忽略元素 text 内容的类名,详见[“隐私”](#隐私)章节 |
-| maskTextSelector | null | 所有 element.matches(maskTextSelector)为 true 的元素及其子元素的 text 内容将会被屏蔽 |
-| maskAllInputs | false | 将所有输入内容记录为 \* |
-| maskInputOptions | { password: true } | 选择将特定类型的输入框内容记录为 \*
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
-| maskInputFn | - | 自定义特定类型的输入框内容记录逻辑 |
-| maskTextFn | - | 自定义文字内容的记录逻辑 |
-| slimDOMOptions | {} | 去除 DOM 中不必要的部分
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
-| inlineStylesheet | true | 是否将样式表内联 |
-| hooks | {} | 各类事件的回调
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
-| packFn | - | 数据压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
-| sampling | - | 数据抽样策略,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
-| dataURLOptions | {} | Canvas 图像快照的格式和质量,这个参数将传递给 OffscreenCanvas.convertToBlob(),使用这个参数能有效减小录制数据的大小 |
-| recordCanvas | false | 是否记录 canvas 内容, 可用选项:false, true |
-| inlineImages | false | 是否将图片内容记内联录制 |
-| collectFonts | false | 是否记录页面中的字体文件 |
-| userTriggeredOnInput | false | [什么是 `userTriggered`](https://github.com/rrweb-io/rrweb/pull/495) |
-| plugins | [] | 加载插件以获得额外的录制功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) |
+| key | 默认值 | 功能 |
+| ------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| emit | 必填 | 获取当前录制的数据 |
+| checkoutEveryNth | - | 每 N 次事件重新制作一次全量快照
详见[“重新制作快照”](#重新制作快照)章节 |
+| checkoutEveryNms | - | 每 N 毫秒重新制作一次全量快照
详见[“重新制作快照”](#重新制作快照)章节 |
+| blockClass | 'rr-block' | 字符串或正则表达式,可用于自定义屏蔽元素的类名,详见[“隐私”](#隐私)章节 |
+| blockSelector | null | 所有 element.matches(blockSelector)为 true 的元素都不会被录制,回放时取而代之的是一个同等宽高的占位元素 |
+| ignoreClass | 'rr-ignore' | 字符串或正则表达式,可用于自定义忽略元素的类名,详见[“隐私”](#隐私)章节 |
+| ignoreCSSAttributes | null | 应该被忽略的 CSS 属性数组 |
+| maskTextClass | 'rr-mask' | 字符串或正则表达式,可用于自定义忽略元素 text 内容的类名,详见[“隐私”](#隐私)章节 |
+| maskTextSelector | null | 所有 element.matches(maskTextSelector)为 true 的元素及其子元素的 text 内容将会被屏蔽 |
+| maskAllInputs | false | 将所有输入内容记录为 \* |
+| maskInputOptions | { password: true } | 选择将特定类型的输入框内容记录为 \*
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
+| maskInputFn | - | 自定义特定类型的输入框内容记录逻辑 |
+| maskTextFn | - | 自定义文字内容的记录逻辑 |
+| slimDOMOptions | {} | 去除 DOM 中不必要的部分
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
+| inlineStylesheet | true | 是否将样式表内联 |
+| hooks | {} | 各类事件的回调
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
+| packFn | - | 数据压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
+| sampling | - | 数据抽样策略,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
+| dataURLOptions | {} | Canvas 图像快照的格式和质量,这个参数将传递给 OffscreenCanvas.convertToBlob(),使用这个参数能有效减小录制数据的大小 |
+| recordCanvas | false | 是否记录 canvas 内容, 可用选项:`false`, `true` |
+| recordCrossOriginIframes | false | 是否记录 cross origin iframes。 必须在每个子 iframe 中注入 rrweb 才能使其工作。 可用选项:`false`, `true` |
+| inlineImages | false | 是否将图片内容记内联录制 |
+| collectFonts | false | 是否记录页面中的字体文件 |
+| userTriggeredOnInput | false | [什么是 `userTriggered`](https://github.com/rrweb-io/rrweb/pull/495) |
+| plugins | [] | 加载插件以获得额外的录制功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) |
#### 隐私
diff --git a/package.json b/package.json
index 66ae51cc19..7192130275 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"concurrently": "^7.1.0",
"eslint": "^8.19.0",
"eslint-plugin-compat": "^4.0.2",
- "eslint-plugin-jest": "^26.5.3",
+ "eslint-plugin-jest": "^27.1.3",
"eslint-plugin-tsdoc": "^0.2.16",
"lerna": "^4.0.0",
"markdownlint": "^0.25.1",
diff --git a/packages/rrweb-player/src/utils.ts b/packages/rrweb-player/src/utils.ts
index 796d18106c..2a3f29219c 100644
--- a/packages/rrweb-player/src/utils.ts
+++ b/packages/rrweb-player/src/utils.ts
@@ -16,7 +16,7 @@ declare global {
}
import { EventType, IncrementalSource } from 'rrweb';
-import type { eventWithTime } from 'rrweb/typings/types';
+import type { eventWithTime } from '@rrweb/types';
export function inlineCss(cssObj: Record
-
- Verify that block class bugs are fixed
-
-
+
+ Verify that block class bugs are fixed
+
+
+
+
+
+
diff --git a/packages/rrweb/test/html/hello-world.html b/packages/rrweb/test/html/hello-world.html new file mode 100644 index 0000000000..04c1907d6a --- /dev/null +++ b/packages/rrweb/test/html/hello-world.html @@ -0,0 +1,12 @@ + + +
+ + + +
+ +
+