Fix cross origin iframe bugs (#1093)

* fix: error data while recording some websites are integrated  with stripe

These websites will usually have an iframe with src "https://js.stripe.com/v3/m-outer-xxx.html"

* add test case for the bug

* fix: recordCrossOriginIframes: true does not work with pack/unpack fn

1. bugfix
2. add test case
3. add rrweb-all.js to the result of command: bundle:browser
4. make puppeteer headless in CI by default to increase the speed and stability
This commit is contained in:
Yun Feng
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 529b2ad266
commit f2888d8694
8 changed files with 540 additions and 34 deletions

View File

@@ -307,6 +307,418 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`]
]"
`;
exports[`cross origin iframes blank.html should filter out forwarded cross origin rrweb 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\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {
\\"type\\": \\"\\"
},
\\"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
}
}
},
{
\\"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\\": \\"\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 15
}
],
\\"id\\": 14
}
],
\\"id\\": 13
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n\\",
\\"id\\": 17
}
],
\\"id\\": 16
}
],
\\"id\\": 12
}
],
\\"compatMode\\": \\"BackCompat\\",
\\"id\\": 11
}
}
],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [],
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 16,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"iframe\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 18
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [
{
\\"parentId\\": 18,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {
\\"type\\": \\"\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 23
}
],
\\"id\\": 22
}
],
\\"id\\": 21
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n\\",
\\"id\\": 25
}
],
\\"id\\": 24
}
],
\\"id\\": 20
}
],
\\"compatMode\\": \\"BackCompat\\",
\\"id\\": 19
}
}
],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [],
\\"isAttachIframe\\": true
}
}
]"
`;
exports[`cross origin iframes 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\\": \\"\\"
},
\\"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\\": \\"\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 15
}
],
\\"id\\": 14
}
],
\\"id\\": 13
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n\\",
\\"id\\": 17
}
],
\\"id\\": 16
}
],
\\"id\\": 12
}
],
\\"compatMode\\": \\"BackCompat\\",
\\"id\\": 11
}
}
],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [],
\\"isAttachIframe\\": true
},
\\"v\\": \\"v1\\"
}
]"
`;
exports[`cross origin iframes form.html should map input events correctly 1`] = `
"[
{

View File

@@ -13,9 +13,9 @@ import {
getServerURL,
launchPuppeteer,
startServer,
stripBase64,
waitForRAF,
} from '../utils';
import { unpack } from '../../src/packer/unpack';
import type * as http from 'http';
interface ISuite {
@@ -33,42 +33,61 @@ interface IWindow extends Window {
options: recordOptions<eventWithTime>,
) => listenerHandler | undefined;
addCustomEvent<T>(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) {
async function injectRecordScript(
frame: puppeteer.Frame,
options?: ExtraOptions,
) {
await frame.addScriptTag({
path: path.resolve(__dirname, '../../dist/rrweb.js'),
path: path.resolve(__dirname, '../../dist/rrweb-all.js'),
});
await frame.evaluate(() => {
options = options || {};
await frame.evaluate((options) => {
((window as unknown) as IWindow).snapshots = [];
const { record } = ((window as unknown) as IWindow).rrweb;
record({
const { record, pack } = ((window as unknown) as IWindow).rrweb;
const config: recordOptions<eventWithTime> = {
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);
await injectRecordScript(child, options);
}
}
const setup = function (this: ISuite, content: string): ISuite {
const ctx = {} as ISuite;
const setup = function (
this: ISuite,
content: string,
options?: ExtraOptions,
): ISuite {
const ctx = {} as ISuite & {
serverB: http.Server;
serverBURL: string;
};
beforeAll(async () => {
ctx.browser = await launchPuppeteer();
ctx.server = await startServer();
ctx.serverURL = getServerURL(ctx.server);
// ctx.serverB = await startServer();
// ctx.serverBURL = getServerURL(ctx.serverB);
ctx.serverB = await startServer();
ctx.serverBURL = getServerURL(ctx.serverB);
const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js');
ctx.code = fs.readFileSync(bundlePath, 'utf8');
@@ -90,7 +109,7 @@ const setup = function (this: ISuite, content: string): ISuite {
});
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
await injectRecordScript(ctx.page.mainFrame());
await injectRecordScript(ctx.page.mainFrame(), options);
});
afterEach(async () => {
@@ -100,7 +119,7 @@ const setup = function (this: ISuite, content: string): ISuite {
afterAll(async () => {
await ctx.browser.close();
ctx.server.close();
// ctx.serverB.close();
ctx.serverB.close();
});
return ctx;
@@ -484,6 +503,61 @@ describe('cross origin iframes', function (this: ISuite) {
assertSnapshot(snapshots);
});
});
describe('blank.html', function (this: ISuite) {
const content = `
<!DOCTYPE html>
<html>
<body>
<iframe src="{SERVER_URL}/html/blank.html"></iframe>
</body>
</html>
`;
const ctx = setup.call(this, content) as ISuite & {
serverBURL: string;
};
it('should filter out forwarded cross origin rrweb messages', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
const iframe2URL = `${ctx.serverBURL}/html/blank.html`;
await frame.evaluate((iframe2URL) => {
// Add a message proxy to forward messages from child frames to its parent frame.
window.addEventListener('message', (event) => {
if (event.source !== window)
window.parent.postMessage(event.data, '*');
});
const iframe2 = document.createElement('iframe');
iframe2.src = iframe2URL;
document.body.appendChild(iframe2);
}, iframe2URL);
// Wait for iframe2 to load
await ctx.page.waitForFrame(iframe2URL);
// Record iframe2
await injectRecordScript(frame.childFrames()[0]);
await waitForRAF(frame.childFrames()[0]);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
describe('should support packFn option in record()', () => {
const ctx = setup.call(this, content, { usePackFn: true });
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);
});
});
});
});
describe('same origin iframes', function (this: ISuite) {

View File

@@ -576,8 +576,10 @@ export const polyfillWebGLGlobals = () => {
global.WebGL2RenderingContext = WebGL2RenderingContext as any;
};
export async function waitForRAF(page: puppeteer.Page) {
return await page.evaluate(() => {
export async function waitForRAF(
pageOrFrame: puppeteer.Page | puppeteer.Frame,
) {
return await pageOrFrame.evaluate(() => {
return new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve);