Skip to content

Feat/hanlde frames #1676

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
9 changes: 8 additions & 1 deletion packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,14 @@ function buildNode(
'rrweb-original-srcset',
n.attributes.srcset as string,
);
} else {
}
// Set the sandbox attribute on the iframe element will make it lose its contentDocument access and therefore cause additional playback errors.
else if (
(tagName === 'iframe' || tagName === 'frame') &&
name === 'sandbox'
)
continue;
else {
node.setAttribute(name, value.toString());
}
} catch (error) {
Expand Down
7 changes: 5 additions & 2 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@
// should warn? maybe a text node isn't attached to a parent node yet?
return false;
} else {
el = dom.parentElement(node)!;

Check warning on line 285 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L285

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}
try {
if (typeof maskTextClass === 'string') {
Expand Down Expand Up @@ -702,10 +702,10 @@
const recordInlineImage = () => {
image.removeEventListener('load', recordInlineImage);
try {
canvasService!.width = image.naturalWidth;

Check warning on line 705 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L705

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasService!.height = image.naturalHeight;

Check warning on line 706 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L706

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasCtx!.drawImage(image, 0, 0);

Check warning on line 707 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L707

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
attributes.rr_dataURL = canvasService!.toDataURL(

Check warning on line 708 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L708

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
dataURLOptions.type,
dataURLOptions.quality,
);
Expand Down Expand Up @@ -767,7 +767,10 @@
};
}
// iframe
if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src as string)) {
if (
(tagName === 'iframe' || tagName === 'frame') &&
!keepIframeSrcFn(attributes.src as string)
) {
if (!(n as HTMLIFrameElement).contentDocument) {
// we can't record it directly as we can't see into it
// preserve the src attribute so a decision can be taken at replay time
Expand Down Expand Up @@ -1111,7 +1114,7 @@

if (
serializedNode.type === NodeType.Element &&
serializedNode.tagName === 'iframe'
(serializedNode.tagName === 'iframe' || serializedNode.tagName === 'frame')
) {
onceIframeLoaded(
n as HTMLIFrameElement,
Expand Down
2 changes: 1 addition & 1 deletion packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,13 +363,13 @@
};

while (this.mapRemoves.length) {
this.mirror.removeNodeFromMap(this.mapRemoves.shift()!);

Check warning on line 366 in packages/rrweb/src/record/mutation.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/mutation.ts#L366

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}

for (const n of this.movedSet) {
if (
isParentRemoved(this.removesSubTreeCache, n, this.mirror) &&
!this.movedSet.has(dom.parentNode(n)!)

Check warning on line 372 in packages/rrweb/src/record/mutation.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/mutation.ts#L372

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
) {
continue;
}
Expand Down Expand Up @@ -602,7 +602,7 @@

let item = this.attributeMap.get(m.target);
if (
target.tagName === 'IFRAME' &&
(target.tagName === 'IFRAME' || target.tagName === 'FRAME') &&
attributeName === 'src' &&
!this.keepIframeSrcFn(value as string)
) {
Expand Down Expand Up @@ -836,7 +836,7 @@
function _isParentRemoved(
removes: Set<Node>,
n: Node,
_mirror: Mirror,

Check warning on line 839 in packages/rrweb/src/record/mutation.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/mutation.ts#L839

[@typescript-eslint/no-unused-vars] '_mirror' is defined but never used.
): boolean {
const node: ParentNode | null = dom.parentNode(n);
if (!node) return false;
Expand Down
52 changes: 49 additions & 3 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
private mouseTail: HTMLCanvasElement | null = null;
private tailPositions: Array<{ x: number; y: number }> = [];

private emitter: Emitter = mitt();
private emitter: Emitter = mitt() as Emitter;

private nextUserInteractionEvent: eventWithTime | null;

Expand Down Expand Up @@ -331,6 +331,8 @@
this.applySelection(this.lastSelectionData);
this.lastSelectionData = null;
}

this.emitter.emit(ReplayerEvents.FlushEnd);
});
this.emitter.on(ReplayerEvents.PlayBack, () => {
this.firstFullSnapshot = null;
Expand Down Expand Up @@ -525,6 +527,35 @@
this.emitter.emit(ReplayerEvents.Start);
}

/**
* Applies all events synchronously until the given event index.
* @param eventIndex - number
*/
public replayEvent(eventIndex: number) {
const handleFinish = () => {
this.service.send('END');
this.emitter.off(ReplayerEvents.FlushEnd, handleFinish);
};
this.emitter.on(ReplayerEvents.FlushEnd, handleFinish);

if (this.service.state.matches('paused')) {
this.service.send({
type: 'PLAY_SINGLE_EVENT',
payload: { singleEvent: eventIndex },
});
} else {
this.service.send({ type: 'PAUSE' });
this.service.send({
type: 'PLAY_SINGLE_EVENT',
payload: { singleEvent: eventIndex },
});
}
this.iframe.contentDocument
?.getElementsByTagName('html')[0]
?.classList.remove('rrweb-paused');
this.emitter.emit(ReplayerEvents.Start);
}

public pause(timeOffset?: number) {
if (timeOffset === undefined && this.service.state.matches('playing')) {
this.service.send({ type: 'PAUSE' });
Expand Down Expand Up @@ -558,6 +589,7 @@
this.mediaManager.reset();
this.config.root.removeChild(this.wrapper);
this.emitter.emit(ReplayerEvents.Destroy);
this.emitter.all.clear();
}

public startLive(baselineTime?: number) {
Expand Down Expand Up @@ -932,7 +964,7 @@
sn?.type === NodeType.Element &&
sn?.tagName.toUpperCase() === 'HTML'
) {
const { documentElement, head } = iframeEl.contentDocument!;

Check warning on line 967 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L967

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
this.insertStyleRules(
documentElement as HTMLElement | RRElement,
head as HTMLElement | RRElement,
Expand All @@ -951,14 +983,14 @@
};

buildNodeWithSN(mutation.node, {
doc: iframeEl.contentDocument! as Document,

Check warning on line 986 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L986

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
mirror: mirror as Mirror,
hackCss: true,
skipChild: false,
afterAppend,
cache: this.cache,
});
afterAppend(iframeEl.contentDocument! as Document, mutation.node.id);

Check warning on line 993 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L993

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.

for (const { mutationInQueue, builtNode } of collectedIframes) {
this.attachDocumentToIframe(mutationInQueue, builtNode);
Expand Down Expand Up @@ -1075,7 +1107,7 @@
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imgd = ctx?.createImageData(canvas.width, canvas.height);
ctx?.putImageData(imgd!, 0, 0);

Check warning on line 1110 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1110

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}
}
private async deserializeAndPreloadCanvasEvents(
Expand Down Expand Up @@ -1111,14 +1143,15 @@
e: incrementalSnapshotEvent & { timestamp: number; delay?: number },
isSync: boolean,
) {
const { data: d } = e;
const { data: d, timestamp } = e;

switch (d.source) {
case IncrementalSource.Mutation: {
try {
this.applyMutation(d, isSync);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions
this.warn(`Exception in mutation ${error.message || error}`, d);
this.warn(`Exception in mutation ${String(error)}`, d, timestamp);
}
break;
}
Expand Down Expand Up @@ -1396,7 +1429,7 @@
// Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events.
if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) {
this.usingVirtualDom = true;
buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom);

Check warning on line 1432 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1432

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
// If these legacy missing nodes haven't been resolved, they should be converted to virtual nodes.
if (Object.keys(this.legacy_missingNodeRetryMap).length) {
for (const key in this.legacy_missingNodeRetryMap) {
Expand Down Expand Up @@ -1509,11 +1542,24 @@
return queue.push(mutation);
}

if (
mutation.node.type === NodeType.Document &&
parent?.nodeName?.toLowerCase() !== 'iframe' &&
parent?.nodeName?.toLowerCase() !== 'frame'
) {
console.warn(
'[Replayer] Skipping invalid document append to a non-iframe parent. hi2',
mutation,
parent,
);
return;
}

if (mutation.node.isShadow) {
// If the parent is attached a shadow dom after it's created, it won't have a shadow root.
if (!hasShadowRoot(parent)) {
(parent as Element | RRElement).attachShadow({ mode: 'open' });
parent = (parent as Element | RRElement).shadowRoot! as Node | RRNode;

Check warning on line 1562 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1562

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
} else parent = parent.shadowRoot as Node | RRNode;
}

Expand Down
51 changes: 51 additions & 0 deletions packages/rrweb/src/replay/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
timeOffset: number;
};
}
| {
type: 'PLAY_SINGLE_EVENT';
payload: {
singleEvent: number;
};
}
| {
type: 'CAST_EVENT';
payload: {
Expand Down Expand Up @@ -78,6 +84,30 @@
return events;
}

function discardPriorSnapshotsToEvent(
events: eventWithTime[],
targetIndex: number,
) {
const targetEvent = events[targetIndex];

if (!targetEvent) {
return [];
}

for (let idx = targetIndex; idx >= 0; idx--) {
const event = events[idx];

if (!event) {
continue;
}

if (event.type === EventType.Meta) {
return events.slice(idx, targetIndex + 1);
}
}
return events;
}

type PlayerAssets = {
emitter: Emitter;
applyEventsSynchronously(events: Array<eventWithTime>): void;
Expand Down Expand Up @@ -119,6 +149,10 @@
target: 'playing',
actions: ['recordTimeOffset', 'play'],
},
PLAY_SINGLE_EVENT: {
target: 'playing',
actions: ['playSingleEvent'],
},
CAST_EVENT: {
target: 'paused',
actions: 'castEvent',
Expand Down Expand Up @@ -168,6 +202,23 @@
baselineTime: ctx.events[0].timestamp + timeOffset,
};
}),

playSingleEvent(ctx, event) {
if (event.type !== 'PLAY_SINGLE_EVENT') {
return;
}

const { singleEvent } = event.payload;

const neededEvents = discardPriorSnapshotsToEvent(
ctx.events,
singleEvent,
);

applyEventsSynchronously(neededEvents);
emitter.emit(ReplayerEvents.Flush);
},

play(ctx) {
const { timer, events, baselineTime, lastPlayedEvent } = ctx;
timer.clear();
Expand Down Expand Up @@ -209,7 +260,7 @@
doAction: () => {
castFn();
},
delay: event.delay!,

Check warning on line 263 in packages/rrweb/src/replay/machine.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/machine.ts#L263

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
});
}
}
Expand Down Expand Up @@ -271,7 +322,7 @@
doAction: () => {
castFn();
},
delay: event.delay!,

Check warning on line 325 in packages/rrweb/src/replay/machine.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/machine.ts#L325

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
});
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/rrweb/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
'now you can use replayer.getMirror() to access the mirror instance of a replayer,' +
'\r\n' +
'or you can use record.mirror to access the mirror instance during recording.';
/** @deprecated */

