Skip to content
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