From 4a28623afb14ea5576e377e6ab217819b9343411 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 18 Oct 2023 10:43:35 +0200 Subject: [PATCH] feat: Add build flags to allow noop iframe/canvas/shadow dom managers (#114) This PR adds 3 new build flags to rrweb: * `__RRWEB_EXCLUDE_CANVAS__` * `__RRWEB_EXCLUDE_SHADOW_DOM__` * `__RRWEB_EXCLUDE_IFRAME__` If you set these to `true` at build time, it will replace the regular `ShadowDomManager` / `CanvasManager` / `IframeManager` with a noop variant of these managers. All of these together shave off about 8 KB gzipped from our replay bundles, if set to `true`. For now, we'll probably keep this enabled by default, but at least we have a path for users to shake this out if they don't need these features. Note: I played with some other approaches, e.g. instead of having the noop class make these e.g. `iframeManager: IframeManager | undefined`, but I think overall the code to guard against using this everywhere ends up being a similar amount of bytes, plus we need to spread this much more through the codebase, making rebasing on upstream master etc. potentially harder. This way, IMHO it should be the easiest way to keep this as contained as possible. --- packages/rrweb/src/record/iframe-manager.ts | 32 ++++- packages/rrweb/src/record/index.ts | 134 +++++++++++------- .../record/observers/canvas/canvas-manager.ts | 28 +++- .../rrweb/src/record/shadow-dom-manager.ts | 24 +++- packages/rrweb/src/types.ts | 21 ++- 5 files changed, 177 insertions(+), 62 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 09ff366eb4..616c137856 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -12,7 +12,37 @@ import type { } from '@sentry-internal/rrweb-types'; import type { StylesheetManager } from './stylesheet-manager'; -export class IframeManager { +export interface IframeManagerInterface { + crossOriginIframeMirror: CrossOriginIframeMirror; + crossOriginIframeStyleMirror: CrossOriginIframeMirror; + crossOriginIframeRootIdMap: WeakMap; + + addIframe(iframeEl: HTMLIFrameElement): void; + addLoadListener(cb: (iframeEl: HTMLIFrameElement) => unknown): void; + attachIframe( + iframeEl: HTMLIFrameElement, + childSn: serializedNodeWithId, + ): void; +} + +export class IframeManagerNoop implements IframeManagerInterface { + public crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + public crossOriginIframeStyleMirror: CrossOriginIframeMirror; + public crossOriginIframeRootIdMap: WeakMap = + new WeakMap(); + + public addIframe() { + // noop + } + public addLoadListener() { + // noop + } + public attachIframe() { + // noop + } +} + +export class IframeManager implements IframeManagerInterface { private iframes: WeakMap = new WeakMap(); private crossOriginIframeMap: WeakMap = new WeakMap(); diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index a6221abd90..b9ca102944 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -29,9 +29,21 @@ import { adoptedStyleSheetParam, } from '@sentry-internal/rrweb-types'; import type { CrossOriginIframeMessageEventContent } from '../types'; -import { IframeManager } from './iframe-manager'; -import { ShadowDomManager } from './shadow-dom-manager'; -import { CanvasManager } from './observers/canvas/canvas-manager'; +import { + IframeManager, + IframeManagerInterface, + IframeManagerNoop, +} from './iframe-manager'; +import { + ShadowDomManager, + ShadowDomManagerInterface, + ShadowDomManagerNoop, +} from './shadow-dom-manager'; +import { + CanvasManager, + CanvasManagerInterface, + CanvasManagerNoop, +} from './observers/canvas/canvas-manager'; import { StylesheetManager } from './stylesheet-manager'; import ProcessedNodeManager from './processed-node-manager'; import { @@ -47,10 +59,16 @@ function wrapEvent(e: event): eventWithTime { }; } +declare global { + const __RRWEB_EXCLUDE_CANVAS__: boolean; + const __RRWEB_EXCLUDE_SHADOW_DOM__: boolean; + const __RRWEB_EXCLUDE_IFRAME__: boolean; +} + let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void; let takeFullSnapshot!: (isCheckout?: boolean) => void; -let canvasManager!: CanvasManager; +let canvasManager: CanvasManagerInterface; let recording = false; const mirror = createMirror(); @@ -292,13 +310,16 @@ function record( adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, }); - const iframeManager = new IframeManager({ - mirror, - mutationCb: wrappedMutationEmit, - stylesheetManager: stylesheetManager, - recordCrossOriginIframes, - wrappedEmit, - }); + const iframeManager: IframeManagerInterface = + typeof __RRWEB_EXCLUDE_IFRAME__ === 'boolean' && __RRWEB_EXCLUDE_IFRAME__ + ? new IframeManagerNoop() + : new IframeManager({ + mirror, + mutationCb: wrappedMutationEmit, + stylesheetManager: stylesheetManager, + recordCrossOriginIframes, + wrappedEmit, + }); /** * Exposes mirror to the plugins @@ -315,49 +336,56 @@ function record( const processedNodeManager = new ProcessedNodeManager(); - canvasManager = new CanvasManager({ - recordCanvas, - mutationCb: wrappedCanvasMutationEmit, - win: window, - blockClass, - blockSelector, - unblockSelector, - mirror, - sampling: sampling.canvas, - dataURLOptions, - }); + canvasManager = + typeof __RRWEB_EXCLUDE_CANVAS__ === 'boolean' && __RRWEB_EXCLUDE_CANVAS__ + ? new CanvasManagerNoop() + : new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + blockSelector, + unblockSelector, + mirror, + sampling: sampling.canvas, + dataURLOptions, + }); - const shadowDomManager = new ShadowDomManager({ - mutationCb: wrappedMutationEmit, - scrollCb: wrappedScrollEmit, - bypassOptions: { - onMutation, - blockClass, - blockSelector, - unblockSelector, - maskAllText, - maskTextClass, - unmaskTextClass, - maskTextSelector, - unmaskTextSelector, - inlineStylesheet, - maskInputOptions, - dataURLOptions, - maskAttributeFn, - maskTextFn, - maskInputFn, - recordCanvas, - inlineImages, - sampling, - slimDOMOptions, - iframeManager, - stylesheetManager, - canvasManager, - keepIframeSrcFn, - processedNodeManager, - }, - mirror, - }); + const shadowDomManager: ShadowDomManagerInterface = + typeof __RRWEB_EXCLUDE_SHADOW_DOM__ === 'boolean' && + __RRWEB_EXCLUDE_SHADOW_DOM__ + ? new ShadowDomManagerNoop() + : new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + onMutation, + blockClass, + blockSelector, + unblockSelector, + maskAllText, + maskTextClass, + unmaskTextClass, + maskTextSelector, + unmaskTextSelector, + inlineStylesheet, + maskInputOptions, + dataURLOptions, + maskAttributeFn, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + stylesheetManager, + canvasManager, + keepIframeSrcFn, + processedNodeManager, + }, + mirror, + }); takeFullSnapshot = (isCheckout = false) => { if (!recordDOM) { diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index d98c1777e8..93dcd9e674 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -28,7 +28,33 @@ type pendingCanvasMutationsMap = Map< canvasMutationWithType[] >; -export class CanvasManager { +export interface CanvasManagerInterface { + reset(): void; + freeze(): void; + unfreeze(): void; + lock(): void; + unlock(): void; +} + +export class CanvasManagerNoop implements CanvasManagerInterface { + public reset() { + // noop + } + public freeze() { + // noop + } + public unfreeze() { + // noop + } + public lock() { + // noop + } + public unlock() { + // noop + } +} + +export class CanvasManager implements CanvasManagerInterface { private pendingCanvasMutations: pendingCanvasMutationsMap = new Map(); private rafStamps: RafStamps = { latestId: 0, invokeId: null }; private mirror: Mirror; diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 9e15433da0..ed814f49c6 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -20,7 +20,29 @@ type BypassOptions = Omit< sampling: SamplingStrategy; }; -export class ShadowDomManager { +export interface ShadowDomManagerInterface { + init(): void; + addShadowRoot(shadowRoot: ShadowRoot, doc: Document): void; + observeAttachShadow(iframeElement: HTMLIFrameElement): void; + reset(): void; +} + +export class ShadowDomManagerNoop implements ShadowDomManagerInterface { + public init() { + // noop + } + public addShadowRoot() { + // noop + } + public observeAttachShadow() { + // noop + } + public reset() { + // noop + } +} + +export class ShadowDomManager implements ShadowDomManagerInterface { private shadowDoms = new WeakSet(); private mutationCb: mutationCallBack; private scrollCb: scrollCallback; diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 31279be429..16da94fd4b 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -8,11 +8,20 @@ import type { MaskAttributeFn, } from '@sentry-internal/rrweb-snapshot'; import type { PackFn, UnpackFn } from './packer/base'; -import type { IframeManager } from './record/iframe-manager'; -import type { ShadowDomManager } from './record/shadow-dom-manager'; +import type { + IframeManager, + IframeManagerInterface, +} from './record/iframe-manager'; +import type { + ShadowDomManager, + ShadowDomManagerInterface, +} from './record/shadow-dom-manager'; import type { Replayer } from './replay'; import type { RRNode } from '@sentry-internal/rrdom'; -import type { CanvasManager } from './record/observers/canvas/canvas-manager'; +import type { + CanvasManager, + CanvasManagerInterface, +} from './record/observers/canvas/canvas-manager'; import type { StylesheetManager } from './record/stylesheet-manager'; import type { addedNodeMutation, @@ -124,10 +133,10 @@ export type observerParam = { dataURLOptions: DataURLOptions; doc: Document; mirror: Mirror; - iframeManager: IframeManager; + iframeManager: IframeManagerInterface; stylesheetManager: StylesheetManager; - shadowDomManager: ShadowDomManager; - canvasManager: CanvasManager; + shadowDomManager: ShadowDomManagerInterface; + canvasManager: CanvasManagerInterface; processedNodeManager: ProcessedNodeManager; ignoreCSSAttributes: Set; plugins: Array<{