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:
36
src/record/iframe-manager.ts
Normal file
36
src/record/iframe-manager.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { serializedNodeWithId, INode } from 'rrweb-snapshot';
|
||||
import { mutationCallBack } from '../types';
|
||||
|
||||
export class IframeManager {
|
||||
private iframes: WeakMap<HTMLIFrameElement, true> = new WeakMap();
|
||||
private mutationCb: mutationCallBack;
|
||||
private loadListener?: (iframeEl: HTMLIFrameElement) => unknown;
|
||||
|
||||
constructor(options: { mutationCb: mutationCallBack }) {
|
||||
this.mutationCb = options.mutationCb;
|
||||
}
|
||||
|
||||
public addIframe(iframeEl: HTMLIFrameElement) {
|
||||
this.iframes.set(iframeEl, true);
|
||||
}
|
||||
|
||||
public addLoadListener(cb: (iframeEl: HTMLIFrameElement) => unknown) {
|
||||
this.loadListener = cb;
|
||||
}
|
||||
|
||||
public attachIframe(iframeEl: INode, childSn: serializedNodeWithId) {
|
||||
this.mutationCb({
|
||||
adds: [
|
||||
{
|
||||
parentId: iframeEl.__sn.id,
|
||||
nextId: null,
|
||||
node: childSn,
|
||||
},
|
||||
],
|
||||
removes: [],
|
||||
texts: [],
|
||||
attributes: [],
|
||||
});
|
||||
this.loadListener?.((iframeEl as unknown) as HTMLIFrameElement);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { snapshot, MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
|
||||
import { initObservers, mutationBuffer } from './observer';
|
||||
import { initObservers, mutationBuffers } from './observer';
|
||||
import {
|
||||
mirror,
|
||||
on,
|
||||
getWindowWidth,
|
||||
getWindowHeight,
|
||||
polyfill,
|
||||
isIframeINode,
|
||||
} from '../utils';
|
||||
import {
|
||||
EventType,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
listenerHandler,
|
||||
LogRecordOptions,
|
||||
} from '../types';
|
||||
import { IframeManager } from './iframe-manager';
|
||||
|
||||
function wrapEvent(e: event): eventWithTime {
|
||||
return {
|
||||
@@ -138,7 +140,7 @@ function record<T = eventWithTime>(
|
||||
let incrementalSnapshotCount = 0;
|
||||
wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => {
|
||||
if (
|
||||
mutationBuffer.isFrozen() &&
|
||||
mutationBuffers[0]?.isFrozen() &&
|
||||
e.type !== EventType.FullSnapshot &&
|
||||
!(
|
||||
e.type === EventType.IncrementalSnapshot &&
|
||||
@@ -147,7 +149,7 @@ function record<T = eventWithTime>(
|
||||
) {
|
||||
// we've got a user initiated event so first we need to apply
|
||||
// all DOM changes that have been buffering during paused state
|
||||
mutationBuffer.unfreeze();
|
||||
mutationBuffers.forEach((buf) => buf.unfreeze());
|
||||
}
|
||||
|
||||
emit(((packFn ? packFn(e) : e) as unknown) as T, isCheckout);
|
||||
@@ -167,6 +169,19 @@ function record<T = eventWithTime>(
|
||||
}
|
||||
};
|
||||
|
||||
const iframeManager = new IframeManager({
|
||||
mutationCb: (m) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
...m,
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
function takeFullSnapshot(isCheckout = false) {
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
@@ -180,8 +195,7 @@ function record<T = eventWithTime>(
|
||||
isCheckout,
|
||||
);
|
||||
|
||||
let wasFrozen = mutationBuffer.isFrozen();
|
||||
mutationBuffer.lock(); // don't allow any mirror modifications during snapshotting
|
||||
mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting
|
||||
const [node, idNodeMap] = snapshot(document, {
|
||||
blockClass,
|
||||
blockSelector,
|
||||
@@ -189,6 +203,14 @@ function record<T = eventWithTime>(
|
||||
maskAllInputs: maskInputOptions,
|
||||
slimDOM: slimDOMOptions,
|
||||
recordCanvas,
|
||||
onSerialize: (n) => {
|
||||
if (isIframeINode(n)) {
|
||||
iframeManager.addIframe(n);
|
||||
}
|
||||
},
|
||||
onIframeLoad: (iframe, childSn) => {
|
||||
iframeManager.attachIframe(iframe, childSn);
|
||||
},
|
||||
});
|
||||
|
||||
if (!node) {
|
||||
@@ -220,7 +242,7 @@ function record<T = eventWithTime>(
|
||||
},
|
||||
}),
|
||||
);
|
||||
mutationBuffer.unlock(); // generate & emit any mutations that happened during snapshotting, as can now apply against the newly built mirror
|
||||
mutationBuffers.forEach((buf) => buf.unlock()); // generate & emit any mutations that happened during snapshotting, as can now apply against the newly built mirror
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -235,137 +257,145 @@ function record<T = eventWithTime>(
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const observe = (doc: Document) => {
|
||||
return initObservers(
|
||||
{
|
||||
mutationCb: (m) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
...m,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mousemoveCb: (positions, source) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source,
|
||||
positions,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mouseInteractionCb: (d) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.MouseInteraction,
|
||||
...d,
|
||||
},
|
||||
}),
|
||||
),
|
||||
scrollCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Scroll,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
viewportResizeCb: (d) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.ViewportResize,
|
||||
...d,
|
||||
},
|
||||
}),
|
||||
),
|
||||
inputCb: (v) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Input,
|
||||
...v,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mediaInteractionCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.MediaInteraction,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
styleSheetRuleCb: (r) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
...r,
|
||||
},
|
||||
}),
|
||||
),
|
||||
canvasMutationCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.CanvasMutation,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
fontCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Font,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
logCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Log,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
blockClass,
|
||||
ignoreClass,
|
||||
maskInputOptions,
|
||||
inlineStylesheet,
|
||||
sampling,
|
||||
recordCanvas,
|
||||
collectFonts,
|
||||
doc,
|
||||
maskInputFn,
|
||||
logOptions,
|
||||
blockSelector,
|
||||
slimDOMOptions,
|
||||
iframeManager,
|
||||
},
|
||||
hooks,
|
||||
);
|
||||
};
|
||||
|
||||
iframeManager.addLoadListener((iframeEl) => {
|
||||
handlers.push(observe(iframeEl.contentDocument!));
|
||||
});
|
||||
|
||||
const init = () => {
|
||||
takeFullSnapshot();
|
||||
|
||||
handlers.push(
|
||||
initObservers(
|
||||
{
|
||||
mutationCb: (m) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
...m,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mousemoveCb: (positions, source) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source,
|
||||
positions,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mouseInteractionCb: (d) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.MouseInteraction,
|
||||
...d,
|
||||
},
|
||||
}),
|
||||
),
|
||||
scrollCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Scroll,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
viewportResizeCb: (d) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.ViewportResize,
|
||||
...d,
|
||||
},
|
||||
}),
|
||||
),
|
||||
inputCb: (v) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Input,
|
||||
...v,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mediaInteractionCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.MediaInteraction,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
styleSheetRuleCb: (r) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
...r,
|
||||
},
|
||||
}),
|
||||
),
|
||||
canvasMutationCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.CanvasMutation,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
fontCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Font,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
logCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Log,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
blockClass,
|
||||
blockSelector,
|
||||
ignoreClass,
|
||||
maskInputOptions,
|
||||
maskInputFn,
|
||||
inlineStylesheet,
|
||||
sampling,
|
||||
recordCanvas,
|
||||
collectFonts,
|
||||
slimDOMOptions,
|
||||
logOptions,
|
||||
},
|
||||
hooks,
|
||||
),
|
||||
);
|
||||
handlers.push(observe(document));
|
||||
};
|
||||
if (
|
||||
document.readyState === 'interactive' ||
|
||||
@@ -414,7 +444,7 @@ record.addCustomEvent = <T>(tag: string, payload: T) => {
|
||||
};
|
||||
|
||||
record.freezePage = () => {
|
||||
mutationBuffer.freeze();
|
||||
mutationBuffers.forEach((buf) => buf.freeze());
|
||||
};
|
||||
|
||||
export default record;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
MaskInputOptions,
|
||||
SlimDOMOptions,
|
||||
IGNORED_NODE,
|
||||
NodeType,
|
||||
} from 'rrweb-snapshot';
|
||||
import {
|
||||
mutationRecord,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
addedNodeMutation,
|
||||
} from '../types';
|
||||
import { mirror, isBlocked, isAncestorRemoved, isIgnored } from '../utils';
|
||||
import { IframeManager } from './iframe-manager';
|
||||
|
||||
type DoubleLinkedListNode = {
|
||||
previous: DoubleLinkedListNode | null;
|
||||
@@ -149,6 +151,9 @@ export default class MutationBuffer {
|
||||
private maskInputOptions: MaskInputOptions;
|
||||
private recordCanvas: boolean;
|
||||
private slimDOMOptions: SlimDOMOptions;
|
||||
private doc: Document;
|
||||
|
||||
private iframeManager: IframeManager;
|
||||
|
||||
public init(
|
||||
cb: mutationCallBack,
|
||||
@@ -158,6 +163,8 @@ export default class MutationBuffer {
|
||||
maskInputOptions: MaskInputOptions,
|
||||
recordCanvas: boolean,
|
||||
slimDOMOptions: SlimDOMOptions,
|
||||
doc: Document,
|
||||
iframeManager: IframeManager,
|
||||
) {
|
||||
this.blockClass = blockClass;
|
||||
this.blockSelector = blockSelector;
|
||||
@@ -166,6 +173,8 @@ export default class MutationBuffer {
|
||||
this.recordCanvas = recordCanvas;
|
||||
this.slimDOMOptions = slimDOMOptions;
|
||||
this.emissionCallback = cb;
|
||||
this.doc = doc;
|
||||
this.iframeManager = iframeManager;
|
||||
}
|
||||
|
||||
public freeze() {
|
||||
@@ -223,7 +232,7 @@ export default class MutationBuffer {
|
||||
return nextId;
|
||||
};
|
||||
const pushAdd = (n: Node) => {
|
||||
if (!n.parentNode || !document.contains(n)) {
|
||||
if (!n.parentNode || !this.doc.contains(n)) {
|
||||
return;
|
||||
}
|
||||
const parentId = mirror.getId((n.parentNode as Node) as INode);
|
||||
@@ -232,7 +241,7 @@ export default class MutationBuffer {
|
||||
return addList.addNode(n);
|
||||
}
|
||||
let sn = serializeNodeWithId(n, {
|
||||
doc: document,
|
||||
doc: this.doc,
|
||||
map: mirror.map,
|
||||
blockClass: this.blockClass,
|
||||
blockSelector: this.blockSelector,
|
||||
@@ -241,6 +250,19 @@ export default class MutationBuffer {
|
||||
maskInputOptions: this.maskInputOptions,
|
||||
slimDOMOptions: this.slimDOMOptions,
|
||||
recordCanvas: this.recordCanvas,
|
||||
onSerialize: (currentN) => {
|
||||
if (
|
||||
currentN.__sn.type === NodeType.Element &&
|
||||
currentN.__sn.tagName === 'iframe'
|
||||
) {
|
||||
this.iframeManager.addIframe(
|
||||
(currentN as unknown) as HTMLIFrameElement,
|
||||
);
|
||||
}
|
||||
},
|
||||
onIframeLoad: (iframe, childSn) => {
|
||||
this.iframeManager.attachIframe(iframe, childSn);
|
||||
},
|
||||
});
|
||||
if (sn) {
|
||||
adds.push({
|
||||
@@ -391,7 +413,7 @@ export default class MutationBuffer {
|
||||
}
|
||||
// overwrite attribute if the mutations was triggered in same time
|
||||
item.attributes[m.attributeName!] = transformAttribute(
|
||||
document,
|
||||
this.doc,
|
||||
m.attributeName!,
|
||||
value!,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
MouseInteractions,
|
||||
playerConfig,
|
||||
playerMetaData,
|
||||
viewportResizeDimention,
|
||||
viewportResizeDimension,
|
||||
missingNodeMap,
|
||||
addedNodeMutation,
|
||||
missingNode,
|
||||
@@ -37,6 +37,9 @@ import {
|
||||
TreeIndex,
|
||||
queueToResolveTrees,
|
||||
iterateResolveTree,
|
||||
AppendedIframe,
|
||||
isIframeINode,
|
||||
getBaseDimension,
|
||||
} from '../utils';
|
||||
import getInjectStyleRules from './styles/inject-style';
|
||||
import './styles/style.css';
|
||||
@@ -49,6 +52,7 @@ const SKIP_TIME_INTERVAL = 5 * 1000;
|
||||
const mitt = (mittProxy as any).default || mittProxy;
|
||||
|
||||
const REPLAY_CONSOLE_PREFIX = '[replayer]';
|
||||
const SCROLL_ATTRIBUTE_NAME = '__rrweb_scroll__';
|
||||
|
||||
const defaultMouseTailConfig = {
|
||||
duration: 500,
|
||||
@@ -111,6 +115,8 @@ export class Replayer {
|
||||
|
||||
private imageMap: Map<eventWithTime, HTMLImageElement> = new Map();
|
||||
|
||||
private newDocumentQueue: addedNodeMutation[] = [];
|
||||
|
||||
constructor(
|
||||
events: Array<eventWithTime | string>,
|
||||
config?: Partial<playerConfig>,
|
||||
@@ -233,6 +239,10 @@ export class Replayer {
|
||||
}
|
||||
if (firstFullsnapshot) {
|
||||
setTimeout(() => {
|
||||
// when something has been played, there is no need to rebuild poster
|
||||
if (this.timer.timeOffset > 0) {
|
||||
return;
|
||||
}
|
||||
this.rebuildFullSnapshot(
|
||||
firstFullsnapshot as fullSnapshotEvent & { timestamp: number },
|
||||
);
|
||||
@@ -406,7 +416,7 @@ export class Replayer {
|
||||
}
|
||||
}
|
||||
|
||||
private handleResize(dimension: viewportResizeDimention) {
|
||||
private handleResize(dimension: viewportResizeDimension) {
|
||||
this.iframe.style.display = 'inherit';
|
||||
for (const el of [this.mouseTail, this.iframe]) {
|
||||
if (!el) {
|
||||
@@ -534,11 +544,44 @@ export class Replayer {
|
||||
);
|
||||
}
|
||||
this.legacy_missingNodeRetryMap = {};
|
||||
const collected: AppendedIframe[] = [];
|
||||
mirror.map = rebuild(event.data.node, {
|
||||
doc: this.iframe.contentDocument,
|
||||
afterAppend: (builtNode) => {
|
||||
this.collectIframeAndAttachDocument(collected, builtNode);
|
||||
},
|
||||
})[1];
|
||||
const styleEl = document.createElement('style');
|
||||
for (const { mutationInQueue, builtNode } of collected) {
|
||||
this.attachDocumentToIframe(mutationInQueue, builtNode);
|
||||
this.newDocumentQueue = this.newDocumentQueue.filter(
|
||||
(m) => m !== mutationInQueue,
|
||||
);
|
||||
if (builtNode.contentDocument) {
|
||||
const { documentElement, head } = builtNode.contentDocument;
|
||||
this.insertStyleRules(documentElement, head);
|
||||
}
|
||||
}
|
||||
const { documentElement, head } = this.iframe.contentDocument;
|
||||
this.insertStyleRules(documentElement, head);
|
||||
if (!this.service.state.matches('playing')) {
|
||||
this.iframe.contentDocument
|
||||
.getElementsByTagName('html')[0]
|
||||
.classList.add('rrweb-paused');
|
||||
}
|
||||
this.emitter.emit(ReplayerEvents.FullsnapshotRebuilded, event);
|
||||
if (!isSync) {
|
||||
this.waitForStylesheetLoad();
|
||||
}
|
||||
if (this.config.UNSAFE_replayCanvas) {
|
||||
this.preloadAllImages();
|
||||
}
|
||||
}
|
||||
|
||||
private insertStyleRules(
|
||||
documentElement: HTMLElement,
|
||||
head: HTMLHeadElement,
|
||||
) {
|
||||
const styleEl = document.createElement('style');
|
||||
documentElement!.insertBefore(styleEl, head);
|
||||
const injectStylesRules = getInjectStyleRules(
|
||||
this.config.blockClass,
|
||||
@@ -548,20 +591,48 @@ export class Replayer {
|
||||
'html.rrweb-paused * { animation-play-state: paused !important; }',
|
||||
);
|
||||
}
|
||||
if (!this.service.state.matches('playing')) {
|
||||
this.iframe.contentDocument
|
||||
.getElementsByTagName('html')[0]
|
||||
.classList.add('rrweb-paused');
|
||||
}
|
||||
for (let idx = 0; idx < injectStylesRules.length; idx++) {
|
||||
(styleEl.sheet! as CSSStyleSheet).insertRule(injectStylesRules[idx], idx);
|
||||
}
|
||||
this.emitter.emit(ReplayerEvents.FullsnapshotRebuilded, event);
|
||||
if (!isSync) {
|
||||
this.waitForStylesheetLoad();
|
||||
}
|
||||
|
||||
private attachDocumentToIframe(
|
||||
mutation: addedNodeMutation,
|
||||
iframeEl: HTMLIFrameElement,
|
||||
) {
|
||||
const collected: AppendedIframe[] = [];
|
||||
buildNodeWithSN(mutation.node, {
|
||||
doc: iframeEl.contentDocument!,
|
||||
map: mirror.map,
|
||||
hackCss: true,
|
||||
skipChild: false,
|
||||
afterAppend: (builtNode) => {
|
||||
this.collectIframeAndAttachDocument(collected, builtNode);
|
||||
},
|
||||
});
|
||||
for (const { mutationInQueue, builtNode } of collected) {
|
||||
this.attachDocumentToIframe(mutationInQueue, builtNode);
|
||||
this.newDocumentQueue = this.newDocumentQueue.filter(
|
||||
(m) => m !== mutationInQueue,
|
||||
);
|
||||
if (builtNode.contentDocument) {
|
||||
const { documentElement, head } = builtNode.contentDocument;
|
||||
this.insertStyleRules(documentElement, head);
|
||||
}
|
||||
}
|
||||
if (this.config.UNSAFE_replayCanvas) {
|
||||
this.preloadAllImages();
|
||||
}
|
||||
|
||||
private collectIframeAndAttachDocument(
|
||||
collected: AppendedIframe[],
|
||||
builtNode: INode,
|
||||
) {
|
||||
if (isIframeINode(builtNode)) {
|
||||
const mutationInQueue = this.newDocumentQueue.find(
|
||||
(m) => m.parentId === builtNode.__sn.id,
|
||||
);
|
||||
if (mutationInQueue) {
|
||||
collected.push({ mutationInQueue, builtNode });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,6 +1092,10 @@ export class Replayer {
|
||||
}
|
||||
let parent = mirror.getNode(mutation.parentId);
|
||||
if (!parent) {
|
||||
if (mutation.node.type === NodeType.Document) {
|
||||
// is newly added document, maybe the document node of an iframe
|
||||
return this.newDocumentQueue.push(mutation);
|
||||
}
|
||||
return queue.push(mutation);
|
||||
}
|
||||
|
||||
@@ -1059,12 +1134,23 @@ export class Replayer {
|
||||
return queue.push(mutation);
|
||||
}
|
||||
|
||||
if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetDoc = mutation.node.rootId
|
||||
? mirror.getNode(mutation.node.rootId)
|
||||
: this.iframe.contentDocument;
|
||||
if (isIframeINode(parent)) {
|
||||
this.attachDocumentToIframe(mutation, parent);
|
||||
return;
|
||||
}
|
||||
const target = buildNodeWithSN(mutation.node, {
|
||||
doc: this.iframe.contentDocument,
|
||||
doc: targetDoc as Document,
|
||||
map: mirror.map,
|
||||
skipChild: true,
|
||||
hackCss: true,
|
||||
}) as Node;
|
||||
}) as INode;
|
||||
|
||||
// legacy data, we should not have -1 siblings any more
|
||||
if (mutation.previousId === -1 || mutation.nextId === -1) {
|
||||
@@ -1087,6 +1173,22 @@ export class Replayer {
|
||||
parent.appendChild(target);
|
||||
}
|
||||
|
||||
if (isIframeINode(target)) {
|
||||
const mutationInQueue = this.newDocumentQueue.find(
|
||||
(m) => m.parentId === target.__sn.id,
|
||||
);
|
||||
if (mutationInQueue) {
|
||||
this.attachDocumentToIframe(mutationInQueue, target);
|
||||
this.newDocumentQueue = this.newDocumentQueue.filter(
|
||||
(m) => m !== mutationInQueue,
|
||||
);
|
||||
}
|
||||
if (target.contentDocument) {
|
||||
const { documentElement, head } = target.contentDocument;
|
||||
this.insertStyleRules(documentElement, head);
|
||||
}
|
||||
}
|
||||
|
||||
if (mutation.previousId || mutation.nextId) {
|
||||
this.legacy_resolveMissingNode(
|
||||
legacy_missingNodeMap,
|
||||
@@ -1228,7 +1330,7 @@ export class Replayer {
|
||||
* generate a console log replayer which implement the interface ReplayLogger
|
||||
*/
|
||||
private getConsoleLogger(): ReplayLogger {
|
||||
const rrwebOriginal = '__rrweb_original__';
|
||||
const rrwebOriginal = SCROLL_ATTRIBUTE_NAME;
|
||||
const replayLogger: ReplayLogger = {};
|
||||
for (const level of this.config.logConfig.level!)
|
||||
if (level === 'trace')
|
||||
@@ -1284,14 +1386,18 @@ export class Replayer {
|
||||
}
|
||||
|
||||
private moveAndHover(d: incrementalData, x: number, y: number, id: number) {
|
||||
this.mouse.style.left = `${x}px`;
|
||||
this.mouse.style.top = `${y}px`;
|
||||
this.drawMouseTail({ x, y });
|
||||
|
||||
const target = mirror.getNode(id);
|
||||
if (!target) {
|
||||
return this.debugNodeNotFound(d, id);
|
||||
}
|
||||
|
||||
const base = getBaseDimension(target);
|
||||
const _x = x + base.x;
|
||||
const _y = y + base.y;
|
||||
|
||||
this.mouse.style.left = `${_x}px`;
|
||||
this.mouse.style.top = `${_y}px`;
|
||||
this.drawMouseTail({ x: _x, y: _y });
|
||||
this.hoverElements((target as Node) as Element);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const rules: (blockClass: string) => string[] = (blockClass: string) => [
|
||||
`iframe, .${blockClass} { background: #ccc }`,
|
||||
`.${blockClass} { background: #ccc }`,
|
||||
'noscript { display: none !important; }',
|
||||
];
|
||||
|
||||
|
||||
14
src/types.ts
14
src/types.ts
@@ -7,6 +7,7 @@ import {
|
||||
} from 'rrweb-snapshot';
|
||||
import { PackFn, UnpackFn } from './packer/base';
|
||||
import { FontFaceDescriptors } from 'css-font-loading-module';
|
||||
import { IframeManager } from './record/iframe-manager';
|
||||
|
||||
export enum EventType {
|
||||
DomContentLoaded,
|
||||
@@ -101,7 +102,7 @@ export type scrollData = {
|
||||
|
||||
export type viewportResizeData = {
|
||||
source: IncrementalSource.ViewportResize;
|
||||
} & viewportResizeDimention;
|
||||
} & viewportResizeDimension;
|
||||
|
||||
export type inputData = {
|
||||
source: IncrementalSource.Input;
|
||||
@@ -224,6 +225,8 @@ export type observerParam = {
|
||||
recordCanvas: boolean;
|
||||
collectFonts: boolean;
|
||||
slimDOMOptions: SlimDOMOptions;
|
||||
doc: Document;
|
||||
iframeManager: IframeManager;
|
||||
};
|
||||
|
||||
export type hooksParam = {
|
||||
@@ -430,12 +433,12 @@ export type fontCallback = (p: fontParam) => void;
|
||||
|
||||
export type logCallback = (p: LogParam) => void;
|
||||
|
||||
export type viewportResizeDimention = {
|
||||
export type viewportResizeDimension = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type viewportResizeCallback = (d: viewportResizeDimention) => void;
|
||||
export type viewportResizeCallback = (d: viewportResizeDimension) => void;
|
||||
|
||||
export type inputValue = {
|
||||
text: string;
|
||||
@@ -456,6 +459,11 @@ export type mediaInteractionParam = {
|
||||
|
||||
export type mediaInteractionCallback = (p: mediaInteractionParam) => void;
|
||||
|
||||
export type DocumentDimension = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Mirror = {
|
||||
map: idNodeMap;
|
||||
getId: (n: INode) => number;
|
||||
|
||||
41
src/utils.ts
41
src/utils.ts
@@ -14,8 +14,14 @@ import {
|
||||
mutationData,
|
||||
scrollData,
|
||||
inputData,
|
||||
DocumentDimension,
|
||||
} from './types';
|
||||
import { INode, IGNORED_NODE } from 'rrweb-snapshot';
|
||||
import {
|
||||
INode,
|
||||
IGNORED_NODE,
|
||||
serializedNodeWithId,
|
||||
NodeType,
|
||||
} from 'rrweb-snapshot';
|
||||
|
||||
export function on(
|
||||
type: string,
|
||||
@@ -527,3 +533,36 @@ export function iterateResolveTree(
|
||||
iterateResolveTree(tree.children[i], cb);
|
||||
}
|
||||
}
|
||||
|
||||
type HTMLIFrameINode = HTMLIFrameElement & {
|
||||
__sn: serializedNodeWithId;
|
||||
};
|
||||
export type AppendedIframe = {
|
||||
mutationInQueue: addedNodeMutation;
|
||||
builtNode: HTMLIFrameINode;
|
||||
};
|
||||
|
||||
export function isIframeINode(node: INode): node is HTMLIFrameINode {
|
||||
// node can be document fragment when using the virtual parent feature
|
||||
if (!node.__sn) {
|
||||
return false;
|
||||
}
|
||||
return node.__sn.type === NodeType.Element && node.__sn.tagName === 'iframe';
|
||||
}
|
||||
|
||||
export function getBaseDimension(node: Node): DocumentDimension {
|
||||
const frameElement = node.ownerDocument?.defaultView?.frameElement;
|
||||
if (!frameElement) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const frameDimension = frameElement.getBoundingClientRect();
|
||||
const frameBaseDimension = getBaseDimension(frameElement);
|
||||
return {
|
||||
x: frameDimension.x + frameBaseDimension.x,
|
||||
y: frameDimension.y + frameBaseDimension.y,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user