fix: wrong rootId value in special iframes (#1100)

1. When some same-origin iframes are nested in cross-origin iframes, their `rootId`s are wrong.

2. The property `rootId` is missing in serialized cross-origin iframes
This commit is contained in:
Yun Feng
2026-04-01 12:00:00 +08:00
committed by GitHub
parent d97c82c41d
commit d5ff6efc9a
5 changed files with 639 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
import { genId } from 'rrweb-snapshot';
import { genId, NodeType } from 'rrweb-snapshot';
import type { CrossOriginIframeMessageEvent } from '../types';
import CrossOriginIframeMirror from './cross-origin-iframe-mirror';
import { EventType, IncrementalSource } from '@rrweb/types';
@@ -14,6 +14,10 @@ export class IframeManager {
> = new WeakMap();
public crossOriginIframeMirror = new CrossOriginIframeMirror(genId);
public crossOriginIframeStyleMirror: CrossOriginIframeMirror;
public crossOriginIframeRootIdMap: WeakMap<
HTMLIFrameElement,
number
> = new WeakMap();
private mirror: Mirror;
private mutationCb: mutationCallBack;
private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void;
@@ -121,6 +125,9 @@ export class IframeManager {
* Replaces the original id of the iframe with a new set of unique ids
*/
this.replaceIdOnNode(e.data.node, iframeEl);
const rootId = e.data.node.id;
this.crossOriginIframeRootIdMap.set(iframeEl, rootId);
this.patchRootIdOnNode(e.data.node, rootId);
return {
timestamp: e.timestamp,
type: EventType.IncrementalSnapshot,
@@ -171,6 +178,8 @@ export class IframeManager {
'previousId',
]);
this.replaceIdOnNode(n.node, iframeEl);
const rootId = this.crossOriginIframeRootIdMap.get(iframeEl);
rootId && this.patchRootIdOnNode(n.node, rootId);
});
e.data.removes.forEach((n) => {
this.replaceIds(n, iframeEl, ['parentId', 'id']);
@@ -273,11 +282,20 @@ export class IframeManager {
node: serializedNodeWithId,
iframeEl: HTMLIFrameElement,
) {
this.replaceIds(node, iframeEl, ['id']);
this.replaceIds(node, iframeEl, ['id', 'rootId']);
if ('childNodes' in node) {
node.childNodes.forEach((child) => {
this.replaceIdOnNode(child, iframeEl);
});
}
}
private patchRootIdOnNode(node: serializedNodeWithId, rootId: number) {
if (node.type !== NodeType.Document && !node.rootId) node.rootId = rootId;
if ('childNodes' in node) {
node.childNodes.forEach((child) => {
this.patchRootIdOnNode(child, rootId);
});
}
}
}

View File

@@ -1,7 +1,8 @@
import * as fs from 'fs';
import * as path from 'path';
import type { Page } from 'puppeteer';
import type { eventWithTime, recordOptions } from '../../src/types';
import type { eventWithTime } from '@rrweb/types';
import type { recordOptions } from '../../src/types';
import { startServer, launchPuppeteer, ISuite, getServerURL } from '../utils';
const suites: Array<

View File

@@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import type { eventWithTime, recordOptions } from '../../src/types';
import type { eventWithTime } from '@rrweb/types';
import type { recordOptions } from '../../src/types';
import { launchPuppeteer, ISuite } from '../utils';
const suites: Array<{

View File

@@ -517,6 +517,24 @@ describe('cross origin iframes', function (this: ISuite) {
serverBURL: string;
};
it('should record same-origin iframe in cross-origin iframe', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.evaluate(() => {
const iframe2 = document.createElement('iframe');
// Append a same-origin iframe in a cross-origin iframe.
document.body.appendChild(iframe2);
iframe2.contentDocument!.body.appendChild(
document.createTextNode('Same-origin iframe in cross-origin iframe'),
);
});
await waitForRAF(ctx.page);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should filter out forwarded cross origin rrweb messages', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
const iframe2URL = `${ctx.serverBURL}/html/blank.html`;
@@ -533,10 +551,11 @@ describe('cross origin iframes', function (this: ISuite) {
// Wait for iframe2 to load
await ctx.page.waitForFrame(iframe2URL);
const iframe2 = frame.childFrames()[0];
// Record iframe2
await injectRecordScript(frame.childFrames()[0]);
await injectRecordScript(iframe2);
await waitForRAF(frame.childFrames()[0]);
await waitForRAF(iframe2);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];