From 6ecf26cf9eeae736b0c9821cc3977105938b16b6 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 14:55:01 -0400 Subject: [PATCH 1/3] ref: config argv --- packages/rrweb-snapshot/src/snapshot.ts | 439 +++++++++--------- .../rrweb-snapshot/test/integration.test.ts | 8 +- packages/rrweb/src/record/index.ts | 14 +- packages/rrweb/src/record/mutation.ts | 25 +- 4 files changed, 249 insertions(+), 237 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 88b6c8b0ae..bc08602c5f 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -207,7 +207,7 @@ export function absoluteToDoc(doc: Document, attributeValue: string): string { } function isSVGElement(el: Element): boolean { - return Boolean(el.tagName === 'svg' || (el as SVGElement).ownerSVGElement); + return el.tagName === 'svg' || !!(el as SVGElement).ownerSVGElement; } function getHref(doc: Document, customHref?: string) { @@ -445,40 +445,40 @@ function onceStylesheetLoaded( function serializeNode( n: Node, options: { - doc: Document; - mirror: Mirror; blockClass: string | RegExp; blockSelector: string | null; - needsMask: boolean; - inlineStylesheet: boolean; - maskInputOptions: MaskInputOptions; - maskTextFn: MaskTextFn | undefined; - maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; + doc: Document; inlineImages: boolean; - recordCanvas: boolean; + inlineStylesheet: boolean; keepIframeSrcFn: KeepIframeSrcFn; + maskInputFn: MaskInputFn | undefined; + maskInputOptions: MaskInputOptions; + maskTextFn: MaskTextFn | undefined; + mirror: Mirror; + needsMask: boolean; /** * `newlyAddedElement: true` skips scrollTop and scrollLeft check */ - newlyAddedElement?: boolean; + newlyAddedElement: boolean; + recordCanvas: boolean; }, ): serializedNode | false { const { - doc, - mirror, blockClass, blockSelector, - needsMask, - inlineStylesheet, - maskInputOptions = {}, - maskTextFn, - maskInputFn, dataURLOptions = {}, + doc, inlineImages, - recordCanvas, + inlineStylesheet, keepIframeSrcFn, + maskInputFn, + maskInputOptions = {}, + maskTextFn, + mirror, + needsMask, newlyAddedElement = false, + recordCanvas, } = options; // Only record root id when document object is not the base document const rootId = getRootId(doc, mirror); @@ -506,17 +506,17 @@ function serializeNode( }; case n.ELEMENT_NODE: return serializeElementNode(n as HTMLElement, { - doc, blockClass, blockSelector, - inlineStylesheet, - maskInputOptions, - maskInputFn, dataURLOptions, + doc, inlineImages, - recordCanvas, + inlineStylesheet, keepIframeSrcFn, + maskInputFn, + maskInputOptions, newlyAddedElement, + recordCanvas, rootId, }); case n.TEXT_NODE: @@ -606,38 +606,23 @@ function serializeTextNode( function serializeElementNode( n: HTMLElement, options: { - doc: Document; blockClass: string | RegExp; blockSelector: string | null; - inlineStylesheet: boolean; - maskInputOptions: MaskInputOptions; - maskInputFn: MaskInputFn | undefined; - dataURLOptions?: DataURLOptions; + dataURLOptions: DataURLOptions | undefined; + doc: Document; inlineImages: boolean; - recordCanvas: boolean; + inlineStylesheet: boolean; keepIframeSrcFn: KeepIframeSrcFn; + maskInputFn: MaskInputFn | undefined; + maskInputOptions: MaskInputOptions; /** * `newlyAddedElement: true` skips scrollTop and scrollLeft check */ - newlyAddedElement?: boolean; + newlyAddedElement: boolean; + recordCanvas: boolean; rootId: number | undefined; }, ): serializedNode | false { - const { - doc, - blockClass, - blockSelector, - inlineStylesheet, - maskInputOptions = {}, - maskInputFn, - dataURLOptions = {}, - inlineImages, - recordCanvas, - keepIframeSrcFn, - newlyAddedElement = false, - rootId, - } = options; - const needBlock = _isBlockedElement(n, blockClass, blockSelector); const tagName = getValidTagName(n); let attributes: attributes = {}; const len = n.attributes.length; @@ -645,7 +630,7 @@ function serializeElementNode( const attr = n.attributes[i]; if (!ignoreAttribute(tagName, attr.name, attr.value)) { attributes[attr.name] = transformAttribute( - doc, + options.doc, tagName, toLowerCase(attr.name), attr.value, @@ -653,8 +638,8 @@ function serializeElementNode( } } // remote css - if (tagName === 'link' && inlineStylesheet) { - const stylesheet = Array.from(doc.styleSheets).find((s) => { + if (tagName === 'link' && options.inlineStylesheet) { + const stylesheet = Array.from(options.doc.styleSheets).find((s) => { return s.href === (n as HTMLLinkElement).href; }); let cssText: string | null = null; @@ -678,7 +663,7 @@ function serializeElementNode( (n as HTMLStyleElement).sheet as CSSStyleSheet, ); if (cssText) { - attributes._cssText = absoluteToStylesheet(cssText, getHref(doc)); + attributes._cssText = absoluteToStylesheet(cssText, getHref(options.doc)); } } // form fields @@ -697,15 +682,18 @@ function serializeElementNode( type: getInputType(n), tagName, value, - maskInputOptions, - maskInputFn, + maskInputOptions: options.maskInputOptions, + maskInputFn: options.maskInputFn, }); } else if (checked) { attributes.checked = checked; } } if (tagName === 'option') { - if ((n as HTMLOptionElement).selected && !maskInputOptions['select']) { + if ( + (n as HTMLOptionElement).selected && + !options.maskInputOptions['select'] + ) { attributes.selected = true; } else { // ignore the html attribute (which corresponds to DOM (n as HTMLOptionElement).defaultSelected) @@ -714,29 +702,29 @@ function serializeElementNode( } } // canvas image data - if (tagName === 'canvas' && recordCanvas) { + if (tagName === 'canvas' && options.recordCanvas) { if ((n as ICanvas).__context === '2d') { // only record this on 2d canvas if (!is2DCanvasBlank(n as HTMLCanvasElement)) { attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL( - dataURLOptions.type, - dataURLOptions.quality, + options.dataURLOptions?.type, + options.dataURLOptions?.quality, ); } } else if (!('__context' in n)) { // context is unknown, better not call getContext to trigger it const canvasDataURL = (n as HTMLCanvasElement).toDataURL( - dataURLOptions.type, - dataURLOptions.quality, + options.dataURLOptions?.type, + options.dataURLOptions?.quality, ); // create blank canvas of same dimensions - const blankCanvas = doc.createElement('canvas'); + const blankCanvas = options.doc.createElement('canvas'); blankCanvas.width = (n as HTMLCanvasElement).width; blankCanvas.height = (n as HTMLCanvasElement).height; const blankCanvasDataURL = blankCanvas.toDataURL( - dataURLOptions.type, - dataURLOptions.quality, + options.dataURLOptions?.type, + options.dataURLOptions?.quality, ); // no need to save dataURL if it's the same as blank canvas @@ -746,9 +734,9 @@ function serializeElementNode( } } // save image offline - if (tagName === 'img' && inlineImages) { + if (tagName === 'img' && options.inlineImages) { if (!canvasService) { - canvasService = doc.createElement('canvas'); + canvasService = options.doc.createElement('canvas'); canvasCtx = canvasService.getContext('2d'); } const image = n as HTMLImageElement; @@ -762,8 +750,8 @@ function serializeElementNode( canvasService!.height = image.naturalHeight; canvasCtx!.drawImage(image, 0, 0); attributes.rr_dataURL = canvasService!.toDataURL( - dataURLOptions.type, - dataURLOptions.quality, + options.dataURLOptions?.type, + options.dataURLOptions?.quality, ); } catch (err) { if (image.crossOrigin !== 'anonymous') { @@ -801,7 +789,7 @@ function serializeElementNode( mediaAttributes.rr_mediaVolume = (n as HTMLMediaElement).volume; } // Scroll - if (!newlyAddedElement) { + if (!options.newlyAddedElement) { // `scrollTop` and `scrollLeft` are expensive calls because they trigger reflow. // Since `scrollTop` & `scrollLeft` are always 0 when an element is added to the DOM. // And scrolls also get picked up by rrweb's ScrollObserver @@ -814,6 +802,11 @@ function serializeElementNode( } } // block element + const needBlock = _isBlockedElement( + n, + options.blockClass, + options.blockSelector, + ); if (needBlock) { const { width, height } = n.getBoundingClientRect(); attributes = { @@ -823,7 +816,10 @@ function serializeElementNode( }; } // iframe - if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src as string)) { + if ( + tagName === 'iframe' && + !options.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 @@ -832,13 +828,6 @@ function serializeElementNode( delete attributes.src; // prevent auto loading } - let isCustomElement: true | undefined; - try { - if (customElements.get(tagName)) isCustomElement = true; - } catch (e) { - // In case old browsers don't support customElements - } - return { type: NodeType.Element, tagName, @@ -846,8 +835,11 @@ function serializeElementNode( childNodes: [], isSVG: isSVGElement(n as Element) || undefined, needBlock, - rootId, - isCustom: isCustomElement, + rootId: options.rootId, + isCustom: + (typeof customElements !== 'undefined' && + !!customElements.get(tagName)) || + undefined, }; } @@ -956,61 +948,65 @@ function slimDOMExcluded( export function serializeNodeWithId( n: Node, options: { - doc: Document; - mirror: Mirror; blockClass: string | RegExp; blockSelector: string | null; + dataURLOptions: DataURLOptions | undefined; + doc: Document; + iframeLoadTimeout: number | undefined; + inlineImages: boolean | undefined; + inlineStylesheet: boolean; + keepIframeSrcFn: KeepIframeSrcFn | undefined; + maskInputFn: MaskInputFn | undefined; + maskInputOptions: MaskInputOptions | undefined; maskTextClass: string | RegExp; + maskTextFn: MaskTextFn | undefined; maskTextSelector: string | null; + mirror: Mirror; + needsMask: boolean | undefined; + newlyAddedElement: boolean | undefined; + onIframeLoad: + | (( + iframeNode: HTMLIFrameElement, + node: serializedElementNodeWithId, + ) => unknown) + | undefined; + onSerialize: ((n: Node) => unknown) | undefined; + onStylesheetLoad: + | (( + linkNode: HTMLLinkElement, + node: serializedElementNodeWithId, + ) => unknown) + | undefined; + preserveWhiteSpace: boolean | undefined; + recordCanvas: boolean | undefined; skipChild: boolean; - inlineStylesheet: boolean; - newlyAddedElement?: boolean; - maskInputOptions?: MaskInputOptions; - needsMask?: boolean; - maskTextFn: MaskTextFn | undefined; - maskInputFn: MaskInputFn | undefined; slimDOMOptions: SlimDOMOptions; - dataURLOptions?: DataURLOptions; - keepIframeSrcFn?: KeepIframeSrcFn; - inlineImages?: boolean; - recordCanvas?: boolean; - preserveWhiteSpace?: boolean; - onSerialize?: (n: Node) => unknown; - onIframeLoad?: ( - iframeNode: HTMLIFrameElement, - node: serializedElementNodeWithId, - ) => unknown; - iframeLoadTimeout?: number; - onStylesheetLoad?: ( - linkNode: HTMLLinkElement, - node: serializedElementNodeWithId, - ) => unknown; - stylesheetLoadTimeout?: number; + stylesheetLoadTimeout: number | undefined; }, ): serializedNodeWithId | null { const { - doc, - mirror, blockClass, blockSelector, - maskTextClass, - maskTextSelector, - skipChild = false, + dataURLOptions = {}, + doc, + iframeLoadTimeout = 5000, + inlineImages = false, inlineStylesheet = true, + keepIframeSrcFn = () => false, + maskInputFn, maskInputOptions = {}, + maskTextClass, maskTextFn, - maskInputFn, - slimDOMOptions, - dataURLOptions = {}, - inlineImages = false, - recordCanvas = false, - onSerialize, + maskTextSelector, + mirror, + newlyAddedElement = false, onIframeLoad, - iframeLoadTimeout = 5000, + onSerialize, onStylesheetLoad, + recordCanvas = false, + skipChild = false, + slimDOMOptions, stylesheetLoadTimeout = 5000, - keepIframeSrcFn = () => false, - newlyAddedElement = false, } = options; let { needsMask } = options; let { preserveWhiteSpace = true } = options; @@ -1027,20 +1023,20 @@ export function serializeNodeWithId( } const _serializedNode = serializeNode(n, { - doc, - mirror, blockClass, blockSelector, - needsMask, - inlineStylesheet, - maskInputOptions, - maskTextFn, - maskInputFn, dataURLOptions, + doc, inlineImages, - recordCanvas, + inlineStylesheet, keepIframeSrcFn, + maskInputFn, + maskInputOptions, + maskTextFn, + mirror, + needsMask, newlyAddedElement, + recordCanvas, }); if (!_serializedNode) { // TODO: dev only @@ -1098,29 +1094,30 @@ export function serializeNodeWithId( preserveWhiteSpace = false; } const bypassOptions = { - doc, - mirror, blockClass, blockSelector, - needsMask, - maskTextClass, - maskTextSelector, - skipChild, + dataURLOptions, + doc, + iframeLoadTimeout, + inlineImages, inlineStylesheet, + keepIframeSrcFn, + maskInputFn, maskInputOptions, + maskTextClass, maskTextFn, - maskInputFn, - slimDOMOptions, - dataURLOptions, - inlineImages, - recordCanvas, - preserveWhiteSpace, - onSerialize, + maskTextSelector, + mirror, + needsMask, + newlyAddedElement: undefined, onIframeLoad, - iframeLoadTimeout, + onSerialize, onStylesheetLoad, + preserveWhiteSpace, + recordCanvas, + skipChild, + slimDOMOptions, stylesheetLoadTimeout, - keepIframeSrcFn, }; if ( @@ -1168,29 +1165,30 @@ export function serializeNodeWithId( const iframeDoc = (n as HTMLIFrameElement).contentDocument; if (iframeDoc && onIframeLoad) { const serializedIframeNode = serializeNodeWithId(iframeDoc, { - doc: iframeDoc, - mirror, blockClass, blockSelector, - needsMask, - maskTextClass, - maskTextSelector, - skipChild: false, + dataURLOptions, + doc: iframeDoc, + iframeLoadTimeout, + inlineImages, inlineStylesheet, + keepIframeSrcFn, + maskInputFn, maskInputOptions, + maskTextClass, maskTextFn, - maskInputFn, - slimDOMOptions, - dataURLOptions, - inlineImages, - recordCanvas, - preserveWhiteSpace, - onSerialize, + maskTextSelector, + mirror, + needsMask, + newlyAddedElement: undefined, onIframeLoad, - iframeLoadTimeout, + onSerialize, onStylesheetLoad, + preserveWhiteSpace, + recordCanvas, + skipChild: false, + slimDOMOptions, stylesheetLoadTimeout, - keepIframeSrcFn, }); if (serializedIframeNode) { @@ -1220,29 +1218,30 @@ export function serializeNodeWithId( () => { if (onStylesheetLoad) { const serializedLinkNode = serializeNodeWithId(n, { - doc, - mirror, blockClass, blockSelector, - needsMask, - maskTextClass, - maskTextSelector, - skipChild: false, + dataURLOptions, + doc, + iframeLoadTimeout, + inlineImages, inlineStylesheet, + keepIframeSrcFn, + maskInputFn, maskInputOptions, + maskTextClass, maskTextFn, - maskInputFn, - slimDOMOptions, - dataURLOptions, - inlineImages, - recordCanvas, - preserveWhiteSpace, - onSerialize, + maskTextSelector, + mirror, + needsMask, + newlyAddedElement: undefined, onIframeLoad, - iframeLoadTimeout, + onSerialize, onStylesheetLoad, + preserveWhiteSpace, + recordCanvas, + skipChild: false, + slimDOMOptions, stylesheetLoadTimeout, - keepIframeSrcFn, }); if (serializedLinkNode) { @@ -1262,31 +1261,31 @@ export function serializeNodeWithId( function snapshot( n: Document, - options?: { - mirror?: Mirror; + options: { blockClass?: string | RegExp; blockSelector?: string | null; - maskTextClass?: string | RegExp; - maskTextSelector?: string | null; + dataURLOptions?: DataURLOptions; + inlineImages?: boolean; inlineStylesheet?: boolean; maskAllInputs?: boolean | MaskInputOptions; - maskTextFn?: MaskTextFn; maskInputFn?: MaskInputFn; - slimDOM?: 'all' | boolean | SlimDOMOptions; - dataURLOptions?: DataURLOptions; - inlineImages?: boolean; - recordCanvas?: boolean; - preserveWhiteSpace?: boolean; - onSerialize?: (n: Node) => unknown; + maskTextClass?: string | RegExp; + maskTextFn?: MaskTextFn; + maskTextSelector?: string | null; + mirror?: Mirror; onIframeLoad?: ( iframeNode: HTMLIFrameElement, node: serializedElementNodeWithId, ) => unknown; - iframeLoadTimeout?: number; + onSerialize?: (n: Node) => unknown; onStylesheetLoad?: ( linkNode: HTMLLinkElement, node: serializedElementNodeWithId, ) => unknown; + preserveWhiteSpace?: boolean; + recordCanvas?: boolean; + slimDOM?: 'all' | boolean | SlimDOMOptions; + iframeLoadTimeout?: number; stylesheetLoadTimeout?: number; keepIframeSrcFn?: KeepIframeSrcFn; }, @@ -1311,33 +1310,16 @@ function snapshot( iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, - keepIframeSrcFn = () => false, - } = options || {}; + keepIframeSrcFn, + } = options; + const maskInputOptions: MaskInputOptions = maskAllInputs === true - ? { - color: true, - date: true, - 'datetime-local': true, - email: true, - month: true, - number: true, - range: true, - search: true, - tel: true, - text: true, - time: true, - url: true, - week: true, - textarea: true, - select: true, - password: true, - } + ? MASK_ALL_INPUT_SETTINGS : maskAllInputs === false - ? { - password: true, - } + ? MASK_ONLY_PASSWORD_SETTINGS : maskAllInputs; + const slimDOMOptions: SlimDOMOptions = slimDOM === true || slimDOM === 'all' ? // if true: set of sensible options that should not throw away any information @@ -1356,33 +1338,58 @@ function snapshot( : slimDOM === false ? {} : slimDOM; + return serializeNodeWithId(n, { - doc: n, - mirror, blockClass, blockSelector, - maskTextClass, - maskTextSelector, - skipChild: false, + dataURLOptions, + doc: n, + iframeLoadTimeout, + inlineImages, inlineStylesheet, + keepIframeSrcFn, + maskInputFn, maskInputOptions, + maskTextClass, maskTextFn, - maskInputFn, - slimDOMOptions, - dataURLOptions, - inlineImages, - recordCanvas, - preserveWhiteSpace, - onSerialize, + maskTextSelector, + mirror, + needsMask: undefined, + newlyAddedElement: false, onIframeLoad, - iframeLoadTimeout, + onSerialize, onStylesheetLoad, + preserveWhiteSpace, + recordCanvas, + skipChild: false, + slimDOMOptions, stylesheetLoadTimeout, - keepIframeSrcFn, - newlyAddedElement: false, }); } +const MASK_ALL_INPUT_SETTINGS: MaskInputOptions = { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, +} + +const MASK_ONLY_PASSWORD_SETTINGS: MaskInputOptions = { + password: true, + } + export function visitSnapshot( node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown, diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 23a19c1a29..208045b318 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -138,7 +138,7 @@ describe('integration tests', function (this: ISuite) { const rebuildHtml = ( (await page.evaluate(`${code} const x = new XMLSerializer(); - const snap = rrwebSnapshot.snapshot(document); + const snap = rrwebSnapshot.snapshot(document, {}); let out = x.serializeToString(rrwebSnapshot.rebuild(snap, { doc: document })); if (document.querySelector('html').getAttribute('xmlns') !== 'http://www.w3.org/1999/xhtml') { // this is just an artefact of serializeToString @@ -180,7 +180,7 @@ describe('integration tests', function (this: ISuite) { `pre-check: images will be rendered ~326px high in BackCompat mode, and ~588px in CSS1Compat mode; getting: ${renderedHeight}px`, ); const rebuildRenderedHeight = await page.evaluate(`${code} -const snap = rrwebSnapshot.snapshot(document); +const snap = rrwebSnapshot.snapshot(document, {}); const iframe = document.createElement('iframe'); iframe.setAttribute('width', document.body.clientWidth) iframe.setAttribute('height', document.body.clientHeight) @@ -422,7 +422,7 @@ describe('iframe integration tests', function (this: ISuite) { }); const snapshotResult = JSON.stringify( await page.evaluate(`${code}; - rrwebSnapshot.snapshot(document); + rrwebSnapshot.snapshot(document, {}); `), null, 2, @@ -465,7 +465,7 @@ describe('shadow DOM integration tests', function (this: ISuite) { }); const snapshotResult = JSON.stringify( await page.evaluate(`${code}; - rrwebSnapshot.snapshot(document); + rrwebSnapshot.snapshot(document, {}); `), null, 2, diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 36300d0595..5e6fa1c4f5 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -375,19 +375,19 @@ function record( mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting const node = snapshot(document, { - mirror, blockClass, blockSelector, - maskTextClass, - maskTextSelector, + dataURLOptions, + inlineImages, inlineStylesheet, maskAllInputs: maskInputOptions, - maskTextFn, maskInputFn, - slimDOM: slimDOMOptions, - dataURLOptions, + maskTextClass, + maskTextFn, + maskTextSelector, + mirror, recordCanvas, - inlineImages, + slimDOM: slimDOMOptions, onSerialize: (n) => { if (isSerializedIframe(n, mirror)) { iframeManager.addIframe(n as HTMLIFrameElement); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index a798441969..ec5109c929 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -300,22 +300,27 @@ export default class MutationBuffer { return addList.addNode(n); } const sn = serializeNodeWithId(n, { - doc: this.doc, - mirror: this.mirror, blockClass: this.blockClass, blockSelector: this.blockSelector, - maskTextClass: this.maskTextClass, - maskTextSelector: this.maskTextSelector, - skipChild: true, - newlyAddedElement: true, + dataURLOptions: this.dataURLOptions, + doc: this.doc, + iframeLoadTimeout: undefined, + inlineImages: this.inlineImages, inlineStylesheet: this.inlineStylesheet, + keepIframeSrcFn: undefined, + maskInputFn: this.maskInputFn, maskInputOptions: this.maskInputOptions, + maskTextClass: this.maskTextClass, maskTextFn: this.maskTextFn, - maskInputFn: this.maskInputFn, - slimDOMOptions: this.slimDOMOptions, - dataURLOptions: this.dataURLOptions, + maskTextSelector: this.maskTextSelector, + mirror: this.mirror, + needsMask: undefined, + newlyAddedElement: true, + preserveWhiteSpace: undefined, recordCanvas: this.recordCanvas, - inlineImages: this.inlineImages, + skipChild: true, + slimDOMOptions: this.slimDOMOptions, + stylesheetLoadTimeout: undefined, onSerialize: (currentN) => { if (isSerializedIframe(currentN, this.mirror)) { this.iframeManager.addIframe(currentN as HTMLIFrameElement); From 2f88e64a2b1efcea3d0258b922169765d42c6ea6 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 17:36:32 -0400 Subject: [PATCH 2/3] cleanup arguments --- packages/rrweb-snapshot/src/snapshot.ts | 177 ++++++++++-------------- packages/rrweb-snapshot/src/utils.ts | 7 +- 2 files changed, 79 insertions(+), 105 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index bc08602c5f..5b4d07582d 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -447,13 +447,13 @@ function serializeNode( options: { blockClass: string | RegExp; blockSelector: string | null; - dataURLOptions?: DataURLOptions; + dataURLOptions: DataURLOptions | undefined; doc: Document; inlineImages: boolean; inlineStylesheet: boolean; keepIframeSrcFn: KeepIframeSrcFn; maskInputFn: MaskInputFn | undefined; - maskInputOptions: MaskInputOptions; + maskInputOptions: MaskInputOptions | undefined; maskTextFn: MaskTextFn | undefined; mirror: Mirror; needsMask: boolean; @@ -464,24 +464,8 @@ function serializeNode( recordCanvas: boolean; }, ): serializedNode | false { - const { - blockClass, - blockSelector, - dataURLOptions = {}, - doc, - inlineImages, - inlineStylesheet, - keepIframeSrcFn, - maskInputFn, - maskInputOptions = {}, - maskTextFn, - mirror, - needsMask, - newlyAddedElement = false, - recordCanvas, - } = options; // Only record root id when document object is not the base document - const rootId = getRootId(doc, mirror); + const rootId = getRootId(options.doc, options.mirror); switch (n.nodeType) { case n.DOCUMENT_NODE: if ((n as Document).compatMode !== 'CSS1Compat') { @@ -506,24 +490,24 @@ function serializeNode( }; case n.ELEMENT_NODE: return serializeElementNode(n as HTMLElement, { - blockClass, - blockSelector, - dataURLOptions, - doc, - inlineImages, - inlineStylesheet, - keepIframeSrcFn, - maskInputFn, - maskInputOptions, - newlyAddedElement, - recordCanvas, + blockClass: options.blockClass, + blockSelector: options.blockSelector, + dataURLOptions: options.dataURLOptions, + doc: options.doc, + inlineImages: options.inlineImages, + inlineStylesheet: options.inlineStylesheet, + keepIframeSrcFn: options.keepIframeSrcFn, + maskInputFn: options.maskInputFn, + maskInputOptions: options.maskInputOptions, + newlyAddedElement: options.newlyAddedElement, + recordCanvas: options.recordCanvas, rootId, }); case n.TEXT_NODE: return serializeTextNode(n as Text, { - doc, - needsMask, - maskTextFn, + doc: options.doc, + needsMask: options.needsMask, + maskTextFn: options.maskTextFn, rootId, }); case n.CDATA_SECTION_NODE: @@ -614,7 +598,7 @@ function serializeElementNode( inlineStylesheet: boolean; keepIframeSrcFn: KeepIframeSrcFn; maskInputFn: MaskInputFn | undefined; - maskInputOptions: MaskInputOptions; + maskInputOptions: MaskInputOptions | undefined; /** * `newlyAddedElement: true` skips scrollTop and scrollLeft check */ @@ -692,6 +676,7 @@ function serializeElementNode( if (tagName === 'option') { if ( (n as HTMLOptionElement).selected && + options.maskInputOptions && !options.maskInputOptions['select'] ) { attributes.selected = true; @@ -945,6 +930,9 @@ function slimDOMExcluded( return false; } +const DEFAULT_KEEP_IFRAME_SRC = () => false; +const DEFAULT_DATA_URL_OPTIONS = {}; + export function serializeNodeWithId( n: Node, options: { @@ -987,14 +975,14 @@ export function serializeNodeWithId( const { blockClass, blockSelector, - dataURLOptions = {}, + dataURLOptions = DEFAULT_DATA_URL_OPTIONS, doc, iframeLoadTimeout = 5000, inlineImages = false, inlineStylesheet = true, - keepIframeSrcFn = () => false, + keepIframeSrcFn = DEFAULT_KEEP_IFRAME_SRC, maskInputFn, - maskInputOptions = {}, + maskInputOptions, maskTextClass, maskTextFn, maskTextSelector, @@ -1008,8 +996,7 @@ export function serializeNodeWithId( slimDOMOptions, stylesheetLoadTimeout = 5000, } = options; - let { needsMask } = options; - let { preserveWhiteSpace = true } = options; + let { needsMask, preserveWhiteSpace = true } = options; if (!needsMask) { // perf: if needsMask = true, children won't also need to check @@ -1127,8 +1114,11 @@ export function serializeNodeWithId( ) { // value parameter in DOM reflects the correct value, so ignore childNode } else { - for (const childN of Array.from(n.childNodes)) { - const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + for (let i = 0; i < n.childNodes.length; i++) { + const serializedChildNode = serializeNodeWithId( + n.childNodes[i], + bypassOptions, + ); if (serializedChildNode) { serializedNode.childNodes.push(serializedChildNode); } @@ -1136,8 +1126,11 @@ export function serializeNodeWithId( } if (isElement(n) && n.shadowRoot) { - for (const childN of Array.from(n.shadowRoot.childNodes)) { - const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + for (let i = 0; i < n.shadowRoot.childNodes.length; i++) { + const serializedChildNode = serializeNodeWithId( + n.shadowRoot.childNodes[i], + bypassOptions, + ); if (serializedChildNode) { isNativeShadowDom(n.shadowRoot) && (serializedChildNode.isShadow = true); @@ -1290,28 +1283,8 @@ function snapshot( keepIframeSrcFn?: KeepIframeSrcFn; }, ): serializedNodeWithId | null { - const { - mirror = new Mirror(), - blockClass = 'rr-block', - blockSelector = null, - maskTextClass = 'rr-mask', - maskTextSelector = null, - inlineStylesheet = true, - inlineImages = false, - recordCanvas = false, - maskAllInputs = false, - maskTextFn, - maskInputFn, - slimDOM = false, - dataURLOptions, - preserveWhiteSpace, - onSerialize, - onIframeLoad, - iframeLoadTimeout, - onStylesheetLoad, - stylesheetLoadTimeout, - keepIframeSrcFn, - } = options; + const slimDOM = options.slimDOM ?? false; + const maskAllInputs = options.maskAllInputs ?? false; const maskInputOptions: MaskInputOptions = maskAllInputs === true @@ -1340,55 +1313,55 @@ function snapshot( : slimDOM; return serializeNodeWithId(n, { - blockClass, - blockSelector, - dataURLOptions, + blockClass: options.blockClass ?? 'rr-block', + blockSelector: options.blockSelector ?? null, + dataURLOptions: options.dataURLOptions, doc: n, - iframeLoadTimeout, - inlineImages, - inlineStylesheet, - keepIframeSrcFn, - maskInputFn, + iframeLoadTimeout: options.iframeLoadTimeout, + inlineImages: options.inlineImages ?? false, + inlineStylesheet: options.inlineStylesheet ?? true, + keepIframeSrcFn: options.keepIframeSrcFn, + maskInputFn: options.maskInputFn, maskInputOptions, - maskTextClass, - maskTextFn, - maskTextSelector, - mirror, + maskTextClass: options.maskTextClass ?? 'rr-mask', + maskTextFn: options.maskTextFn, + maskTextSelector: options.maskTextSelector ?? null, + mirror: options.mirror ?? new Mirror(), needsMask: undefined, newlyAddedElement: false, - onIframeLoad, - onSerialize, - onStylesheetLoad, - preserveWhiteSpace, - recordCanvas, + onIframeLoad: options.onIframeLoad, + onSerialize: options.onSerialize, + onStylesheetLoad: options.onStylesheetLoad, + preserveWhiteSpace: options.preserveWhiteSpace, + recordCanvas: options.recordCanvas, skipChild: false, slimDOMOptions, - stylesheetLoadTimeout, + stylesheetLoadTimeout: options.stylesheetLoadTimeout, }); } const MASK_ALL_INPUT_SETTINGS: MaskInputOptions = { - color: true, - date: true, - 'datetime-local': true, - email: true, - month: true, - number: true, - range: true, - search: true, - tel: true, - text: true, - time: true, - url: true, - week: true, - textarea: true, - select: true, - password: true, -} + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, +}; const MASK_ONLY_PASSWORD_SETTINGS: MaskInputOptions = { - password: true, - } + password: true, +}; export function visitSnapshot( node: serializedNodeWithId, diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 13da20f0f5..44e5ad646c 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -221,7 +221,7 @@ export function maskInputValue({ maskInputFn, }: { element: HTMLElement; - maskInputOptions: MaskInputOptions; + maskInputOptions: MaskInputOptions | undefined; tagName: string; type: string | null; value: string | null; @@ -231,8 +231,9 @@ export function maskInputValue({ const actualType = type && toLowerCase(type); if ( - maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || - (actualType && maskInputOptions[actualType as keyof MaskInputOptions]) + maskInputOptions && + (maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || + (actualType && maskInputOptions[actualType as keyof MaskInputOptions])) ) { if (maskInputFn) { text = maskInputFn(text, element); From 63b9db63de02faf41188f321c97f6bbeea6b1a51 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 17:45:21 -0400 Subject: [PATCH 3/3] add changeset --- .changeset/healthy-brooms-glow.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/healthy-brooms-glow.md diff --git a/.changeset/healthy-brooms-glow.md b/.changeset/healthy-brooms-glow.md new file mode 100644 index 0000000000..f180409a66 --- /dev/null +++ b/.changeset/healthy-brooms-glow.md @@ -0,0 +1,6 @@ +--- +"rrweb-snapshot": major +"rrweb": major +--- + +change optional params of serialize object