Check warning on line 35 in packages/rrweb/src/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/utils.ts#L35

[tsdoc/syntax] tsdoc-missing-deprecation-message: The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
export let _mirror: DeprecatedMirror = {
map: {},
getId() {
Expand Down Expand Up @@ -116,7 +116,7 @@
set(value) {
// put hooked setter into event loop to avoid of set latency
setTimeout(() => {
d.set!.call(this, value);

Check warning on line 119 in packages/rrweb/src/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/utils.ts#L119

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}, 0);
if (original && original.set) {
original.set.call(this, value);
Expand Down Expand Up @@ -364,7 +364,9 @@
n: TNode,
mirror: IMirror<TNode>,
): boolean {
return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
return Boolean(
(n.nodeName === 'IFRAME' || n.nodeName === 'FRAME') && mirror.getMeta(n),
);
}

export function isSerializedStylesheet<TNode extends Node | RRNode>(
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@
export type customElementCallback = (c: customElementParam) => void;

/**
* @deprecated

Check warning on line 612 in packages/types/src/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/types/src/index.ts#L612

[tsdoc/syntax] tsdoc-missing-deprecation-message: The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
*/
interface INode extends Node {
__sn: serializedNodeWithId;
Expand Down Expand Up @@ -651,6 +651,7 @@
on(type: string, handler: Handler): void;
emit(type: string, event?: unknown): void;
off(type: string, handler: Handler): void;
all: Map<string | symbol, Handler[]>;
};

export type Arguments<T> = T extends (...payload: infer U) => unknown
Expand All @@ -675,6 +676,7 @@
EventCast = 'event-cast',
CustomEvent = 'custom-event',
Flush = 'flush',
FlushEnd = 'flush-end',
StateChange = 'state-change',
PlayBack = 'play-back',
Destroy = 'destroy',
Expand Down
Loading