Impl record iframe (#481)

* Impl record iframe

* iframe observe

* temp: add bundle file to git

* update bundle

* update with pick

* update bundle

* fix fragment map remove

* feat: add an option to determine whether to pause CSS animation when playback is paused (#428)

set pauseAnimation to true by default

* fix: elements would lose some states like scroll position because of "virtual parent" optimization (#427)

* fix: elements would lose some state like scroll position because of "virtual parent" optimization

* refactor: the bugfix code

bug: elements would lose some state like scroll position because of "virtual parent" optimization

* fix: an error occured at applyMutation(remove nodes part)

error message:
Uncaught (in promise) DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node

* pick fixes

* revert ignore file

* re-impl iframe record

* re-impl iframe replay

* code housekeeping

* move multi layer dimension calculation to replay side

* update test cases

* teardown test server

* upgrade rrweb-snapshot with iframe load timeout

Co-authored-by: Lucky Feng <yun.feng@smartx.com>
This commit is contained in:
yz-yu
2026-04-01 12:00:00 +08:00
committed by GitHub
parent b99e843e2a
commit 33f0ac5cfe
24 changed files with 1414 additions and 246 deletions

View File

@@ -43,6 +43,7 @@ import {
} from '../types';
import MutationBuffer from './mutation';
import { stringify } from './stringify';
import { IframeManager } from './iframe-manager';
type WindowWithStoredMutationObserver = Window & {
__rrMutationObserver?: MutationObserver;
@@ -53,17 +54,21 @@ type WindowWithAngularZone = Window & {
};
};
export const mutationBuffer = new MutationBuffer();
export const mutationBuffers: MutationBuffer[] = [];
function initMutationObserver(
cb: mutationCallBack,
doc: Document,
blockClass: blockClass,
blockSelector: string | null,
inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions,
recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions,
iframeManager: IframeManager,
): MutationObserver {
const mutationBuffer = new MutationBuffer();
mutationBuffers.push(mutationBuffer);
// see mutation.ts for details
mutationBuffer.init(
cb,
@@ -73,8 +78,10 @@ function initMutationObserver(
maskInputOptions,
recordCanvas,
slimDOMOptions,
doc,
iframeManager,
);
let mutationBufferCtor =
let mutationObserverCtor =
window.MutationObserver ||
/**
* Some websites may disable MutationObserver by removing it from the window object.
@@ -94,15 +101,15 @@ function initMutationObserver(
angularZoneSymbol
]
) {
mutationBufferCtor = ((window as unknown) as Record<
mutationObserverCtor = ((window as unknown) as Record<
string,
typeof MutationObserver
>)[angularZoneSymbol];
}
const observer = new mutationBufferCtor(
const observer = new mutationObserverCtor(
mutationBuffer.processMutations.bind(mutationBuffer),
);
observer.observe(document, {
observer.observe(doc, {
attributes: true,
attributeOldValue: true,
characterData: true,
@@ -116,6 +123,7 @@ function initMutationObserver(
function initMoveObserver(
cb: mousemoveCallBack,
sampling: SamplingStrategy,
doc: Document,
): listenerHandler {
if (sampling.mousemove === false) {
return () => {};
@@ -161,8 +169,8 @@ function initMoveObserver(
},
);
const handlers = [
on('mousemove', updatePosition),
on('touchmove', updatePosition),
on('mousemove', updatePosition, doc),
on('touchmove', updatePosition, doc),
];
return () => {
handlers.forEach((h) => h());
@@ -171,6 +179,7 @@ function initMoveObserver(
function initMouseInteractionObserver(
cb: mouseInteractionCallBack,
doc: Document,
blockClass: blockClass,
sampling: SamplingStrategy,
): listenerHandler {
@@ -211,7 +220,7 @@ function initMouseInteractionObserver(
.forEach((eventKey: keyof typeof MouseInteractions) => {
const eventName = eventKey.toLowerCase();
const handler = getHandler(eventKey);
handlers.push(on(eventName, handler));
handlers.push(on(eventName, handler, doc));
});
return () => {
handlers.forEach((h) => h());
@@ -220,6 +229,7 @@ function initMouseInteractionObserver(
function initScrollObserver(
cb: scrollCallback,
doc: Document,
blockClass: blockClass,
sampling: SamplingStrategy,
): listenerHandler {
@@ -228,8 +238,8 @@ function initScrollObserver(
return;
}
const id = mirror.getId(evt.target as INode);
if (evt.target === document) {
const scrollEl = (document.scrollingElement || document.documentElement)!;
if (evt.target === doc) {
const scrollEl = (doc.scrollingElement || doc.documentElement)!;
cb({
id,
x: scrollEl.scrollLeft,
@@ -270,6 +280,7 @@ export const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
const lastInputValueMap: WeakMap<EventTarget, inputValue> = new WeakMap();
function initInputObserver(
cb: inputCallback,
doc: Document,
blockClass: blockClass,
ignoreClass: string,
maskInputOptions: MaskInputOptions,
@@ -314,7 +325,7 @@ function initInputObserver(
// the other radios with the same name attribute will be unchecked.
const name: string | undefined = (target as HTMLInputElement).name;
if (type === 'radio' && name && isChecked) {
document
doc
.querySelectorAll(`input[type="radio"][name="${name}"]`)
.forEach((el) => {
if (el !== target) {
@@ -344,7 +355,7 @@ function initInputObserver(
const events = sampling.input === 'last' ? ['change'] : ['input', 'change'];
const handlers: Array<
listenerHandler | hookResetter
> = events.map((eventName) => on(eventName, eventHandler));
> = events.map((eventName) => on(eventName, eventHandler, doc));
const propertyDescriptor = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
@@ -727,27 +738,32 @@ export function initObservers(
mergeHooks(o, hooks);
const mutationObserver = initMutationObserver(
o.mutationCb,
o.doc,
o.blockClass,
o.blockSelector,
o.inlineStylesheet,
o.maskInputOptions,
o.recordCanvas,
o.slimDOMOptions,
o.iframeManager,
);
const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling);
const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling, o.doc);
const mouseInteractionHandler = initMouseInteractionObserver(
o.mouseInteractionCb,
o.doc,
o.blockClass,
o.sampling,
);
const scrollHandler = initScrollObserver(
o.scrollCb,
o.doc,
o.blockClass,
o.sampling,
);
const viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb);
const inputHandler = initInputObserver(
o.inputCb,
o.doc,
o.blockClass,
o.ignoreClass,
o.maskInputOptions,