From 3a68385a3e98f106a926206f23ea8a6cd317f5fa Mon Sep 17 00:00:00 2001 From: Carlos Ortiz Date: Thu, 17 Oct 2024 20:39:55 -0600 Subject: [PATCH 1/7] feat: allow replaying a single event working add events refactor --- packages/rrweb/src/replay/index.ts | 30 +++++++++++++++- packages/rrweb/src/replay/machine.ts | 51 ++++++++++++++++++++++++++++ packages/types/src/index.ts | 2 ++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index be30e7d8fc..a93d1fc67a 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -134,7 +134,7 @@ export class Replayer { 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; @@ -331,6 +331,8 @@ export class Replayer { this.applySelection(this.lastSelectionData); this.lastSelectionData = null; } + + this.emitter.emit(ReplayerEvents.FlushEnd); }); this.emitter.on(ReplayerEvents.PlayBack, () => { this.firstFullSnapshot = null; @@ -525,6 +527,31 @@ export class Replayer { this.emitter.emit(ReplayerEvents.Start); } + public playSingleEvent(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' }); @@ -558,6 +585,7 @@ export class Replayer { this.mediaManager.reset(); this.config.root.removeChild(this.wrapper); this.emitter.emit(ReplayerEvents.Destroy); + this.emitter.all.clear(); } public startLive(baselineTime?: number) { diff --git a/packages/rrweb/src/replay/machine.ts b/packages/rrweb/src/replay/machine.ts index 08b72c9543..74f1e668c7 100644 --- a/packages/rrweb/src/replay/machine.ts +++ b/packages/rrweb/src/replay/machine.ts @@ -28,6 +28,12 @@ export type PlayerEvent = timeOffset: number; }; } + | { + type: 'PLAY_SINGLE_EVENT'; + payload: { + singleEvent: number; + }; + } | { type: 'CAST_EVENT'; payload: { @@ -78,6 +84,30 @@ export function discardPriorSnapshots( 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): void; @@ -119,6 +149,10 @@ export function createPlayerService( target: 'playing', actions: ['recordTimeOffset', 'play'], }, + PLAY_SINGLE_EVENT: { + target: 'playing', + actions: ['playSingleEvent'], + }, CAST_EVENT: { target: 'paused', actions: 'castEvent', @@ -168,6 +202,23 @@ export function createPlayerService( 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(); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 75155cab34..b6a31f36fb 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -651,6 +651,7 @@ export type Emitter = { on(type: string, handler: Handler): void; emit(type: string, event?: unknown): void; off(type: string, handler: Handler): void; + all: Map; }; export type Arguments = T extends (...payload: infer U) => unknown @@ -675,6 +676,7 @@ export enum ReplayerEvents { EventCast = 'event-cast', CustomEvent = 'custom-event', Flush = 'flush', + FlushEnd = 'flush-end', StateChange = 'state-change', PlayBack = 'play-back', Destroy = 'destroy', From 6b286f85543246a884daae8d027ec822f1f9c5e2 Mon Sep 17 00:00:00 2001 From: Carlos Ortiz Date: Fri, 18 Oct 2024 10:23:15 -0600 Subject: [PATCH 2/7] change name of public method --- packages/rrweb/src/replay/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index a93d1fc67a..ab7e37856b 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -527,7 +527,11 @@ export class Replayer { this.emitter.emit(ReplayerEvents.Start); } - public playSingleEvent(eventIndex: number) { + /** + * 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); From 4a0bf347a055424c94794bb63766349a96500789 Mon Sep 17 00:00:00 2001 From: Carlos Ortiz Date: Wed, 4 Dec 2024 12:26:18 -0600 Subject: [PATCH 3/7] errors with rebuild --- packages/rrweb-snapshot/src/rebuild.ts | 28 +++++++++++++++++--------- packages/rrweb/src/replay/index.ts | 9 +++++++-- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index e4a4c9df4f..d23277a7ce 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -61,16 +61,21 @@ function getTagName(n: elementNode): string { } export function adaptCssForReplay(cssText: string, cache: BuildCache): string { - const cachedStyle = cache?.stylesWithHoverClass.get(cssText); - if (cachedStyle) return cachedStyle; + try { + const cachedStyle = cache?.stylesWithHoverClass.get(cssText); + if (cachedStyle) return cachedStyle; - const ast: { css: string } = postcss([ - mediaSelectorPlugin, - pseudoClassPlugin, - ]).process(cssText); - const result = ast.css; - cache?.stylesWithHoverClass.set(cssText, result); - return result; + const ast: { css: string } = postcss([ + mediaSelectorPlugin, + pseudoClassPlugin, + ]).process(cssText); + const result = ast.css; + cache?.stylesWithHoverClass.set(cssText, result); + return result; + } catch (e) { + console.warn('Failed to adapt css for replay. Using default cssText', e); + return cssText; + } } export function createCache(): BuildCache { @@ -288,7 +293,10 @@ 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' && name === 'sandbox') continue; + else { node.setAttribute(name, value.toString()); } } catch (error) { diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index ab7e37856b..51ee81628e 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1143,14 +1143,19 @@ export class Replayer { 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 ${error.message || error}`, + d, + timestamp, + ); } break; } From 26106b8d73874e146438f2d15cc23ec8c7b1adea Mon Sep 17 00:00:00 2001 From: io agudo Date: Tue, 8 Apr 2025 14:27:42 +0200 Subject: [PATCH 4/7] feat: add support for frameset --- packages/rrweb-snapshot/src/rebuild.ts | 6 +++++- packages/rrweb-snapshot/src/snapshot.ts | 7 +++++-- packages/rrweb/src/record/mutation.ts | 2 +- packages/rrweb/src/replay/index.ts | 12 ++++++++++++ packages/rrweb/src/utils.ts | 4 +++- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 9f4cb62bb8..88d1dad38b 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -293,7 +293,11 @@ function buildNode( ); } // 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' && name === 'sandbox') continue; + else if ( + (tagName === 'iframe' || tagName === 'frame') && + name === 'sandbox' + ) + continue; else { node.setAttribute(name, value.toString()); } diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 0e8ec68be9..5a4dc536a3 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -767,7 +767,10 @@ function serializeElementNode( }; } // 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 @@ -1112,7 +1115,7 @@ export function serializeNodeWithId( if ( serializedNode.type === NodeType.Element && - serializedNode.tagName === 'iframe' + (serializedNode.tagName === 'iframe' || serializedNode.tagName === 'frame') ) { onceIframeLoaded( n as HTMLIFrameElement, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 42170b4940..1d8094ebfd 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -594,7 +594,7 @@ export default class MutationBuffer { let item = this.attributeMap.get(m.target); if ( - target.tagName === 'IFRAME' && + (target.tagName === 'IFRAME' || target.tagName === 'FRAME') && attributeName === 'src' && !this.keepIframeSrcFn(value as string) ) { diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 26b3055c51..ff968207b6 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1546,6 +1546,18 @@ export class Replayer { return queue.push(mutation); } + // if ( + // mutation.node.type === NodeType.Document && + // parent?.nodeName?.toLowerCase() !== 'iframe' + // ) { + // console.warn( + // '[Replayer] Skipping invalid document append to a non-iframe parent.', + // 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)) { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index ecf72d05ea..0e30cf3cf4 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -407,7 +407,9 @@ export function isSerializedIframe( n: TNode, mirror: IMirror, ): boolean { - return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); + return Boolean( + (n.nodeName === 'IFRAME' || n.nodeName === 'FRAME') && mirror.getMeta(n), + ); } export function isSerializedStylesheet( From 0b43a7289869a97c0dc0bf633491c2457aee8d2e Mon Sep 17 00:00:00 2001 From: io agudo Date: Tue, 8 Apr 2025 22:40:46 +0200 Subject: [PATCH 5/7] fix: add fix for iframes --- packages/rrweb/src/replay/index.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index d3106493a5..d990975e7d 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1546,17 +1546,19 @@ export class Replayer { return queue.push(mutation); } - // if ( - // mutation.node.type === NodeType.Document && - // parent?.nodeName?.toLowerCase() !== 'iframe' - // ) { - // console.warn( - // '[Replayer] Skipping invalid document append to a non-iframe parent.', - // mutation, - // parent, - // ); - // return; - // } + if ( + mutation.node.type === NodeType.Document && + parent?.nodeName?.toLowerCase() !== 'iframe' && + parent?.nodeName?.toLowerCase() !== 'frame' + ) { + console.log(parent?.nodeName?.toLowerCase()); + 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. From 6a48c91f8dd5a54fec387ef1cbae29111ab3b4d8 Mon Sep 17 00:00:00 2001 From: io agudo Date: Tue, 8 Apr 2025 22:53:12 +0200 Subject: [PATCH 6/7] feat: delete console log --- packages/rrweb/src/replay/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index d990975e7d..d7f60f4504 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1551,7 +1551,6 @@ export class Replayer { parent?.nodeName?.toLowerCase() !== 'iframe' && parent?.nodeName?.toLowerCase() !== 'frame' ) { - console.log(parent?.nodeName?.toLowerCase()); console.warn( '[Replayer] Skipping invalid document append to a non-iframe parent. hi2', mutation, From a0006a98987c816f3fd6e44ee0855b332bb5ae88 Mon Sep 17 00:00:00 2001 From: io agudo Date: Tue, 8 Apr 2025 23:41:23 +0200 Subject: [PATCH 7/7] fix: ifx lint --- packages/rrweb/src/replay/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index d7f60f4504..4eefd6cf6f 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1151,11 +1151,7 @@ export class Replayer { 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, - timestamp, - ); + this.warn(`Exception in mutation ${String(error)}`, d, timestamp); } break; }