From 5353a893f1f623f80e9a3f39ea58879dce88e0cf Mon Sep 17 00:00:00 2001 From: David Newell Date: Fri, 1 Nov 2024 12:13:31 +0000 Subject: [PATCH 001/197] fix typing issue --- packages/rrdom/src/diff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 5d9ba7cea5..19c0c906c0 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -340,7 +340,7 @@ function diffProps( for (const name in newAttributes) { const newValue = newAttributes[name]; const sn = rrnodeMirror.getMeta(newTree) as elementNode | null; - if (sn?.isSVG && NAMESPACES[name]) + if (sn?.type == RRNodeType.Element && sn?.isSVG && NAMESPACES[name]) oldTree.setAttributeNS(NAMESPACES[name], name, newValue); else if (newTree.tagName === 'CANVAS' && name === 'rr_dataURL') { const image = document.createElement('img'); From 1775a5c3e4131791a293329e4fcde12868e22a19 Mon Sep 17 00:00:00 2001 From: David Newell Date: Fri, 1 Nov 2024 12:14:38 +0000 Subject: [PATCH 002/197] typed node --- packages/rrdom/src/diff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 19c0c906c0..5d9ba7cea5 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -340,7 +340,7 @@ function diffProps( for (const name in newAttributes) { const newValue = newAttributes[name]; const sn = rrnodeMirror.getMeta(newTree) as elementNode | null; - if (sn?.type == RRNodeType.Element && sn?.isSVG && NAMESPACES[name]) + if (sn?.isSVG && NAMESPACES[name]) oldTree.setAttributeNS(NAMESPACES[name], name, newValue); else if (newTree.tagName === 'CANVAS' && name === 'rr_dataURL') { const image = document.createElement('img'); From 38877fe0ced6f817eeb180a7ca9809b5e853f3d2 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 8 Jun 2023 16:12:54 +0200 Subject: [PATCH 003/197] Add Asset event type and capture assets --- packages/rrweb-snapshot/package.json | 2 +- packages/rrweb-snapshot/src/snapshot.ts | 52 +- packages/rrweb-snapshot/src/utils.ts | 19 + packages/rrweb-snapshot/test/snapshot.test.ts | 86 ++- packages/rrweb/src/record/index.ts | 26 + packages/rrweb/src/record/mutation.ts | 13 + .../src/record/observers/asset-manager.ts | 180 ++++++ packages/rrweb/src/types.ts | 18 + packages/rrweb/test/record/asset.test.ts | 558 ++++++++++++++++++ .../test/record/cross-origin-iframes.test.ts | 7 +- packages/types/src/index.ts | 19 +- yarn.lock | 7 +- 12 files changed, 978 insertions(+), 9 deletions(-) create mode 100644 packages/rrweb/src/record/observers/asset-manager.ts create mode 100644 packages/rrweb/test/record/asset.test.ts diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 299384b2de..e73a8d5ce7 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -61,7 +61,7 @@ "@types/puppeteer": "^5.4.4", "puppeteer": "^17.1.3", "ts-node": "^7.0.1", - "tslib": "^1.9.3", + "tslib": "^2.5.3", "typescript": "^5.4.5", "vite": "^5.3.1", "vite-plugin-dts": "^3.9.1", diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 8c29fa0d7f..2df2de10d7 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -30,6 +30,7 @@ import { extractFileExtension, absolutifyURLs, markCssSplits, + getUrlsFromSrcset, } from './utils'; import dom from '@rrweb/utils'; @@ -407,6 +408,13 @@ function serializeNode( */ newlyAddedElement?: boolean; cssCaptured?: boolean; + /** + * Called when an asset is detected. + * Example of assets: + * - `src` attribute in `img` tags. + * - `srcset` attribute in `img` tags. + */ + onAssetDetected?: (result: { urls: string[] }) => unknown; }, ): serializedNode | false { const { @@ -425,6 +433,7 @@ function serializeNode( keepIframeSrcFn, newlyAddedElement = false, cssCaptured = false, + onAssetDetected, } = options; // Only record root id when document object is not the base document const rootId = getRootId(doc, mirror); @@ -464,6 +473,7 @@ function serializeNode( keepIframeSrcFn, newlyAddedElement, rootId, + onAssetDetected, }); case n.TEXT_NODE: return serializeTextNode(n as Text, { @@ -557,6 +567,13 @@ function serializeElementNode( */ newlyAddedElement?: boolean; rootId: number | undefined; + /** + * Called when an asset is detected. + * Example of assets: + * - `src` attribute in `img` tags. + * - `srcset` attribute in `img` tags. + */ + onAssetDetected?: (result: { urls: string[] }) => unknown; }, ): serializedNode | false { const { @@ -572,10 +589,12 @@ function serializeElementNode( keepIframeSrcFn, newlyAddedElement = false, rootId, + onAssetDetected = false, } = options; const needBlock = _isBlockedElement(n, blockClass, blockSelector); const tagName = getValidTagName(n); let attributes: attributes = {}; + const assets: string[] = []; const len = n.attributes.length; for (let i = 0; i < len; i++) { const attr = n.attributes[i]; @@ -690,7 +709,16 @@ function serializeElementNode( } } // save image offline - if (tagName === 'img' && inlineImages) { + if (tagName === 'img' && onAssetDetected) { + if (attributes.src) { + assets.push(attributes.src.toString()); + } + if (attributes.srcset) { + assets.push(...getUrlsFromSrcset(attributes.srcset.toString())); + } + // TODO: decide if inlineImages should still be supported, + // and if so if it should be moved into `rrweb` package. + } else if (tagName === 'img' && inlineImages) { if (!canvasService) { canvasService = doc.createElement('canvas'); canvasCtx = canvasService.getContext('2d'); @@ -782,6 +810,9 @@ function serializeElementNode( } catch (e) { // In case old browsers don't support customElements } + if (assets.length && onAssetDetected) { + onAssetDetected({ urls: assets }); + } return { type: NodeType.Element, @@ -930,6 +961,13 @@ export function serializeNodeWithId( ) => unknown; stylesheetLoadTimeout?: number; cssCaptured?: boolean; + /** + * Called when an asset is detected. + * Example of assets: + * - `src` attribute in `img` tags. + * - `srcset` attribute in `img` tags. + */ + onAssetDetected?: (result: { urls: string[] }) => unknown; }, ): serializedNodeWithId | null { const { @@ -956,6 +994,7 @@ export function serializeNodeWithId( keepIframeSrcFn = () => false, newlyAddedElement = false, cssCaptured = false, + onAssetDetected, } = options; let { needsMask } = options; let { preserveWhiteSpace = true } = options; @@ -987,6 +1026,7 @@ export function serializeNodeWithId( keepIframeSrcFn, newlyAddedElement, cssCaptured, + onAssetDetected, }); if (!_serializedNode) { // TODO: dev only @@ -1067,6 +1107,7 @@ export function serializeNodeWithId( stylesheetLoadTimeout, keepIframeSrcFn, cssCaptured: false, + onAssetDetected, }; if ( @@ -1240,6 +1281,13 @@ function snapshot( ) => unknown; stylesheetLoadTimeout?: number; keepIframeSrcFn?: KeepIframeSrcFn; + /** + * Called when an asset is detected. + * Example of assets: + * - `src` attribute in `img` tags. + * - `srcset` attribute in `img` tags. + */ + onAssetDetected?: (result: { urls: string[] }) => unknown; }, ): serializedNodeWithId | null { const { @@ -1262,6 +1310,7 @@ function snapshot( iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, + onAssetDetected, keepIframeSrcFn = () => false, } = options || {}; const maskInputOptions: MaskInputOptions = @@ -1331,6 +1380,7 @@ function snapshot( stylesheetLoadTimeout, keepIframeSrcFn, newlyAddedElement: false, + onAssetDetected, }); } diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 102787c666..5693cd99ee 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -594,3 +594,22 @@ export function markCssSplits( ): string { return splitCssText(cssText, style).join('/* rr_split */'); } + +export function getUrlsFromSrcset(srcset: string): string[] { + const urls: string[] = []; + const parts = srcset.split(','); + for (let i = 0; i < parts.length; i++) { + const trimmed = parts[i].trim(); + const spaceIndex = trimmed.indexOf(' '); + if (spaceIndex === -1) { + // If no descriptor is specified, it's a single URL. + urls.push(trimmed); + } else { + // Otherwise, it's one or more URLs followed by a single descriptor. + // Since we don't know how long the URL will be, we'll assume it's everything + // after the first space. + urls.push(trimmed.substring(0, spaceIndex)); + } + } + return urls; +} diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index 5778eb0aff..97f731669f 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -194,7 +194,7 @@ describe('scrollTop/scrollLeft', () => { }; it('should serialize scroll positions', () => { - const el = render(`
+ const el = render(`
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
`); el.scrollTop = 10; @@ -253,3 +253,87 @@ describe('jsdom snapshot', () => { }); }); }); + +describe('onAssetDetected callback', () => { + const serializeNode = ( + node: Node, + onAssetDetected: (result: { urls: string[] }) => void, + ): serializedNodeWithId | null => { + return serializeNodeWithId(node, { + doc: document, + mirror: new Mirror(), + blockClass: 'blockblock', + blockSelector: null, + maskTextClass: 'maskmask', + maskTextSelector: null, + skipChild: false, + inlineStylesheet: true, + maskTextFn: undefined, + maskInputFn: undefined, + slimDOMOptions: {}, + newlyAddedElement: false, + inlineImages: false, + onAssetDetected, + }); + }; + + const render = (html: string): HTMLDivElement => { + document.write(html); + return document.querySelector('div')!; + }; + + it('should detect `src` attribute in image', () => { + const el = render(`
+ +
`); + + const callback = jest.fn(); + serializeNode(el, callback); + expect(callback).toHaveBeenCalledWith({ + urls: ['https://example.com/image.png'], + }); + }); + + it('should detect `set` attribute in image with ObjectURL', () => { + const el = render(`
+ +
`); + + const callback = jest.fn(); + serializeNode(el, callback); + expect(callback).toHaveBeenCalledWith({ + urls: ['blob:https://example.com/e81acc2b-f460-4aec-91b3-ce9732b837c4'], + }); + }); + it('should detect `srcset` attribute in image', () => { + const el = render(`
+ +
`); + + const callback = jest.fn(); + serializeNode(el, callback); + expect(callback).toHaveBeenCalledWith({ + urls: [ + 'https://example.com/images/team-photo.jpg', + 'https://example.com/images/team-photo-retina.jpg', + ], + }); + }); + + it('should detect `src` attribute in two images', () => { + const el = render(`
+ + +
`); + + const callback = jest.fn(); + serializeNode(el, callback); + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenCalledWith({ + urls: ['https://example.com/image.png'], + }); + expect(callback).toHaveBeenCalledWith({ + urls: ['https://example.com/image2.png'], + }); + }); +}); diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 65da8ec801..065f34972a 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -27,6 +27,7 @@ import { type scrollCallback, type canvasMutationParam, type adoptedStyleSheetParam, + type assetParam, } from '@rrweb/types'; import type { CrossOriginIframeMessageEventContent } from '../types'; import { IframeManager } from './iframe-manager'; @@ -40,11 +41,13 @@ import { unregisterErrorHandler, } from './error-handler'; import dom from '@rrweb/utils'; +import AssetManager from './observers/asset-manager'; let wrappedEmit!: (e: eventWithoutTime, isCheckout?: boolean) => void; let takeFullSnapshot!: (isCheckout?: boolean) => void; let canvasManager!: CanvasManager; +let assetManager!: AssetManager; let recording = false; // Multiple tools (i.e. MooTools, Prototype.js) override Array.from and drop support for the 2nd parameter @@ -95,6 +98,10 @@ function record( userTriggeredOnInput = false, collectFonts = false, inlineImages = false, + assetCaptureConfig = { + captureObjectURLs: true, + captureOrigins: false, + }, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), @@ -279,6 +286,12 @@ function record( }, }); + const wrappedAssetEmit = (p: assetParam) => + wrappedEmit({ + type: EventType.Asset, + data: p, + }); + const wrappedAdoptedStyleSheetEmit = (a: adoptedStyleSheetParam) => wrappedEmit({ type: EventType.IncrementalSnapshot, @@ -327,6 +340,12 @@ function record( dataURLOptions, }); + assetManager = new AssetManager({ + mutationCb: wrappedAssetEmit, + win: window, + assetCaptureConfig, + }); + const shadowDomManager = new ShadowDomManager({ mutationCb: wrappedMutationEmit, scrollCb: wrappedScrollEmit, @@ -349,6 +368,7 @@ function record( canvasManager, keepIframeSrcFn, processedNodeManager, + assetManager, }, mirror, }); @@ -408,6 +428,11 @@ function record( onStylesheetLoad: (linkEl, childSn) => { stylesheetManager.attachLinkElement(linkEl, childSn); }, + onAssetDetected: (assets) => { + assets.urls.forEach((url) => { + assetManager.capture(url); + }); + }, keepIframeSrcFn, }); @@ -552,6 +577,7 @@ function record( shadowDomManager, processedNodeManager, canvasManager, + assetManager, ignoreCSSAttributes, plugins: plugins diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 08e927a98f..73a30b1bf0 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -193,6 +193,7 @@ export default class MutationBuffer { private canvasManager: observerParam['canvasManager']; private processedNodeManager: observerParam['processedNodeManager']; private unattachedDoc: HTMLDocument; + private assetManager: observerParam['assetManager']; public init(options: MutationBufferParam) { ( @@ -218,6 +219,7 @@ export default class MutationBuffer { 'shadowDomManager', 'canvasManager', 'processedNodeManager', + 'assetManager', ] as const ).forEach((key) => { // just a type trick, the runtime result is correct @@ -351,6 +353,11 @@ export default class MutationBuffer { this.stylesheetManager.attachLinkElement(link, childSn); }, cssCaptured, + onAssetDetected: (assets) => { + assets.urls.forEach((url) => { + this.assetManager.capture(url); + }); + }, }); if (sn) { adds.push({ @@ -613,6 +620,12 @@ export default class MutationBuffer { } else { return; } + } else if ( + target.tagName === 'IMG' && + (attributeName === 'src' || attributeName === 'srcset') && + value + ) { + this.assetManager.capture(value); } if (!item) { item = { diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts new file mode 100644 index 0000000000..0f5b1486aa --- /dev/null +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -0,0 +1,180 @@ +import type { + IWindow, + SerializedCanvasArg, + eventWithTime, + listenerHandler, +} from '@rrweb/types'; +import type { assetCallback } from '@rrweb/types'; +import { encode } from 'base64-arraybuffer'; + +import { patch } from '../../utils'; +import type { recordOptions } from '../../types'; + +export default class AssetManager { + private urlObjectMap = new Map(); + private capturedURLs = new Set(); + private capturingURLs = new Set(); + private failedURLs = new Set(); + private resetHandlers: listenerHandler[] = []; + private mutationCb: assetCallback; + public readonly config: Exclude< + recordOptions['assetCaptureConfig'], + undefined + >; + + public reset() { + this.urlObjectMap.clear(); + this.capturedURLs.clear(); + this.capturingURLs.clear(); + this.failedURLs.clear(); + this.resetHandlers.forEach((h) => h()); + } + + constructor(options: { + mutationCb: assetCallback; + win: IWindow; + assetCaptureConfig: Exclude< + recordOptions['assetCaptureConfig'], + undefined + >; + }) { + const { win } = options; + + this.mutationCb = options.mutationCb; + this.config = options.assetCaptureConfig; + + const urlObjectMap = this.urlObjectMap; + + if (this.config.captureObjectURLs) { + try { + const restoreHandler = patch( + win.URL, + 'createObjectURL', + function (original: (obj: File | Blob | MediaSource) => string) { + return function (obj: File | Blob | MediaSource) { + const url = original.apply(this, [obj]); + urlObjectMap.set(url, obj); + return url; + }; + }, + ); + this.resetHandlers.push(restoreHandler); + } catch { + console.error('failed to patch URL.createObjectURL'); + } + + try { + const restoreHandler = patch( + win.URL, + 'revokeObjectURL', + function (original: (objectURL: string) => void) { + return function (objectURL: string) { + urlObjectMap.delete(objectURL); + return original.apply(this, [objectURL]); + }; + }, + ); + this.resetHandlers.push(restoreHandler); + } catch { + console.error('failed to patch URL.revokeObjectURL'); + } + } + } + + public shouldIgnore(url: string): boolean { + const originsToIgnore = ['data:']; + const urlIsBlob = url.startsWith(`blob:${window.location.origin}/`); + + // Check if url is a blob and we should ignore blobs + if (urlIsBlob) return !this.config.captureObjectURLs; + + // Check if url matches any ignorable origins + for (const origin of originsToIgnore) { + if (url.startsWith(origin)) return true; + } + + // Check the captureOrigins + const captureOrigins = this.config.captureOrigins; + if (typeof captureOrigins === 'boolean') { + return !captureOrigins; + } else if (Array.isArray(captureOrigins)) { + const urlOrigin = new URL(url).origin; + return !captureOrigins.includes(urlOrigin); + } + + return false; + } + + public async getURLObject( + url: string, + ): Promise { + const object = this.urlObjectMap.get(url); + if (object) { + return object; + } + + try { + const response = await fetch(url); + const blob = await response.blob(); + console.log('getURLObject', url, blob); + return blob; + } catch (e) { + console.warn(`getURLObject failed for ${url}`); + throw e; + } + } + + public capture(url: string): { + status: 'capturing' | 'captured' | 'error' | 'refused'; + } { + console.log('capture', url, this.shouldIgnore(url)); + if (this.shouldIgnore(url)) return { status: 'refused' }; + + if (this.capturedURLs.has(url)) { + return { status: 'captured' }; + } else if (this.capturingURLs.has(url)) { + return { status: 'capturing' }; + } else if (this.failedURLs.has(url)) { + return { status: 'error' }; + } + this.capturingURLs.add(url); + console.log('capturing'); + void this.getURLObject(url) + .then(async (object) => { + console.log('captured', url); + if (object) { + let payload: SerializedCanvasArg; + if (object instanceof File || object instanceof Blob) { + const arrayBuffer = await object.arrayBuffer(); + const base64 = encode(arrayBuffer); // cpu intensive, probably good idea to move all of this to a webworker + + payload = { + rr_type: 'Blob', + type: object.type, + data: [ + { + rr_type: 'ArrayBuffer', + base64, // base64 + }, + ], + }; + + this.capturedURLs.add(url); + this.capturingURLs.delete(url); + + this.mutationCb({ + url, + payload, + }); + } + } + }) + .catch(() => { + // TODO: add mutationCb for failed urls + this.failedURLs.add(url); + this.capturingURLs.delete(url); + }); + + return { status: 'capturing' }; + } +} diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index a03e326b6f..9d39c98106 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -40,6 +40,7 @@ import type { UnpackFn, } from '@rrweb/types'; import type ProcessedNodeManager from './record/processed-node-manager'; +import type AssetManager from './record/observers/asset-manager'; export type recordOptions = { emit?: (e: T, isCheckout?: boolean) => void; @@ -69,6 +70,21 @@ export type recordOptions = { userTriggeredOnInput?: boolean; collectFonts?: boolean; inlineImages?: boolean; + assetCaptureConfig?: { + /** + * Captures object URLs (blobs, files, media sources). + * More info: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL + */ + captureObjectURLs: boolean; + /** + * Allowlist of origins to capture object URLs from. + * [origin, origin, ...] to capture from specific origins. + * e.g. ['https://example.com', 'https://www.example.com'] + * Set to `true` capture from all origins. + * Set to `false` or `[]` to disable capturing from any origin apart from object URLs. + */ + captureOrigins: string[] | true | false; + }; plugins?: RecordPlugin[]; // departed, please use sampling options mousemoveWait?: number; @@ -116,6 +132,7 @@ export type observerParam = { shadowDomManager: ShadowDomManager; canvasManager: CanvasManager; processedNodeManager: ProcessedNodeManager; + assetManager: AssetManager; ignoreCSSAttributes: Set; plugins: Array<{ observer: ( @@ -151,6 +168,7 @@ export type MutationBufferParam = Pick< | 'shadowDomManager' | 'canvasManager' | 'processedNodeManager' + | 'assetManager' >; export type ReplayPlugin = { diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts new file mode 100644 index 0000000000..dfe486b406 --- /dev/null +++ b/packages/rrweb/test/record/asset.test.ts @@ -0,0 +1,558 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type * as puppeteer from 'puppeteer'; +import type { recordOptions } from '../../src/types'; +import type { listenerHandler, eventWithTime, assetEvent } from '@rrweb/types'; +import { EventType } from '@rrweb/types'; +import { + getServerURL, + launchPuppeteer, + startServer, + waitForRAF, +} from '../utils'; +import type * as http from 'http'; + +interface ISuite { + code: string; + browser: puppeteer.Browser; + page: puppeteer.Page; + events: eventWithTime[]; + server: http.Server; + serverURL: string; + serverB: http.Server; + serverBURL: string; +} + +interface IWindow extends Window { + rrweb: { + record: ( + options: recordOptions, + ) => listenerHandler | undefined; + addCustomEvent(tag: string, payload: T): void; + pack: (e: eventWithTime) => string; + }; + emit: (e: eventWithTime) => undefined; + snapshots: eventWithTime[]; +} +type ExtraOptions = { + assetCaptureConfig?: recordOptions['assetCaptureConfig']; +}; + +const BASE64_PNG_RECTANGLE = + 'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAWtJREFUeF7t1cEJAEAIxEDtv2gProo8xgpCwuLezI3LGFhBMi0+iCCtHoLEeggiSM1AjMcPESRmIIZjIYLEDMRwLESQmIEYjoUIEjMQw7EQQWIGYjgWIkjMQAzHQgSJGYjhWIggMQMxHAsRJGYghmMhgsQMxHAsRJCYgRiOhQgSMxDDsRBBYgZiOBYiSMxADMdCBIkZiOFYiCAxAzEcCxEkZiCGYyGCxAzEcCxEkJiBGI6FCBIzEMOxEEFiBmI4FiJIzEAMx0IEiRmI4ViIIDEDMRwLESRmIIZjIYLEDMRwLESQmIEYjoUIEjMQw7EQQWIGYjgWIkjMQAzHQgSJGYjhWIggMQMxHAsRJGYghmMhgsQMxHAsRJCYgRiOhQgSMxDDsRBBYgZiOBYiSMxADMdCBIkZiOFYiCAxAzEcCxEkZiCGYyGCxAzEcCxEkJiBGI6FCBIzEMOxEEFiBmI4FiJIzEAMx0IEiRmI4TwVjsedWCiXGAAAAABJRU5ErkJggg=='; + +async function injectRecordScript( + frame: puppeteer.Frame, + options?: ExtraOptions, +) { + await frame.addScriptTag({ + path: path.resolve(__dirname, '../../dist/rrweb-all.js'), + }); + options = options || {}; + await frame.evaluate((options) => { + (window as unknown as IWindow).snapshots = []; + const { record, pack } = (window as unknown as IWindow).rrweb; + const config: recordOptions = { + assetCaptureConfig: options.assetCaptureConfig, + emit(event) { + (window as unknown as IWindow).snapshots.push(event); + (window as unknown as IWindow).emit(event); + }, + }; + record(config); + }, options); + + for (const child of frame.childFrames()) { + await injectRecordScript(child, options); + } +} + +const setup = function ( + this: ISuite, + content: string, + options?: ExtraOptions, +): ISuite { + const ctx = {} as ISuite; + beforeAll(async () => { + ctx.browser = await launchPuppeteer(); + ctx.server = await startServer(); + ctx.serverURL = getServerURL(ctx.server); + ctx.serverB = await startServer(); + ctx.serverBURL = getServerURL(ctx.serverB); + + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js'); + ctx.code = fs.readFileSync(bundlePath, 'utf8'); + }); + + beforeEach(async () => { + ctx.page = await ctx.browser.newPage(); + await ctx.page.goto('about:blank'); + await ctx.page.setContent( + content + .replace(/\{SERVER_URL\}/g, ctx.serverURL) + .replace(/\{SERVER_B_URL\}/g, ctx.serverBURL), + ); + // await ctx.page.evaluate(ctx.code); + await waitForRAF(ctx.page); + await ctx.page.waitForTimeout(500); // FIXME!! + ctx.events = []; + await ctx.page.exposeFunction('emit', (e: eventWithTime) => { + if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) { + return; + } + ctx.events.push(e); + }); + + ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + if ( + options?.assetCaptureConfig?.captureOrigins && + Array.isArray(options.assetCaptureConfig.captureOrigins) + ) { + options.assetCaptureConfig.captureOrigins = + options.assetCaptureConfig.captureOrigins.map((origin) => + origin.replace(/\{SERVER_URL\}/g, ctx.serverURL), + ); + } + await injectRecordScript(ctx.page.mainFrame(), options); + }); + + afterEach(async () => { + await ctx.page.close(); + }); + + afterAll(async () => { + await ctx.browser.close(); + ctx.server.close(); + ctx.serverB.close(); + }); + + return ctx; +}; + +describe('asset caching', function (this: ISuite) { + jest.setTimeout(100_000); + + describe('captureObjectURLs: true with incremental snapshots', function (this: ISuite) { + const ctx: ISuite = setup.call( + this, + ` + + + + + `, + { + assetCaptureConfig: { + captureObjectURLs: true, + captureOrigins: false, + }, + }, + ); + + it('will emit asset when included as img attribute mutation', async () => { + const url = (await ctx.page.evaluate(() => { + return new Promise((resolve) => { + // create a blob of an image, then create an object URL for the blob + // and append it to the DOM as `src` attribute of an existing image + const img = document.createElement('img'); + document.body.appendChild(img); + + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = canvas.getContext('2d')!; + context.fillStyle = 'red'; + context.fillRect(0, 0, 100, 100); + + canvas.toBlob((blob) => { + if (!blob) return; + + const url = URL.createObjectURL(blob); + img.src = url; + resolve(url); + }); + }); + })) as string; + await waitForRAF(ctx.page); + // await ctx.page.waitForTimeout(40_000); + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + const expected: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: { + rr_type: 'Blob', + data: [ + { + rr_type: 'ArrayBuffer', + base64: BASE64_PNG_RECTANGLE, // base64 + }, + ], + }, + }, + }; + console.log(events); + expect(events[events.length - 1]).toMatchObject(expected); + }); + + it('will emit asset when included with new img', async () => { + const url = (await ctx.page.evaluate(() => { + return new Promise((resolve) => { + // create a blob of an image, then create an object URL for the blob and append it to the DOM as image `src` attribute + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = canvas.getContext('2d')!; + context.fillStyle = 'red'; + context.fillRect(0, 0, 100, 100); + + canvas.toBlob((blob) => { + if (!blob) return; + + const url = URL.createObjectURL(blob); + const img = document.createElement('img'); + img.src = url; + document.body.appendChild(img); + resolve(url); + }); + }); + })) as string; + await waitForRAF(ctx.page); + // await ctx.page.waitForTimeout(40_000); + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + const expected: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: { + rr_type: 'Blob', + data: [ + { + rr_type: 'ArrayBuffer', + base64: BASE64_PNG_RECTANGLE, // base64 + }, + ], + }, + }, + }; + console.log(events); + expect(events[events.length - 1]).toMatchObject(expected); + }); + }); + + describe('captureObjectURLs: true with fullSnapshot', function (this: ISuite) { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + { + assetCaptureConfig: { + captureObjectURLs: true, + captureOrigins: false, + }, + }, + ); + + it('will emit asset when included with existing img', async () => { + await waitForRAF(ctx.page); + const url = (await ctx.page.evaluate(() => { + return document.querySelector('img')?.src; + })) as string; + await waitForRAF(ctx.page); + + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + const expected: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: { + rr_type: 'Blob', + data: [ + { + rr_type: 'ArrayBuffer', + base64: BASE64_PNG_RECTANGLE, // base64 + }, + ], + }, + }, + }; + expect(events[events.length - 1]).toMatchObject(expected); + }); + }); + describe('captureObjectURLs: false', () => { + const ctx: ISuite = setup.call( + this, + ` + + + + + `, + { + assetCaptureConfig: { + captureObjectURLs: false, + captureOrigins: false, + }, + }, + ); + it("shouldn't capture ObjectURLs when its turned off in config", async () => { + const url = (await ctx.page.evaluate(() => { + return new Promise((resolve) => { + // create a blob of an image, then create an object URL for the blob and append it to the DOM as image `src` attribute + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = canvas.getContext('2d')!; + context.fillStyle = 'red'; + context.fillRect(0, 0, 100, 100); + + canvas.toBlob((blob) => { + if (!blob) return; + + const url = URL.createObjectURL(blob); + const img = document.createElement('img'); + img.src = url; + document.body.appendChild(img); + resolve(url); + }); + }); + })) as string; + await waitForRAF(ctx.page); + // await ctx.page.waitForTimeout(40_000); + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + expect(events).not.toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + }), + ); + }); + }); + describe('data urls', () => { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + ); + + it("shouldn't re-capture data:urls", async () => { + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect no event to be emitted with `event.type` === EventType.Asset + console.log(events); + expect(events).not.toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + }), + ); + }); + }); + describe('captureOrigins: false', () => { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + { + assetCaptureConfig: { + captureOrigins: false, + captureObjectURLs: false, + }, + }, + ); + + it("shouldn't capture any urls", async () => { + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect no event to be emitted with `event.type` === EventType.Asset + expect(events).not.toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + }), + ); + }); + }); + describe('captureOrigins: []', () => { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + { + assetCaptureConfig: { + captureOrigins: [], + captureObjectURLs: false, + }, + }, + ); + + it("shouldn't capture any urls", async () => { + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect no event to be emitted with `event.type` === EventType.Asset + expect(events).not.toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + }), + ); + }); + }); + describe('captureOrigins: true', () => { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + { + assetCaptureConfig: { + captureOrigins: true, + captureObjectURLs: false, + }, + }, + ); + + it('capture all urls', async () => { + await ctx.page.waitForNetworkIdle({ idleTime: 100 }); + await waitForRAF(ctx.page); + + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect an event to be emitted with `event.type` === EventType.Asset + expect(events).toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + }), + ); + }); + }); + describe('captureOrigins: ["http://localhost:xxxxx/"]', () => { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + + `, + { + assetCaptureConfig: { + captureOrigins: ['{SERVER_URL}'], + captureObjectURLs: false, + }, + }, + ); + + it('should capture assets with origin defined in config', async () => { + await ctx.page.waitForNetworkIdle({ idleTime: 100 }); + await waitForRAF(ctx.page); + + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect an event to be emitted with `event.type` === EventType.Asset + expect(events).toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + data: { + url: `${ctx.serverURL}/html/assets/robot.png`, + payload: expect.any(Object), + }, + }), + ); + }); + it("shouldn't capture assets with origin not defined in config", async () => { + await ctx.page.waitForNetworkIdle({ idleTime: 100 }); + await waitForRAF(ctx.page); + + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect an event to be emitted with `event.type` === EventType.Asset + expect(events).not.toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + data: { + url: `${ctx.serverBURL}/html/assets/robot.png`, + payload: expect.any(Object), + }, + }), + ); + }); + }); +}); diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index cd6738fa78..26c38c9df7 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -25,6 +25,8 @@ interface ISuite { events: eventWithTime[]; server: http.Server; serverURL: string; + serverB: http.Server; + serverBURL: string; } interface IWindow extends Window { @@ -86,10 +88,7 @@ const setup = function ( content: string, options?: ExtraOptions, ): ISuite { - const ctx = {} as ISuite & { - serverB: http.Server; - serverBURL: string; - }; + const ctx = {} as ISuite; beforeAll(async () => { ctx.browser = await launchPuppeteer(); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index bba276e483..ef9f7127a6 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -6,6 +6,7 @@ export enum EventType { Meta, Custom, Plugin, + Asset, } export type domContentLoadedEvent = { @@ -59,6 +60,14 @@ export type pluginEvent = { }; }; +export type assetEvent = { + type: EventType.Asset; + data: { + url: string; + payload: SerializedCanvasArg; + }; +}; + export enum IncrementalSource { Mutation, MouseMove, @@ -163,7 +172,8 @@ export type eventWithoutTime = | incrementalSnapshotEvent | metaEvent | customEvent - | pluginEvent; + | pluginEvent + | assetEvent; /** * @deprecated intended for internal use @@ -608,6 +618,13 @@ export type customElementParam = { export type customElementCallback = (c: customElementParam) => void; +export type assetParam = { + url: string; + payload: SerializedCanvasArg; +}; + +export type assetCallback = (d: assetParam) => void; + /** * @deprecated */ diff --git a/yarn.lock b/yarn.lock index 7d0b8c00de..6ff3998432 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9927,7 +9927,7 @@ tslib@2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@^1.8.1, tslib@^1.9.3: +tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -9937,6 +9937,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" + integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From 5b994a7303c6860f28b1f330881e04b3ec525a21 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 8 Jun 2023 16:57:50 +0200 Subject: [PATCH 004/197] Add test to prove player works --- packages/rrweb/test/events/assets.ts | 124 ++++++++++++++++++ ...ncorporate-assets-emitted-later-1-snap.png | Bin 0 -> 10796 bytes packages/rrweb/test/replay/asset.test.ts | 65 +++++++++ 3 files changed, 189 insertions(+) create mode 100644 packages/rrweb/test/events/assets.ts create mode 100644 packages/rrweb/test/replay/__image_snapshots__/asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png create mode 100644 packages/rrweb/test/replay/asset.test.ts diff --git a/packages/rrweb/test/events/assets.ts b/packages/rrweb/test/events/assets.ts new file mode 100644 index 0000000000..3c7b36b3de --- /dev/null +++ b/packages/rrweb/test/events/assets.ts @@ -0,0 +1,124 @@ +import { EventType, type eventWithTime } from '@rrweb/types'; + +const events: eventWithTime[] = [ + { + type: 4, + data: { + href: '', + width: 1600, + height: 900, + }, + timestamp: 1636379531385, + }, + { + type: 2, + data: { + node: { + type: 0, + childNodes: [ + { type: 1, name: 'html', publicId: '', systemId: '', id: 2 }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 5 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + id: 6, + }, + { type: 3, textContent: '\n ', id: 7 }, + { + type: 2, + tagName: 'meta', + attributes: { + name: 'viewport', + content: 'width=device-width, initial-scale=1.0', + }, + childNodes: [], + id: 8, + }, + { type: 3, textContent: '\n ', id: 9 }, + { + type: 2, + tagName: 'title', + attributes: {}, + childNodes: [{ type: 3, textContent: 'assets', id: 11 }], + id: 10, + }, + { type: 3, textContent: '\n ', id: 12 }, + ], + id: 4, + }, + { type: 3, textContent: '\n ', id: 13 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 15 }, + { + type: 2, + tagName: 'img', + attributes: { + width: '100', + height: '100', + style: 'border: 1px solid #000000', + src: 'httpx://example.com/image.png', + }, + childNodes: [{ type: 3, textContent: '\n ', id: 17 }], + id: 16, + }, + { type: 3, textContent: '\n ', id: 18 }, + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 20 }, + ], + id: 19, + }, + { type: 3, textContent: '\n \n\n', id: 21 }, + ], + id: 14, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: 1636379531389, + }, + { + type: EventType.Asset, + data: { + url: 'httpx://example.com/image.png', + payload: { + rr_type: 'Blob', + type: 'image/png', + data: [ + { + rr_type: 'ArrayBuffer', + base64: + 'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAWtJREFUeF7t1cEJAEAIxEDtv2gProo8xgpCwuLezI3LGFhBMi0+iCCtHoLEeggiSM1AjMcPESRmIIZjIYLEDMRwLESQmIEYjoUIEjMQw7EQQWIGYjgWIkjMQAzHQgSJGYjhWIggMQMxHAsRJGYghmMhgsQMxHAsRJCYgRiOhQgSMxDDsRBBYgZiOBYiSMxADMdCBIkZiOFYiCAxAzEcCxEkZiCGYyGCxAzEcCxEkJiBGI6FCBIzEMOxEEFiBmI4FiJIzEAMx0IEiRmI4ViIIDEDMRwLESRmIIZjIYLEDMRwLESQmIEYjoUIEjMQw7EQQWIGYjgWIkjMQAzHQgSJGYjhWIggMQMxHAsRJGYghmMhgsQMxHAsRJCYgRiOhQgSMxDDsRBBYgZiOBYiSMxADMdCBIkZiOFYiCAxAzEcCxEkZiCGYyGCxAzEcCxEkJiBGI6FCBIzEMOxEEFiBmI4FiJIzEAMx0IEiRmI4TwVjsedWCiXGAAAAABJRU5ErkJggg==', // base64 + }, + ], + }, + }, + timestamp: 1636379532355, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replay/__image_snapshots__/asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbd91056eb79bc92f28acef03c14da255a83e26 GIT binary patch literal 10796 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4F%}28J29*~C-ahlL4m>3 z#WAE}&YL?MbD0fASRLmr%(~I^Dlk+lNcF`n@gyqn}zyo0kU{f|dUH;Dd@8NGCN-U1fDUMlxeRDcP z&O=s+0Ux$Ko0EL5@*7V>?!0{r|B80stt(~+t3GhrR<3`uIz!G=s8R+3cHwnWOqr}z zU-ETLv#9L3woso{Ow47h=rLS!Y`^Oyh@B1JABkUpxRrreFXFY9dnetkI-2nv@Vp30ti;nnFfX z$Y=@y7onrABycc{Hdw&HFj}92gMpHc%4m-P6ojJ*X*3~?_5#6aU^Fj4f?>2!g@nUs ziwhhMv}$q9e|z^JC$pgdizBzp*=O^ww<@$3ust+33C*{Cc3-DUpaD9Hpu~~@iH`;j zMhOT@!HEGh2nz%qqsld$O7|k-^a2S}vVF&Ml?6vA;keFa7I6;j` z6Q=s;qs1yPC`L;WNO~A8MZn=OFr~<7y9N~aL$6)4e`D<~1_sVs zptS%A;Pf+Eo{To+hhj<|B&qk&-?NKnR>ofc`dSx~ofu?x>}8lY^ZFT^jjYfN#+?0r z?|)k+D6jb6Xfp;F6r;@;NO~A;#(=|Nv>5{qhN0J@NPr~c(d-NfhXKyc9sG^SYMU-F R%;x|($J5o%Wt~$(696>PBJ= { + browser = await launchPuppeteer(); + + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js'); + code = fs.readFileSync(bundlePath, 'utf8'); + }); + + beforeEach(async () => { + page = await browser.newPage(); + await page.goto('about:blank'); + // mouse cursor canvas is large and pushes the replayer below the fold + // lets hide it... + await page.addStyleTag({ + content: '.replayer-mouse-tail{display: none !important;}', + }); + await page.evaluate(code); + await page.evaluate(`let events = ${JSON.stringify(events)}`); + + page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + await browser.close(); + }); + + describe('asset', () => { + it('should incorporate assets emitted later', async () => { + await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events, { + }); + replayer.pause(0); + `); + + const image = await page.screenshot(); + expect(image).toMatchImageSnapshot(); + }); + }); +}); From 78df5b035127211cd41a1e64d763563b806cad82 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 9 Jun 2023 15:27:52 +0200 Subject: [PATCH 005/197] Rename `assetCaptureConfig` to `assetCapture` --- packages/rrweb/src/record/index.ts | 4 +-- .../src/record/observers/asset-manager.ts | 8 +++--- packages/rrweb/src/types.ts | 2 +- packages/rrweb/test/record/asset.test.ts | 26 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 065f34972a..378777180a 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -98,7 +98,7 @@ function record( userTriggeredOnInput = false, collectFonts = false, inlineImages = false, - assetCaptureConfig = { + assetCapture = { captureObjectURLs: true, captureOrigins: false, }, @@ -343,7 +343,7 @@ function record( assetManager = new AssetManager({ mutationCb: wrappedAssetEmit, win: window, - assetCaptureConfig, + assetCapture, }); const shadowDomManager = new ShadowDomManager({ diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts index 0f5b1486aa..595aa3665f 100644 --- a/packages/rrweb/src/record/observers/asset-manager.ts +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -18,7 +18,7 @@ export default class AssetManager { private resetHandlers: listenerHandler[] = []; private mutationCb: assetCallback; public readonly config: Exclude< - recordOptions['assetCaptureConfig'], + recordOptions['assetCapture'], undefined >; @@ -33,15 +33,15 @@ export default class AssetManager { constructor(options: { mutationCb: assetCallback; win: IWindow; - assetCaptureConfig: Exclude< - recordOptions['assetCaptureConfig'], + assetCapture: Exclude< + recordOptions['assetCapture'], undefined >; }) { const { win } = options; this.mutationCb = options.mutationCb; - this.config = options.assetCaptureConfig; + this.config = options.assetCapture; const urlObjectMap = this.urlObjectMap; diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 9d39c98106..eab2aa38aa 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -70,7 +70,7 @@ export type recordOptions = { userTriggeredOnInput?: boolean; collectFonts?: boolean; inlineImages?: boolean; - assetCaptureConfig?: { + assetCapture?: { /** * Captures object URLs (blobs, files, media sources). * More info: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index dfe486b406..a34db4e86f 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -35,7 +35,7 @@ interface IWindow extends Window { snapshots: eventWithTime[]; } type ExtraOptions = { - assetCaptureConfig?: recordOptions['assetCaptureConfig']; + assetCapture?: recordOptions['assetCapture']; }; const BASE64_PNG_RECTANGLE = @@ -53,7 +53,7 @@ async function injectRecordScript( (window as unknown as IWindow).snapshots = []; const { record, pack } = (window as unknown as IWindow).rrweb; const config: recordOptions = { - assetCaptureConfig: options.assetCaptureConfig, + assetCapture: options.assetCapture, emit(event) { (window as unknown as IWindow).snapshots.push(event); (window as unknown as IWindow).emit(event); @@ -105,11 +105,11 @@ const setup = function ( ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); if ( - options?.assetCaptureConfig?.captureOrigins && - Array.isArray(options.assetCaptureConfig.captureOrigins) + options?.assetCapture?.captureOrigins && + Array.isArray(options.assetCapture.captureOrigins) ) { - options.assetCaptureConfig.captureOrigins = - options.assetCaptureConfig.captureOrigins.map((origin) => + options.assetCapture.captureOrigins = + options.assetCapture.captureOrigins.map((origin) => origin.replace(/\{SERVER_URL\}/g, ctx.serverURL), ); } @@ -142,7 +142,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCaptureConfig: { + assetCapture: { captureObjectURLs: true, captureOrigins: false, }, @@ -284,7 +284,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCaptureConfig: { + assetCapture: { captureObjectURLs: true, captureOrigins: false, }, @@ -329,7 +329,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCaptureConfig: { + assetCapture: { captureObjectURLs: false, captureOrigins: false, }, @@ -409,7 +409,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCaptureConfig: { + assetCapture: { captureOrigins: false, captureObjectURLs: false, }, @@ -441,7 +441,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCaptureConfig: { + assetCapture: { captureOrigins: [], captureObjectURLs: false, }, @@ -473,7 +473,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCaptureConfig: { + assetCapture: { captureOrigins: true, captureObjectURLs: false, }, @@ -509,7 +509,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCaptureConfig: { + assetCapture: { captureOrigins: ['{SERVER_URL}'], captureObjectURLs: false, }, From 171236d72c8c00d90fe283526b8ae915bceb5e68 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 9 Jun 2023 15:28:09 +0200 Subject: [PATCH 006/197] Document `assetCapture` config --- docs/recipes/assets.md | 40 +++++++++++++++++++++++++++ guide.md | 61 +++++++++++++++++++++--------------------- 2 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 docs/recipes/assets.md diff --git a/docs/recipes/assets.md b/docs/recipes/assets.md new file mode 100644 index 0000000000..c6b08a2b01 --- /dev/null +++ b/docs/recipes/assets.md @@ -0,0 +1,40 @@ +# Asset Capture Methods & Configuration in rrweb + +[rrweb](https://rrweb.io/) is a JavaScript library that allows you to record and replay user interactions on your website. It provides various configuration options for capturing assets (such as images) during the recording process. In this document, we will explore the different asset capture methods and their configuration options in rrweb. + +## Inline Images (Deprecated) + +The `inlineImages` configuration option is deprecated and should not be used anymore. It has some issues, namely rewriting events that are already emitted which might make you miss the inlined image if the event has already been sent to the server. Instead, use the `assetCapture` option to configure asset capture. + +## Asset Capture Configuration + +The `assetCapture` configuration option allows you to customize the asset capture process. It is an object with the following properties: + +- `captureObjectURLs` (default: `true`): This property specifies whether to capture same-origin `blob:` assets using object URLs. Object URLs are created using the `URL.createObjectURL()` method. Setting `captureObjectURLs` to `true` enables the capture of object URLs. + +- `captureOrigins` (default: `false`): This property determines which origins to capture assets from. It can have the following values: + - `false` or `[]`: Disables capturing any assets apart from object URLs. + - `true`: Captures assets from all origins. + - `[origin1, origin2, ...]`: Captures assets only from the specified origins. For example, `captureOrigins: ['https://s3.example.com/']` captures all assets from the origin `https://s3.example.com/`. + +## TypeScript Type Definition + +Here is the TypeScript type definition for the `recordOptions` object, which includes the asset capture configuration options: + +```typescript +export type recordOptions = { + // Other configuration options... + inlineImages?: boolean; + assetCapture?: { + captureObjectURLs: boolean; + captureOrigins: string[] | true | false; + }; + // Other configuration options... +}; +``` + +This type definition shows that `assetCapture` is an optional property of the `recordOptions` object. It contains the `captureObjectURLs` and `captureOrigins` properties, which have the same meanings as described above. + +## Conclusion + +By configuring the `assetCapture` option in rrweb, you can control how assets like images are captured during the recording process. This allows you to customize which assets are included in the recorded interactions on your website. diff --git a/guide.md b/guide.md index 764e359fb4..4641e078ee 100644 --- a/guide.md +++ b/guide.md @@ -131,36 +131,37 @@ setInterval(save, 10 * 1000); The parameter of `rrweb.record` accepts the following options. -| key | default | description | -| ------------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| emit | required | the callback function to get emitted events | -| checkoutEveryNth | - | take a full snapshot after every N events
refer to the [checkout](#checkout) chapter | -| checkoutEveryNms | - | take a full snapshot after every N ms
refer to the [checkout](#checkout) chapter | -| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | -| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | -| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | -| ignoreSelector | null | Use a string to configure which selector should be ignored, refer to the [privacy](#privacy) chapter | -| ignoreCSSAttributes | null | array of CSS attributes that should be ignored | -| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | -| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | -| maskAllInputs | false | mask all input content as \* | -| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | -| maskInputFn | - | customize mask input content recording logic | -| maskTextFn | - | customize mask text content recording logic | -| slimDOMOptions | {} | remove unnecessary parts of the DOM
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) | -| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data | -| inlineStylesheet | true | whether to inline the stylesheet in the events | -| hooks | {} | hooks for events
refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | -| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | -| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | -| recordCanvas | false | Whether to record the canvas element. Available options:
`false`,
`true` | -| recordCrossOriginIframes | false | Whether to record cross origin iframes. rrweb has to be injected in each child iframe for this to work. Available options:
`false`,
`true` | -| recordAfter | 'load' | If the document is not ready, then the recorder will start recording after the specified event is fired. Available options: `DOMContentLoaded`, `load` | -| inlineImages | false | whether to record the image content | -| collectFonts | false | whether to collect fonts in the website | -| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) | -| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) | -| errorHandler | - | A callback that is called if something inside of rrweb throws an error. The callback receives the error as argument. | +| key | default | description | +| ------------------------ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| emit | required | the callback function to get emitted events | +| checkoutEveryNth | - | take a full snapshot after every N events
refer to the [checkout](#checkout) chapter | +| checkoutEveryNms | - | take a full snapshot after every N ms
refer to the [checkout](#checkout) chapter | +| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | +| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | +| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | +| ignoreSelector | null | Use a string to configure which selector should be ignored, refer to the [privacy](#privacy) chapter | +| ignoreCSSAttributes | null | array of CSS attributes that should be ignored | +| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | +| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | +| maskAllInputs | false | mask all input content as \* | +| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | +| maskInputFn | - | customize mask input content recording logic | +| maskTextFn | - | customize mask text content recording logic | +| slimDOMOptions | {} | remove unnecessary parts of the DOM
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) | +| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data | +| inlineStylesheet | true | whether to inline the stylesheet in the events | +| hooks | {} | hooks for events
refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | +| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | +| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | +| recordCanvas | false | Whether to record the canvas element. Available options:
`false`,
`true` | +| recordCrossOriginIframes | false | Whether to record cross origin iframes. rrweb has to be injected in each child iframe for this to work. Available options:
`false`,
`true` | +| recordAfter | 'load' | If the document is not ready, then the recorder will start recording after the specified event is fired. Available options: `DOMContentLoaded`, `load` | +| inlineImages | false | whether to record the image content (deprecated, use `assetCapture` instead) | +| assetCapture | { captureObjectURLs: true, captureOrigins: false } | Configure the asset (image) capture and generates async asset events.
Refer to the [asset capture documentation](./docs/recipes/assets.md) for more info. | +| collectFonts | false | whether to collect fonts in the website | +| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) | +| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) | +| errorHandler | - | A callback that is called if something inside of rrweb throws an error. The callback receives the error as argument. | #### Privacy From 7432ee6b22342213c800acd116c6063be50ed214 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 13 Jun 2023 15:39:26 +0200 Subject: [PATCH 007/197] Create asset event for assets that failed to load --- .../src/record/observers/asset-manager.ts | 18 ++++- packages/rrweb/test/integration.test.ts | 17 +++-- packages/rrweb/test/record/asset.test.ts | 72 ++++++++++++++++++- packages/rrweb/test/utils.ts | 3 +- packages/types/src/index.ts | 21 +++--- 5 files changed, 115 insertions(+), 16 deletions(-) diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts index 595aa3665f..4249ef014d 100644 --- a/packages/rrweb/src/record/observers/asset-manager.ts +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -169,8 +169,22 @@ export default class AssetManager { } } }) - .catch(() => { - // TODO: add mutationCb for failed urls + .catch((e: unknown) => { + let message = ''; + if (e instanceof Error) { + message = e.message; + } else if (typeof e === 'string') { + message = e; + } else if (e && typeof e === 'object' && 'toString' in e) { + message = (e as { toString(): string }).toString(); + } + this.mutationCb({ + url, + failed: { + message, + }, + }); + this.failedURLs.add(url); this.capturingURLs.delete(url); }); diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 89534c99c7..393b82e8bb 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -73,7 +73,7 @@ describe('record integration tests', function (this: ISuite) { x: Math.round(x + width / 2), y: Math.round(y + height / 2), }; - }, span); + }, span!); await page.touchscreen.tap(center.x, center.y); await page.click('a'); @@ -920,7 +920,10 @@ describe('record integration tests', function (this: ISuite) { page.on('console', (msg) => console.log(msg.text())); await page.goto(`${serverURL}/html`); page.setContent( - getHtml.call(this, 'image-blob-url.html', { inlineImages: true }), + getHtml.call(this, 'image-blob-url.html', { + inlineImages: true, + assetCapture: { captureObjectURLs: false, captureOrigins: false }, + }), ); await page.waitForResponse(`${serverURL}/html/assets/robot.png`); await page.waitForSelector('img'); // wait for image to get added @@ -937,7 +940,10 @@ describe('record integration tests', function (this: ISuite) { page.on('console', (msg) => console.log(msg.text())); await page.goto(`${serverURL}/html`); await page.setContent( - getHtml.call(this, 'frame-image-blob-url.html', { inlineImages: true }), + getHtml.call(this, 'frame-image-blob-url.html', { + inlineImages: true, + assetCapture: { captureObjectURLs: false, captureOrigins: false }, + }), ); await page.waitForResponse(`${serverURL}/html/assets/robot.png`); await page.waitForTimeout(50); // wait for image to get added @@ -954,7 +960,10 @@ describe('record integration tests', function (this: ISuite) { page.on('console', (msg) => console.log(msg.text())); await page.goto(`${serverURL}/html`); await page.setContent( - getHtml.call(this, 'frame2.html', { inlineImages: true }), + getHtml.call(this, 'frame2.html', { + inlineImages: true, + assetCapture: { captureObjectURLs: false, captureOrigins: false }, + }), ); await page.waitForSelector('iframe'); // wait for iframe to get added await waitForRAF(page); // wait for iframe to load diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index a34db4e86f..dafd7648d0 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -94,7 +94,6 @@ const setup = function ( ); // await ctx.page.evaluate(ctx.code); await waitForRAF(ctx.page); - await ctx.page.waitForTimeout(500); // FIXME!! ctx.events = []; await ctx.page.exposeFunction('emit', (e: eventWithTime) => { if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) { @@ -496,6 +495,77 @@ describe('asset caching', function (this: ISuite) { ); }); }); + + describe('captureOrigins: true with invalid urls', () => { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + + `, + { + assetCapture: { + captureOrigins: true, + captureObjectURLs: false, + }, + }, + ); + + it('capture invalid url', async () => { + await waitForRAF(ctx.page); + + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect an event to be emitted with `event.type` === EventType.Asset + expect(events).toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + data: { + url: `failprotocol://example.com/image.png`, + failed: { + message: 'Failed to fetch', + }, + }, + }), + ); + }); + + it('capture url failed due to CORS', async () => { + // Puppeteer has issues with failed requests below 19.8.0 (more info: https://github.com/puppeteer/puppeteer/pull/9883) + // TODO: re-enable next line after upgrading to puppeteer 19.8.0 + // await ctx.page.waitForNetworkIdle({ idleTime: 100 }); + + // TODO: remove next line after upgrading to puppeteer 19.8.0 + await ctx.page.waitForTimeout(500); + + await waitForRAF(ctx.page); + + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect an event to be emitted with `event.type` === EventType.Asset + expect(events).toContainEqual( + expect.objectContaining({ + type: EventType.Asset, + data: { + url: `https://example.com/image.png`, + failed: { + message: 'Failed to fetch', + }, + }, + }), + ); + }); + }); + describe('captureOrigins: ["http://localhost:xxxxx/"]', () => { const ctx: ISuite = setup.call( this, diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index d8463591b3..790430a921 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -801,7 +801,8 @@ export function generateRecordSnippet(options: recordOptions) { recordCanvas: ${options.recordCanvas}, recordAfter: '${options.recordAfter || 'load'}', inlineImages: ${options.inlineImages}, - plugins: ${options.plugins} + plugins: ${options.plugins}, + assetCapture: ${JSON.stringify(options.assetCapture)}, }); `; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ef9f7127a6..b46261db14 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -62,10 +62,7 @@ export type pluginEvent = { export type assetEvent = { type: EventType.Asset; - data: { - url: string; - payload: SerializedCanvasArg; - }; + data: assetParam; }; export enum IncrementalSource { @@ -618,10 +615,18 @@ export type customElementParam = { export type customElementCallback = (c: customElementParam) => void; -export type assetParam = { - url: string; - payload: SerializedCanvasArg; -}; +export type assetParam = + | { + url: string; + payload: SerializedCanvasArg; + } + | { + url: string; + failed: { + status?: number; + message: string; + }; + }; export type assetCallback = (d: assetParam) => void; From b8d62313ee645e38d2f1343beea02957dd895ed5 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 14 Jun 2023 00:23:42 +0200 Subject: [PATCH 008/197] Move types from rrweb-snapshot to @rrweb/types --- .changeset/yellow-vans-protect.md | 7 +++++ packages/rrdom/src/diff.ts | 3 ++ packages/rrdom/src/index.ts | 2 ++ packages/rrweb-snapshot/package.json | 3 ++ packages/rrweb-snapshot/src/types.ts | 5 +++ packages/rrweb-snapshot/test/snapshot.test.ts | 12 ++++---- .../src/replay/canvas/deserialize-args.ts | 29 +++++++++++++++--- packages/rrweb/src/replay/canvas/webgl.ts | 12 ++++---- packages/rrweb/src/replay/index.ts | 16 ++++++++++ packages/rrweb/test/record/asset.test.ts | 7 +++-- ...corporate-assets-emitted-later-1-snap.png} | Bin packages/rrweb/test/replay/asset.test.ts | 5 +-- packages/rrweb/test/replay/video.test.ts | 2 +- 13 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 .changeset/yellow-vans-protect.md rename packages/rrweb/test/replay/__image_snapshots__/{asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png => asset-test-ts-test-replay-asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png} (100%) diff --git a/.changeset/yellow-vans-protect.md b/.changeset/yellow-vans-protect.md new file mode 100644 index 0000000000..dacf876bc3 --- /dev/null +++ b/.changeset/yellow-vans-protect.md @@ -0,0 +1,7 @@ +--- +"rrweb-snapshot": major +"@rrweb/types": patch +--- + +`NodeType` enum was moved from rrweb-snapshot to @rrweb/types +The following types where moved from rrweb-snapshot to @rrweb/types: `documentNode`, `documentTypeNode`, `attributes`, `legacyAttributes`, `elementNode`, `textNode`, `cdataNode`, `commentNode`, `serializedNode`, `serializedNodeWithId` and `DataURLOptions` diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 5d9ba7cea5..7b8c5533d7 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -570,6 +570,9 @@ export function createOrGetNode( case RRNodeType.CDATA: node = document.createCDATASection((rrNode as IRRCDATASection).data); break; + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unknown node type ${rrNode.RRNodeType}`); } if (sn) domMirror.add(node, { ...sn }); diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index e5984e580a..4eafcf5b07 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -453,6 +453,8 @@ export function getDefaultSN(node: IRRNode, id: number): serializedNodeWithId { type: node.RRNodeType, textContent: '', }; + default: + throw new Error(`Unknown node type`); } } diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index e73a8d5ce7..bdab817710 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -53,6 +53,9 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme", + "dependencies": { + "@rrweb/types": "^2.0.0-alpha.14" + }, "devDependencies": { "@rrweb/types": "^2.0.0-alpha.18", "@rrweb/utils": "^2.0.0-alpha.18", diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index 83b50e266f..bb1b94eced 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -21,6 +21,11 @@ export type DialogAttributes = { // rr_open_mode_index?: number; }; +// @deprecated +export interface INode extends Node { + __sn: serializedNodeWithId; +} + export interface ICanvas extends HTMLCanvasElement { __context: string; } diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index 97f731669f..f68467abf7 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -2,13 +2,13 @@ * @vitest-environment jsdom */ import { JSDOM } from 'jsdom'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import snapshot, { _isBlockedElement, serializeNodeWithId, } from '../src/snapshot'; -import { elementNode, serializedNodeWithId } from '../src/types'; +import type { serializedNodeWithId, elementNode } from '@rrweb/types'; import { Mirror, absolutifyURLs } from '../src/utils'; const serializeNode = (node: Node): serializedNodeWithId | null => { @@ -287,7 +287,7 @@ describe('onAssetDetected callback', () => {
`); - const callback = jest.fn(); + const callback = vi.fn(); serializeNode(el, callback); expect(callback).toHaveBeenCalledWith({ urls: ['https://example.com/image.png'], @@ -299,7 +299,7 @@ describe('onAssetDetected callback', () => { `); - const callback = jest.fn(); + const callback = vi.fn(); serializeNode(el, callback); expect(callback).toHaveBeenCalledWith({ urls: ['blob:https://example.com/e81acc2b-f460-4aec-91b3-ce9732b837c4'], @@ -310,7 +310,7 @@ describe('onAssetDetected callback', () => { `); - const callback = jest.fn(); + const callback = vi.fn(); serializeNode(el, callback); expect(callback).toHaveBeenCalledWith({ urls: [ @@ -326,7 +326,7 @@ describe('onAssetDetected callback', () => { `); - const callback = jest.fn(); + const callback = vi.fn(); serializeNode(el, callback); expect(callback).toBeCalledTimes(2); expect(callback).toHaveBeenCalledWith({ diff --git a/packages/rrweb/src/replay/canvas/deserialize-args.ts b/packages/rrweb/src/replay/canvas/deserialize-args.ts index a690d7986f..7f5d8f98be 100644 --- a/packages/rrweb/src/replay/canvas/deserialize-args.ts +++ b/packages/rrweb/src/replay/canvas/deserialize-args.ts @@ -31,6 +31,21 @@ export function isSerializedArg(arg: unknown): arg is SerializedCanvasArg { return Boolean(arg && typeof arg === 'object' && 'rr_type' in arg); } +type deserializeArgOutput = + | ImageBitmap + | ArrayBuffer + | HTMLImageElement + | Blob + | { + rr_type: string; + index: number; + } + | string + | number + | boolean + | null + | deserializeArgOutput[]; + export function deserializeArg( imageMap: Replayer['imageMap'], ctx: @@ -41,13 +56,17 @@ export function deserializeArg( preload?: { isUnchanged: boolean; }, -): (arg: CanvasArg) => Promise { - return async (arg: CanvasArg): Promise => { +): (arg: CanvasArg) => Promise { + return async (arg: CanvasArg): Promise => { if (arg && typeof arg === 'object' && 'rr_type' in arg) { if (preload) preload.isUnchanged = false; if (arg.rr_type === 'ImageBitmap' && 'args' in arg) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const args = await deserializeArg(imageMap, ctx, preload)(arg.args); + const args = (await deserializeArg( + imageMap, + ctx, + preload, + )(arg.args)) as unknown as Parameters; // eslint-disable-next-line prefer-spread return await createImageBitmap.apply(null, args); } else if ('index' in arg) { @@ -79,9 +98,9 @@ export function deserializeArg( return image; } } else if ('data' in arg && arg.rr_type === 'Blob') { - const blobContents = await Promise.all( + const blobContents = (await Promise.all( arg.data.map(deserializeArg(imageMap, ctx, preload)), - ); + )) as BlobPart[]; const blob = new Blob(blobContents, { type: arg.type, }); diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts index b4faf4c89e..973f8fa497 100644 --- a/packages/rrweb/src/replay/canvas/webgl.ts +++ b/packages/rrweb/src/replay/canvas/webgl.ts @@ -88,7 +88,7 @@ export default async function webglMutation({ const args = await Promise.all( mutation.args.map(deserializeArg(imageMap, ctx)), ); - const result = original.apply(ctx, args); + const result = original.apply(ctx, args as unknown[]); saveToWebGLVarMap(ctx, result); // Slows down replay considerably, only use for debugging @@ -96,21 +96,21 @@ export default async function webglMutation({ if (debugMode) { if (mutation.property === 'compileShader') { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - if (!ctx.getShaderParameter(args[0], ctx.COMPILE_STATUS)) + if (!ctx.getShaderParameter(args[0] as WebGLShader, ctx.COMPILE_STATUS)) console.warn( 'something went wrong in replay', // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ctx.getShaderInfoLog(args[0]), + ctx.getShaderInfoLog(args[0] as WebGLShader), ); } else if (mutation.property === 'linkProgram') { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ctx.validateProgram(args[0]); + ctx.validateProgram(args[0] as WebGLProgram); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - if (!ctx.getProgramParameter(args[0], ctx.LINK_STATUS)) + if (!ctx.getProgramParameter(args[0] as WebGLProgram, ctx.LINK_STATUS)) console.warn( 'something went wrong in replay', // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ctx.getProgramInfoLog(args[0]), + ctx.getProgramInfoLog(args[0] as WebGLProgram), ); } const webglError = ctx.getError(); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 5f1101cc23..3d97a4751f 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -88,6 +88,7 @@ import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; +// import AssetManager from './assets'; import { MediaManager } from './media'; import { applyDialogToTopLevel, removeDialogFromTopLevel } from './dialog'; @@ -868,6 +869,7 @@ export class Replayer { if (this.config.UNSAFE_replayCanvas) { void this.preloadAllImages(); } + void this.preloadAllAssets(); } private insertStyleRules( @@ -1043,6 +1045,20 @@ export class Replayer { } } + /** + * Process all asset events and preload them + */ + private async preloadAllAssets(): Promise { + // const assetManager = new AssetManager(); + const promises: Promise[] = []; + for (const event of this.service.state.context.events) { + if (event.type === EventType.Asset) { + // promises.push(assetManager.add(event)); + } + } + return Promise.all(promises); + } + /** * pause when there are some canvas drawImage args need to be loaded */ diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index dafd7648d0..2de6f4789a 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -11,6 +11,7 @@ import { waitForRAF, } from '../utils'; import type * as http from 'http'; +import { vi } from 'vitest'; interface ISuite { code: string; @@ -46,7 +47,7 @@ async function injectRecordScript( options?: ExtraOptions, ) { await frame.addScriptTag({ - path: path.resolve(__dirname, '../../dist/rrweb-all.js'), + path: path.resolve(__dirname, '../../dist/rrweb.umd.cjs'), }); options = options || {}; await frame.evaluate((options) => { @@ -80,7 +81,7 @@ const setup = function ( ctx.serverB = await startServer(); ctx.serverBURL = getServerURL(ctx.serverB); - const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js'); + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.umd.cjs'); ctx.code = fs.readFileSync(bundlePath, 'utf8'); }); @@ -129,7 +130,7 @@ const setup = function ( }; describe('asset caching', function (this: ISuite) { - jest.setTimeout(100_000); + vi.setConfig({ testTimeout: 100_000 }); describe('captureObjectURLs: true with incremental snapshots', function (this: ISuite) { const ctx: ISuite = setup.call( diff --git a/packages/rrweb/test/replay/__image_snapshots__/asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/asset-test-ts-test-replay-asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png similarity index 100% rename from packages/rrweb/test/replay/__image_snapshots__/asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png rename to packages/rrweb/test/replay/__image_snapshots__/asset-test-ts-test-replay-asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png diff --git a/packages/rrweb/test/replay/asset.test.ts b/packages/rrweb/test/replay/asset.test.ts index ba44cdda20..1db790bb0b 100644 --- a/packages/rrweb/test/replay/asset.test.ts +++ b/packages/rrweb/test/replay/asset.test.ts @@ -4,6 +4,7 @@ import { launchPuppeteer } from '../utils'; import { toMatchImageSnapshot } from 'jest-image-snapshot'; import type * as puppeteer from 'puppeteer'; import events from '../events/assets'; +import { vi } from 'vitest'; interface ISuite { code: string; @@ -14,7 +15,7 @@ interface ISuite { expect.extend({ toMatchImageSnapshot }); describe('replayer', function () { - jest.setTimeout(10_000); + vi.setConfig({ testTimeout: 10_000 }); let code: ISuite['code']; let browser: ISuite['browser']; @@ -23,7 +24,7 @@ describe('replayer', function () { beforeAll(async () => { browser = await launchPuppeteer(); - const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js'); + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.umd.cjs'); code = fs.readFileSync(bundlePath, 'utf8'); }); diff --git a/packages/rrweb/test/replay/video.test.ts b/packages/rrweb/test/replay/video.test.ts index b23996f350..12824e7e2b 100644 --- a/packages/rrweb/test/replay/video.test.ts +++ b/packages/rrweb/test/replay/video.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import type * as puppeteer from 'puppeteer'; +import * as puppeteer from 'puppeteer'; import { startServer, launchPuppeteer, From 8338c532037210b4a09e70a63ebd729a677d4175 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 14 Jun 2023 00:30:52 +0200 Subject: [PATCH 009/197] WIP asset manager for replay --- packages/rrweb-snapshot/src/rebuild.ts | 10 ++ packages/rrweb/src/replay/assets/index.ts | 65 +++++++++++ packages/rrweb/src/replay/index.ts | 6 +- ...ncorporate-assets-emitted-later-1-snap.png | Bin 0 -> 10796 bytes ...sset.test.ts => asset-integration.test.ts} | 0 packages/rrweb/test/replay/asset-unit.test.ts | 101 ++++++++++++++++++ packages/rrweb/test/replay/video.test.ts | 1 + packages/types/src/index.ts | 24 ++++- 8 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 packages/rrweb/src/replay/assets/index.ts create mode 100644 packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png rename packages/rrweb/test/replay/{asset.test.ts => asset-integration.test.ts} (100%) create mode 100644 packages/rrweb/test/replay/asset-unit.test.ts diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 692da2d281..735fdc0df0 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -1,5 +1,6 @@ import { mediaSelectorPlugin, pseudoClassPlugin } from './css'; import { + type RebuildAssetManagerInterface, type serializedNodeWithId, type serializedElementNodeWithId, NodeType, @@ -195,6 +196,7 @@ function buildNode( doc: Document; hackCss: boolean; cache: BuildCache; + assetManager?: RebuildAssetManagerInterface; }, ): Node | null { const { doc, hackCss, cache } = options; @@ -325,6 +327,13 @@ function buildNode( 'rrweb-original-srcset', n.attributes.srcset as string, ); + } else if ( + tagName === 'img' && + n.attributes.src && + options.assetManager + ) { + // TODO: do something with the asset manager + console.log('WIP! Please implement me!'); } else { node.setAttribute(name, value.toString()); } @@ -453,6 +462,7 @@ export function buildNodeWithSN( */ afterAppend?: (n: Node, id: number) => unknown; cache: BuildCache; + assetManager?: RebuildAssetManagerInterface; }, ): Node | null { const { diff --git a/packages/rrweb/src/replay/assets/index.ts b/packages/rrweb/src/replay/assets/index.ts new file mode 100644 index 0000000000..5d898d2b89 --- /dev/null +++ b/packages/rrweb/src/replay/assets/index.ts @@ -0,0 +1,65 @@ +import type { RebuildAssetManagerInterface, assetEvent } from '@rrweb/types'; +import { deserializeArg } from '../canvas/deserialize-args'; + +export default class AssetManager implements RebuildAssetManagerInterface { + private originalToObjectURLMap: Map = new Map(); + private loadingURLs: Set = new Set(); + private failedURLs: Set = new Set(); + + public async add(event: assetEvent) { + const { data } = event; + const { url, payload, failed } = { payload: false, failed: false, ...data }; + if (failed) { + this.failedURLs.add(url); + return; + } + this.loadingURLs.add(url); + + // tracks if deserializing did anything, not really needed for AssetManager + const status = { + isUnchanged: true, + }; + + // TODO: extract the logic only needed for assets from deserializeArg + const result = (await deserializeArg(new Map(), null, status)(payload)) as + | Blob + | MediaSource; + + const objectURL = URL.createObjectURL(result); + this.originalToObjectURLMap.set(url, objectURL); + this.loadingURLs.delete(url); + } + + public get( + url: string, + ): + | { status: 'loading' } + | { status: 'loaded'; url: string } + | { status: 'failed' } + | { status: 'unknown' } { + const result = this.originalToObjectURLMap.get(url); + + if (result) { + return { + status: 'loaded', + url: result, + }; + } + + if (this.loadingURLs.has(url)) { + return { + status: 'loading', + }; + } + + if (this.failedURLs.has(url)) { + return { + status: 'failed', + }; + } + + return { + status: 'unknown', + }; + } +} diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 3d97a4751f..3b00d7d0b0 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -88,7 +88,7 @@ import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; -// import AssetManager from './assets'; +import AssetManager from './assets'; import { MediaManager } from './media'; import { applyDialogToTopLevel, removeDialogFromTopLevel } from './dialog'; @@ -1049,11 +1049,11 @@ export class Replayer { * Process all asset events and preload them */ private async preloadAllAssets(): Promise { - // const assetManager = new AssetManager(); + const assetManager = new AssetManager(); const promises: Promise[] = []; for (const event of this.service.state.context.events) { if (event.type === EventType.Asset) { - // promises.push(assetManager.add(event)); + promises.push(assetManager.add(event)); } } return Promise.all(promises); diff --git a/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbd91056eb79bc92f28acef03c14da255a83e26 GIT binary patch literal 10796 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4F%}28J29*~C-ahlL4m>3 z#WAE}&YL?MbD0fASRLmr%(~I^Dlk+lNcF`n@gyqn}zyo0kU{f|dUH;Dd@8NGCN-U1fDUMlxeRDcP z&O=s+0Ux$Ko0EL5@*7V>?!0{r|B80stt(~+t3GhrR<3`uIz!G=s8R+3cHwnWOqr}z zU-ETLv#9L3woso{Ow47h=rLS!Y`^Oyh@B1JABkUpxRrreFXFY9dnetkI-2nv@Vp30ti;nnFfX z$Y=@y7onrABycc{Hdw&HFj}92gMpHc%4m-P6ojJ*X*3~?_5#6aU^Fj4f?>2!g@nUs ziwhhMv}$q9e|z^JC$pgdizBzp*=O^ww<@$3ust+33C*{Cc3-DUpaD9Hpu~~@iH`;j zMhOT@!HEGh2nz%qqsld$O7|k-^a2S}vVF&Ml?6vA;keFa7I6;j` z6Q=s;qs1yPC`L;WNO~A8MZn=OFr~<7y9N~aL$6)4e`D<~1_sVs zptS%A;Pf+Eo{To+hhj<|B&qk&-?NKnR>ofc`dSx~ofu?x>}8lY^ZFT^jjYfN#+?0r z?|)k+D6jb6Xfp;F6r;@;NO~A;#(=|Nv>5{qhN0J@NPr~c(d-NfhXKyc9sG^SYMU-F R%;x|($J5o%Wt~$(696>PBJ= { + let assetManager: AssetManager; + let useURLPolyfill = false; + const examplePayload: SerializedBlobArg = { + rr_type: 'Blob', + type: 'image/png', + data: [ + { + rr_type: 'ArrayBuffer', + base64: 'fake-base64-abcd', + }, + ], + }; + + beforeAll(() => { + // https://github.com/jsdom/jsdom/issues/1721 + if (typeof window.URL.createObjectURL === 'undefined') { + useURLPolyfill = true; + window.URL.createObjectURL = () => ''; + } + }); + + beforeEach(() => { + assetManager = new AssetManager(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + afterAll(() => { + if (useURLPolyfill) { + delete (window.URL as any).createObjectURL; + } + }); + + it('should add an asset to the manager', async () => { + const url = 'https://example.com/image.png'; + + const event: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: examplePayload, + }, + }; + const createObjectURLSpy = vi + .spyOn(URL, 'createObjectURL') + .mockReturnValue('objectURL'); + + await assetManager.add(event); + + expect(createObjectURLSpy).toHaveBeenCalledWith(expect.any(Blob)); + expect(assetManager.get(url)).toEqual({ + status: 'loaded', + url: 'objectURL', + }); + }); + + it('should not add a failed asset to the manager', async () => { + const url = 'https://example.com/image.png'; + const event: assetEvent = { + type: EventType.Asset, + data: { url, failed: { message: 'failed to load file' } }, + }; + const createObjectURLSpy = vi.spyOn(URL, 'createObjectURL'); + + await assetManager.add(event); + + expect(createObjectURLSpy).not.toHaveBeenCalled(); + expect(assetManager.get(url)).toEqual({ status: 'failed' }); + }); + + it('should return the correct status for a loading asset', () => { + const url = 'https://example.com/image.png'; + const event: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: examplePayload, + }, + }; + void assetManager.add(event); + + expect(assetManager.get(url)).toEqual({ status: 'loading' }); + }); + + it('should return the correct status for an unknown asset', () => { + const url = 'https://example.com/image.png'; + + expect(assetManager.get(url)).toEqual({ status: 'unknown' }); + }); +}); diff --git a/packages/rrweb/test/replay/video.test.ts b/packages/rrweb/test/replay/video.test.ts index 12824e7e2b..27e06debe5 100644 --- a/packages/rrweb/test/replay/video.test.ts +++ b/packages/rrweb/test/replay/video.test.ts @@ -15,6 +15,7 @@ import { vi } from 'vitest'; import { Replayer } from '../../src/replay'; import videoPlaybackEvents from '../events/video-playback'; import videoPlaybackOnFullSnapshotEvents from '../events/video-playback-on-full-snapshot'; +import type * as puppeteer from 'puppeteer'; expect.extend({ toMatchImageSnapshot }); type IWindow = typeof globalThis & Window & { replayer: Replayer }; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b46261db14..dd49cbae8c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -389,16 +389,18 @@ export enum CanvasContext { WebGL2, } +export type SerializedBlobArg = { + rr_type: 'Blob'; + data: Array; + type?: string; +}; + export type SerializedCanvasArg = | { rr_type: 'ArrayBuffer'; base64: string; // base64 } - | { - rr_type: 'Blob'; - data: Array; - type?: string; - } + | SerializedBlobArg | { rr_type: string; src: string; // url of image @@ -727,6 +729,18 @@ export type TakeTypedKeyValues = Pick< TakeTypeHelper[keyof TakeTypeHelper] >; +export abstract class RebuildAssetManagerInterface { + abstract add(event: assetEvent): Promise; + + abstract get( + url: string, + ): + | { status: 'loading' } + | { status: 'loaded'; url: string } + | { status: 'failed' } + | { status: 'unknown' }; +} + export enum NodeType { Document, DocumentType, From e78804c9388ed243705236e2dae5e9127a6ad216 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 14:29:15 +0100 Subject: [PATCH 010/197] Add placeholder tests --- ...corporate-assets-streamed-later-1-snap.png | Bin 0 -> 10796 bytes .../test/replay/asset-integration.test.ts | 28 +++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png diff --git a/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbd91056eb79bc92f28acef03c14da255a83e26 GIT binary patch literal 10796 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4F%}28J29*~C-ahlL4m>3 z#WAE}&YL?MbD0fASRLmr%(~I^Dlk+lNcF`n@gyqn}zyo0kU{f|dUH;Dd@8NGCN-U1fDUMlxeRDcP z&O=s+0Ux$Ko0EL5@*7V>?!0{r|B80stt(~+t3GhrR<3`uIz!G=s8R+3cHwnWOqr}z zU-ETLv#9L3woso{Ow47h=rLS!Y`^Oyh@B1JABkUpxRrreFXFY9dnetkI-2nv@Vp30ti;nnFfX z$Y=@y7onrABycc{Hdw&HFj}92gMpHc%4m-P6ojJ*X*3~?_5#6aU^Fj4f?>2!g@nUs ziwhhMv}$q9e|z^JC$pgdizBzp*=O^ww<@$3ust+33C*{Cc3-DUpaD9Hpu~~@iH`;j zMhOT@!HEGh2nz%qqsld$O7|k-^a2S}vVF&Ml?6vA;keFa7I6;j` z6Q=s;qs1yPC`L;WNO~A8MZn=OFr~<7y9N~aL$6)4e`D<~1_sVs zptS%A;Pf+Eo{To+hhj<|B&qk&-?NKnR>ofc`dSx~ofu?x>}8lY^ZFT^jjYfN#+?0r z?|)k+D6jb6Xfp;F6r;@;NO~A;#(=|Nv>5{qhN0J@NPr~c(d-NfhXKyc9sG^SYMU-F R%;x|($J5o%Wt~$(696>PBJ= { + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer([], { + liveMode: true, + }); + replayer.startLive(); + window.replayer.addEvent(events[0]); + window.replayer.addEvent(events[1]); + `); + + await waitForRAF(page); + + await page.evaluate(` + window.replayer.addEvent(events[2]); + `); + + await waitForRAF(page); + + const image = await page.screenshot(); + expect(image).toMatchImageSnapshot(); + }); + + test.todo('should support video'); }); }); From fd512fe72be3eab6f8ef576214578fd46200479b Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 15:44:50 +0100 Subject: [PATCH 011/197] Fix image source attribute in rebuild function --- packages/rrweb-snapshot/src/rebuild.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 735fdc0df0..cd1c821d50 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -327,9 +327,10 @@ function buildNode( 'rrweb-original-srcset', n.attributes.srcset as string, ); + continue; } else if ( tagName === 'img' && - n.attributes.src && + name === 'src' && options.assetManager ) { // TODO: do something with the asset manager From 6616f592f07bb939199d1d9cf16d912ab0646ad6 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 17:52:35 +0100 Subject: [PATCH 012/197] Update path in tsconfig.json --- packages/rrdom-nodejs/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrdom-nodejs/tsconfig.json b/packages/rrdom-nodejs/tsconfig.json index 8f1d20380a..4da375e447 100644 --- a/packages/rrdom-nodejs/tsconfig.json +++ b/packages/rrdom-nodejs/tsconfig.json @@ -10,7 +10,7 @@ "path": "../rrdom" }, { - "path": "../types" + "path": "../rrweb-snapshot" } ] } From 32181e52e88f4dfa2bbda554f412ea2e55ca9de8 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 17:55:50 +0100 Subject: [PATCH 013/197] Fix asset loading in Replayer --- packages/rrweb-snapshot/src/rebuild.ts | 25 ++++++- packages/rrweb/src/replay/assets/index.ts | 70 +++++++++++++++++-- packages/rrweb/src/replay/index.ts | 15 +++- packages/rrweb/test/replay/asset-unit.test.ts | 43 ++++++++++++ packages/types/src/index.ts | 27 ++++--- 5 files changed, 158 insertions(+), 22 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index cd1c821d50..870c33cadd 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -333,8 +333,22 @@ function buildNode( name === 'src' && options.assetManager ) { - // TODO: do something with the asset manager - console.log('WIP! Please implement me!'); + const originalValue = value.toString(); + node.setAttribute(name, originalValue); + void options.assetManager + .whenReady(value.toString()) + .then((status) => { + if ( + status.status === 'loaded' && + node.getAttribute('src') === originalValue + ) { + node.setAttribute(name, status.url); + } else { + console.log( + `failed to load asset: ${originalValue}, ${status.status}`, + ); + } + }); } else { node.setAttribute(name, value.toString()); } @@ -473,6 +487,7 @@ export function buildNodeWithSN( hackCss = true, afterAppend, cache, + assetManager, } = options; /** * Add a check to see if the node is already in the mirror. If it is, we can skip the whole process. @@ -487,7 +502,7 @@ export function buildNodeWithSN( // For safety concern, check if the node in mirror is the same as the node we are trying to build if (isNodeMetaEqual(meta, n)) return mirror.getNode(n.id); } - let node = buildNode(n, { doc, hackCss, cache }); + let node = buildNode(n, { doc, hackCss, cache, assetManager }); if (!node) { return null; } @@ -539,6 +554,7 @@ export function buildNodeWithSN( hackCss, afterAppend, cache, + assetManager, }); if (!childNode) { console.warn('Failed to rebuild', childN); @@ -628,6 +644,7 @@ function rebuild( afterAppend?: (n: Node, id: number) => unknown; cache: BuildCache; mirror: Mirror; + assetManager?: RebuildAssetManagerInterface; }, ): Node | null { const { @@ -637,6 +654,7 @@ function rebuild( afterAppend, cache, mirror = new Mirror(), + assetManager, } = options; const node = buildNodeWithSN(n, { doc, @@ -645,6 +663,7 @@ function rebuild( hackCss, afterAppend, cache, + assetManager, }); visit(mirror, (visitedNode) => { if (onVisit) { diff --git a/packages/rrweb/src/replay/assets/index.ts b/packages/rrweb/src/replay/assets/index.ts index 5d898d2b89..27391371cb 100644 --- a/packages/rrweb/src/replay/assets/index.ts +++ b/packages/rrweb/src/replay/assets/index.ts @@ -1,16 +1,26 @@ -import type { RebuildAssetManagerInterface, assetEvent } from '@rrweb/types'; +import type { + RebuildAssetManagerFinalStatus, + RebuildAssetManagerInterface, + RebuildAssetManagerStatus, + assetEvent, +} from '@rrweb/types'; import { deserializeArg } from '../canvas/deserialize-args'; export default class AssetManager implements RebuildAssetManagerInterface { private originalToObjectURLMap: Map = new Map(); private loadingURLs: Set = new Set(); private failedURLs: Set = new Set(); + private callbackMap: Map< + string, + Array<(status: RebuildAssetManagerFinalStatus) => void> + > = new Map(); public async add(event: assetEvent) { const { data } = event; const { url, payload, failed } = { payload: false, failed: false, ...data }; if (failed) { this.failedURLs.add(url); + this.executeCallbacks(url, { status: 'failed' }); return; } this.loadingURLs.add(url); @@ -28,15 +38,45 @@ export default class AssetManager implements RebuildAssetManagerInterface { const objectURL = URL.createObjectURL(result); this.originalToObjectURLMap.set(url, objectURL); this.loadingURLs.delete(url); + this.executeCallbacks(url, { status: 'loaded', url: objectURL }); } - public get( + private executeCallbacks( url: string, - ): - | { status: 'loading' } - | { status: 'loaded'; url: string } - | { status: 'failed' } - | { status: 'unknown' } { + status: RebuildAssetManagerFinalStatus, + ) { + const callbacks = this.callbackMap.get(url); + while (callbacks && callbacks.length > 0) { + const callback = callbacks.pop(); + if (!callback) { + break; + } + callback(status); + } + } + + public async whenReady(url: string): Promise { + const currentStatus = this.get(url); + if ( + currentStatus.status === 'loaded' || + currentStatus.status === 'failed' + ) { + return currentStatus; + } + let resolve: (status: RebuildAssetManagerFinalStatus) => void; + const promise = new Promise((r) => { + resolve = r; + }); + if (!this.callbackMap.has(url)) { + this.callbackMap.set(url, []); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.callbackMap.get(url)!.push(resolve!); + + return promise; + } + + public get(url: string): RebuildAssetManagerStatus { const result = this.originalToObjectURLMap.get(url); if (result) { @@ -62,4 +102,20 @@ export default class AssetManager implements RebuildAssetManagerInterface { status: 'unknown', }; } + + public reset(): void { + this.originalToObjectURLMap.forEach((objectURL) => { + URL.revokeObjectURL(objectURL); + }); + this.originalToObjectURLMap.clear(); + this.loadingURLs.clear(); + this.failedURLs.clear(); + this.callbackMap.forEach((callbacks) => { + while (callbacks.length > 0) { + const cb = callbacks.pop(); + if (cb) cb({ status: 'reset' }); + } + }); + this.callbackMap.clear(); + } } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 3b00d7d0b0..46c9dc6dc0 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -145,6 +145,9 @@ export class Replayer { private cache: BuildCache = createCache(); private imageMap: Map = new Map(); + + private assetManager = new AssetManager(); + private canvasEventMap: Map = new Map(); private mirror: Mirror = createMirror(); @@ -747,6 +750,11 @@ export class Replayer { } }; break; + case EventType.Asset: + castFn = () => { + void this.assetManager.add(event); + }; + break; default: } const wrappedCastFn = () => { @@ -829,6 +837,8 @@ export class Replayer { } }; + void this.preloadAllAssets(); + /** * Normally rebuilding full snapshot should not be under virtual dom environment. * But if the order of data events has some issues, it might be possible. @@ -845,6 +855,7 @@ export class Replayer { afterAppend, cache: this.cache, mirror: this.mirror, + assetManager: this.assetManager, }); afterAppend(this.iframe.contentDocument, event.data.node.id); @@ -869,7 +880,6 @@ export class Replayer { if (this.config.UNSAFE_replayCanvas) { void this.preloadAllImages(); } - void this.preloadAllAssets(); } private insertStyleRules( @@ -1049,11 +1059,10 @@ export class Replayer { * Process all asset events and preload them */ private async preloadAllAssets(): Promise { - const assetManager = new AssetManager(); const promises: Promise[] = []; for (const event of this.service.state.context.events) { if (event.type === EventType.Asset) { - promises.push(assetManager.add(event)); + promises.push(this.assetManager.add(event)); } } return Promise.all(promises); diff --git a/packages/rrweb/test/replay/asset-unit.test.ts b/packages/rrweb/test/replay/asset-unit.test.ts index 18f954f3ba..0ee0470615 100644 --- a/packages/rrweb/test/replay/asset-unit.test.ts +++ b/packages/rrweb/test/replay/asset-unit.test.ts @@ -34,6 +34,7 @@ describe('AssetManager', () => { afterEach(() => { vi.restoreAllMocks(); + vi.useRealTimers(); }); afterAll(() => { @@ -98,4 +99,46 @@ describe('AssetManager', () => { expect(assetManager.get(url)).toEqual({ status: 'unknown' }); }); + + it('should execute hook when an asset is added', async () => { + jest.useFakeTimers(); + const url = 'https://example.com/image.png'; + const event: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: examplePayload, + }, + }; + void assetManager.add(event); + const promise = assetManager.whenReady(url); + + jest.spyOn(URL, 'createObjectURL').mockReturnValue('objectURL'); + + jest.runAllTimers(); + + await expect(promise).resolves.toEqual({ + status: 'loaded', + url: 'objectURL', + }); + }); + + it('should send status reset to callbacks when reset', async () => { + jest.useFakeTimers(); + const url = 'https://example.com/image.png'; + const event: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: examplePayload, + }, + }; + void assetManager.add(event); + const promise = assetManager.whenReady(url); + + assetManager.reset(); + jest.runAllTimers(); + + await expect(promise).resolves.toEqual({ status: 'reset' }); + }); }); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index dd49cbae8c..7a8d2bad10 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -729,16 +729,25 @@ export type TakeTypedKeyValues = Pick< TakeTypeHelper[keyof TakeTypeHelper] >; -export abstract class RebuildAssetManagerInterface { +export type RebuildAssetManagerResetStatus = { status: 'reset' }; +export type RebuildAssetManagerUnknownStatus = { status: 'unknown' }; +export type RebuildAssetManagerLoadingStatus = { status: 'loading' }; +export type RebuildAssetManagerLoadedStatus = { status: 'loaded'; url: string }; +export type RebuildAssetManagerFailedStatus = { status: 'failed' }; +export type RebuildAssetManagerFinalStatus = + | RebuildAssetManagerLoadedStatus + | RebuildAssetManagerFailedStatus + | RebuildAssetManagerResetStatus; +export type RebuildAssetManagerStatus = + | RebuildAssetManagerUnknownStatus + | RebuildAssetManagerLoadingStatus + | RebuildAssetManagerFinalStatus; + +export declare abstract class RebuildAssetManagerInterface { abstract add(event: assetEvent): Promise; - - abstract get( - url: string, - ): - | { status: 'loading' } - | { status: 'loaded'; url: string } - | { status: 'failed' } - | { status: 'unknown' }; + abstract get(url: string): RebuildAssetManagerStatus; + abstract whenReady(url: string): Promise; + abstract reset(): void; } export enum NodeType { From ae6c6c14518faa6d3320978f2d5db72963f137e6 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 17:56:15 +0100 Subject: [PATCH 014/197] Add todo tests --- packages/rrweb/test/replay/asset-integration.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/test/replay/asset-integration.test.ts b/packages/rrweb/test/replay/asset-integration.test.ts index 121ab735b4..13e479725a 100644 --- a/packages/rrweb/test/replay/asset-integration.test.ts +++ b/packages/rrweb/test/replay/asset-integration.test.ts @@ -63,7 +63,6 @@ describe('replayer', function () { expect(image).toMatchImageSnapshot(); }); - // FIXME: test not finished yet it('should incorporate assets streamed later', async () => { await page.evaluate(` const { Replayer } = rrweb; @@ -87,6 +86,14 @@ describe('replayer', function () { expect(image).toMatchImageSnapshot(); }); - test.todo('should support video'); + test.todo('should support urls src modified via mutation'); + + test.todo('should support video elements'); + test.todo('should support audio elements'); + test.todo('should support embed elements'); + test.todo('should support source elements'); + test.todo('should support track elements'); + test.todo('should support input#type=image elements'); + test.todo('should support img srcset'); }); }); From 6c20cb51806674eb2b829ccb783009c947d77d9f Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 24 Nov 2023 12:31:33 +0100 Subject: [PATCH 015/197] Add asset mutation events for replayer test --- packages/rrdom-nodejs/tsconfig.json | 6 +- packages/rrweb/test/events/assets-mutation.ts | 141 ++++++++++++++++++ ...-urls-src-modified-via-mutation-1-snap.png | Bin 0 -> 10796 bytes .../test/replay/asset-integration.test.ts | 26 +++- 4 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 packages/rrweb/test/events/assets-mutation.ts create mode 100644 packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-mutation-1-snap.png diff --git a/packages/rrdom-nodejs/tsconfig.json b/packages/rrdom-nodejs/tsconfig.json index 4da375e447..05e2c1bb8f 100644 --- a/packages/rrdom-nodejs/tsconfig.json +++ b/packages/rrdom-nodejs/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.base.json", - "include": ["src"], + "include": [ + "src" + ], "compilerOptions": { "rootDir": "src", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" @@ -10,7 +12,7 @@ "path": "../rrdom" }, { - "path": "../rrweb-snapshot" + "path": "../types" } ] } diff --git a/packages/rrweb/test/events/assets-mutation.ts b/packages/rrweb/test/events/assets-mutation.ts new file mode 100644 index 0000000000..b318183900 --- /dev/null +++ b/packages/rrweb/test/events/assets-mutation.ts @@ -0,0 +1,141 @@ +import { EventType, IncrementalSource, type eventWithTime } from '@rrweb/types'; + +const events: eventWithTime[] = [ + { + type: 4, + data: { + href: '', + width: 1600, + height: 900, + }, + timestamp: 1636379531385, + }, + { + type: 2, + data: { + node: { + type: 0, + childNodes: [ + { type: 1, name: 'html', publicId: '', systemId: '', id: 2 }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 5 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + id: 6, + }, + { type: 3, textContent: '\n ', id: 7 }, + { + type: 2, + tagName: 'meta', + attributes: { + name: 'viewport', + content: 'width=device-width, initial-scale=1.0', + }, + childNodes: [], + id: 8, + }, + { type: 3, textContent: '\n ', id: 9 }, + { + type: 2, + tagName: 'title', + attributes: {}, + childNodes: [{ type: 3, textContent: 'assets', id: 11 }], + id: 10, + }, + { type: 3, textContent: '\n ', id: 12 }, + ], + id: 4, + }, + { type: 3, textContent: '\n ', id: 13 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 15 }, + { + type: 2, + tagName: 'img', + attributes: { + width: '100', + height: '100', + style: 'border: 1px solid #000000', + }, + childNodes: [{ type: 3, textContent: '\n ', id: 17 }], + id: 16, + }, + { type: 3, textContent: '\n ', id: 18 }, + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 20 }, + ], + id: 19, + }, + { type: 3, textContent: '\n \n\n', id: 21 }, + ], + id: 14, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: 1636379531389, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [ + { + id: 16, + attributes: { + src: 'httpx://example.com/image.png', + }, + }, + ], + removes: [], + adds: [], + }, + timestamp: 1636379531390, + }, + { + type: EventType.Asset, + data: { + url: 'httpx://example.com/image.png', + payload: { + rr_type: 'Blob', + type: 'image/png', + data: [ + { + rr_type: 'ArrayBuffer', + base64: + 'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAWtJREFUeF7t1cEJAEAIxEDtv2gProo8xgpCwuLezI3LGFhBMi0+iCCtHoLEeggiSM1AjMcPESRmIIZjIYLEDMRwLESQmIEYjoUIEjMQw7EQQWIGYjgWIkjMQAzHQgSJGYjhWIggMQMxHAsRJGYghmMhgsQMxHAsRJCYgRiOhQgSMxDDsRBBYgZiOBYiSMxADMdCBIkZiOFYiCAxAzEcCxEkZiCGYyGCxAzEcCxEkJiBGI6FCBIzEMOxEEFiBmI4FiJIzEAMx0IEiRmI4ViIIDEDMRwLESRmIIZjIYLEDMRwLESQmIEYjoUIEjMQw7EQQWIGYjgWIkjMQAzHQgSJGYjhWIggMQMxHAsRJGYghmMhgsQMxHAsRJCYgRiOhQgSMxDDsRBBYgZiOBYiSMxADMdCBIkZiOFYiCAxAzEcCxEkZiCGYyGCxAzEcCxEkJiBGI6FCBIzEMOxEEFiBmI4FiJIzEAMx0IEiRmI4TwVjsedWCiXGAAAAABJRU5ErkJggg==', // base64 + }, + ], + }, + }, + timestamp: 1636379532355, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-mutation-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-mutation-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbd91056eb79bc92f28acef03c14da255a83e26 GIT binary patch literal 10796 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4F%}28J29*~C-ahlL4m>3 z#WAE}&YL?MbD0fASRLmr%(~I^Dlk+lNcF`n@gyqn}zyo0kU{f|dUH;Dd@8NGCN-U1fDUMlxeRDcP z&O=s+0Ux$Ko0EL5@*7V>?!0{r|B80stt(~+t3GhrR<3`uIz!G=s8R+3cHwnWOqr}z zU-ETLv#9L3woso{Ow47h=rLS!Y`^Oyh@B1JABkUpxRrreFXFY9dnetkI-2nv@Vp30ti;nnFfX z$Y=@y7onrABycc{Hdw&HFj}92gMpHc%4m-P6ojJ*X*3~?_5#6aU^Fj4f?>2!g@nUs ziwhhMv}$q9e|z^JC$pgdizBzp*=O^ww<@$3ust+33C*{Cc3-DUpaD9Hpu~~@iH`;j zMhOT@!HEGh2nz%qqsld$O7|k-^a2S}vVF&Ml?6vA;keFa7I6;j` z6Q=s;qs1yPC`L;WNO~A8MZn=OFr~<7y9N~aL$6)4e`D<~1_sVs zptS%A;Pf+Eo{To+hhj<|B&qk&-?NKnR>ofc`dSx~ofu?x>}8lY^ZFT^jjYfN#+?0r z?|)k+D6jb6Xfp;F6r;@;NO~A;#(=|Nv>5{qhN0J@NPr~c(d-NfhXKyc9sG^SYMU-F R%;x|($J5o%Wt~$(696>PBJ= console.log('PAGE LOG:', msg.text())); }); @@ -86,7 +88,29 @@ describe('replayer', function () { expect(image).toMatchImageSnapshot(); }); - test.todo('should support urls src modified via mutation'); + it('should support urls src modified via incremental mutation', async () => { + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer([], { + liveMode: true, + }); + replayer.startLive(); + window.replayer.addEvent(events2[0]); + window.replayer.addEvent(events2[1]); + window.replayer.addEvent(events2[2]); + `); + + await waitForRAF(page); + + await page.evaluate(` + window.replayer.addEvent(events2[3]); + `); + + await waitForRAF(page); + + const image = await page.screenshot(); + expect(image).toMatchImageSnapshot(); + }); test.todo('should support video elements'); test.todo('should support audio elements'); From 8e31b6435a401a151c1563753879d06046aa1dd3 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 24 Nov 2023 17:38:29 +0100 Subject: [PATCH 016/197] Add replay for cached asset on attribute changes --- packages/rrweb-snapshot/src/index.ts | 2 + packages/rrweb-snapshot/src/rebuild.ts | 25 +---- packages/rrweb-snapshot/src/snapshot.ts | 26 ++++- packages/rrweb/src/record/mutation.ts | 33 +++--- .../src/record/observers/asset-manager.ts | 6 +- packages/rrweb/src/replay/assets/index.ts | 41 +++++++ packages/rrweb/src/replay/index.ts | 20 +++- packages/rrweb/src/utils.ts | 22 ++++ packages/rrweb/test/events/assets-mutation.ts | 2 +- ...dified-via-incremental-mutation-1-snap.png | Bin 0 -> 10796 bytes .../test/replay/asset-integration.test.ts | 15 +-- packages/rrweb/test/replay/asset-unit.test.ts | 100 +++++++++++++++++- packages/types/src/index.ts | 2 + 13 files changed, 240 insertions(+), 54 deletions(-) create mode 100644 packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png diff --git a/packages/rrweb-snapshot/src/index.ts b/packages/rrweb-snapshot/src/index.ts index c9f91a9100..8e00cd4803 100644 --- a/packages/rrweb-snapshot/src/index.ts +++ b/packages/rrweb-snapshot/src/index.ts @@ -8,6 +8,7 @@ import snapshot, { classMatchesRegex, IGNORED_NODE, genId, + getSourcesFromSrcset, } from './snapshot'; import rebuild, { buildNodeWithSN, @@ -32,4 +33,5 @@ export { classMatchesRegex, IGNORED_NODE, genId, + getSourcesFromSrcset, }; diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 870c33cadd..6ae854e9ff 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -328,29 +328,12 @@ function buildNode( n.attributes.srcset as string, ); continue; - } else if ( - tagName === 'img' && - name === 'src' && - options.assetManager - ) { - const originalValue = value.toString(); - node.setAttribute(name, originalValue); - void options.assetManager - .whenReady(value.toString()) - .then((status) => { - if ( - status.status === 'loaded' && - node.getAttribute('src') === originalValue - ) { - node.setAttribute(name, status.url); - } else { - console.log( - `failed to load asset: ${originalValue}, ${status.status}`, - ); - } - }); } else { node.setAttribute(name, value.toString()); + + if (options.assetManager?.isAttributeCacheable(node, name)) { + options.assetManager.manageAttribute(node, name); + } } } catch (error) { // skip invalid attribute diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 2df2de10d7..9985597284 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -67,7 +67,11 @@ let canvasCtx: CanvasRenderingContext2D | null; const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; // Don't use \s, to avoid matching non-breaking space // eslint-disable-next-line no-control-regex const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; -function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { +function parseSrcsetString( + doc: Document, + attributeValue: string, + urlCallback: (doc: Document, url: string) => string, +) { /* run absoluteToDoc over every url in the srcset @@ -104,13 +108,13 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { let url = collectCharacters(SRCSET_NOT_SPACES); if (url.slice(-1) === ',') { // aside: according to spec more than one comma at the end is a parse error, but we ignore that - url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + url = urlCallback(doc, url.substring(0, url.length - 1)); // the trailing comma splits the srcset, so the interpretion is that // another url will follow, and the descriptor is empty output.push(url); } else { let descriptorsStr = ''; - url = absoluteToDoc(doc, url); + url = urlCallback(doc, url); let inParens = false; // eslint-disable-next-line no-constant-condition while (true) { @@ -141,7 +145,20 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { return output.join(', '); } -const cachedDocument = new WeakMap(); +function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { + return parseSrcsetString(doc, attributeValue, (doc, url) => + absoluteToDoc(doc, url), + ); +} + +export function getSourcesFromSrcset(attributeValue: string): string[] { + const urls = new Set(); + parseSrcsetString(document, attributeValue, (_, url) => { + urls.add(url); + return url; + }); + return Array.from(urls); +} export function absoluteToDoc(doc: Document, attributeValue: string): string { if (!attributeValue || attributeValue.trim() === '') { @@ -155,6 +172,7 @@ function isSVGElement(el: Element): boolean { return Boolean(el.tagName === 'svg' || (el as SVGElement).ownerSVGElement); } +const cachedDocument = new WeakMap(); function getHref(doc: Document, customHref?: string) { let a = cachedDocument.get(doc); if (!a) { diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 73a30b1bf0..b753ad734e 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -10,6 +10,7 @@ import { isNativeShadowDom, getInputType, toLowerCase, + getSourcesFromSrcset, } from 'rrweb-snapshot'; import type { observerParam, MutationBufferParam } from '../types'; import type { @@ -620,12 +621,6 @@ export default class MutationBuffer { } else { return; } - } else if ( - target.tagName === 'IMG' && - (attributeName === 'src' || attributeName === 'srcset') && - value - ) { - this.assetManager.capture(value); } if (!item) { item = { @@ -650,12 +645,26 @@ export default class MutationBuffer { if (!ignoreAttribute(target.tagName, attributeName, value)) { // overwrite attribute if the mutations was triggered in same time - item.attributes[attributeName] = transformAttribute( - this.doc, - toLowerCase(target.tagName), - toLowerCase(attributeName), - value, - ); + const transformedValue = (item.attributes[attributeName] = + transformAttribute( + this.doc, + toLowerCase(target.tagName), + toLowerCase(attributeName), + value, + )); + if ( + transformedValue && + this.assetManager.isAttributeCacheable(target, attributeName) + ) { + if (attributeName === 'srcset') { + getSourcesFromSrcset(transformedValue).forEach((url) => { + this.assetManager.capture(url); + }); + } else { + this.assetManager.capture(transformedValue); + } + } + if (attributeName === 'style') { if (!this.unattachedDoc) { try { diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts index 4249ef014d..c459485ebb 100644 --- a/packages/rrweb/src/record/observers/asset-manager.ts +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -7,7 +7,7 @@ import type { import type { assetCallback } from '@rrweb/types'; import { encode } from 'base64-arraybuffer'; -import { patch } from '../../utils'; +import { isAttributeCacheable, patch } from '../../utils'; import type { recordOptions } from '../../types'; export default class AssetManager { @@ -191,4 +191,8 @@ export default class AssetManager { return { status: 'capturing' }; } + + public isAttributeCacheable(n: Element, attribute: string): boolean { + return isAttributeCacheable(n, attribute); + } } diff --git a/packages/rrweb/src/replay/assets/index.ts b/packages/rrweb/src/replay/assets/index.ts index 27391371cb..2c9488ec5c 100644 --- a/packages/rrweb/src/replay/assets/index.ts +++ b/packages/rrweb/src/replay/assets/index.ts @@ -5,6 +5,9 @@ import type { assetEvent, } from '@rrweb/types'; import { deserializeArg } from '../canvas/deserialize-args'; +import { isAttributeCacheable } from '../../utils'; +import { getSourcesFromSrcset } from 'rrweb-snapshot'; +import type { RRElement } from 'rrdom'; export default class AssetManager implements RebuildAssetManagerInterface { private originalToObjectURLMap: Map = new Map(); @@ -55,6 +58,7 @@ export default class AssetManager implements RebuildAssetManagerInterface { } } + // TODO: turn this into a true promise that throws if the asset fails to load public async whenReady(url: string): Promise { const currentStatus = this.get(url); if ( @@ -103,6 +107,43 @@ export default class AssetManager implements RebuildAssetManagerInterface { }; } + public isAttributeCacheable( + n: RRElement | Element, + attribute: string, + ): boolean { + return isAttributeCacheable(n as Element, attribute); + } + + public async manageAttribute( + node: RRElement | Element, + attribute: string, + ): Promise { + const originalValue = node.getAttribute(attribute); + if (!originalValue) return false; + + const promises = []; + + const values = + attribute === 'srcset' + ? getSourcesFromSrcset(originalValue) + : [originalValue]; + for (const value of values) { + promises.push( + this.whenReady(value).then((status) => { + if ( + status.status === 'loaded' && + node.getAttribute(attribute) === originalValue + ) { + node.setAttribute(attribute, status.url); + } else { + // failed to load asset, or the attribute was changed + } + }), + ); + } + return Promise.all(promises); + } + public reset(): void { this.originalToObjectURLMap.forEach((objectURL) => { URL.revokeObjectURL(objectURL); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 46c9dc6dc0..043519300b 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -969,6 +969,7 @@ export class Replayer { skipChild: false, afterAppend, cache: this.cache, + assetManager: this.assetManager, }); afterAppend(iframeEl.contentDocument! as Document, mutation.node.id); @@ -1585,6 +1586,7 @@ export class Replayer { skipChild: true, hackCss: true, cache: this.cache, + assetManager: this.assetManager, /** * caveat: `afterAppend` only gets called on child nodes of target * we have to call it again below when this target was added to the DOM @@ -1831,6 +1833,7 @@ export class Replayer { skipChild: true, hackCss: true, cache: this.cache, + assetManager: this.assetManager, }, ); // Update mirror meta's attributes @@ -1866,10 +1869,19 @@ export class Replayer { textarea.appendChild(tn as TNode); } } else { - (target as Element | RRElement).setAttribute( - attributeName, - value, - ); + const targetEl = target as Element | RRElement; + targetEl.setAttribute(attributeName, value); + if ( + this.assetManager.isAttributeCacheable( + targetEl, + attributeName, + ) + ) { + void this.assetManager.manageAttribute( + targetEl, + attributeName, + ); + } } if ( diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 13b15ec6c1..a559778e84 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -543,3 +543,25 @@ export function inDom(n: Node): boolean { if (!doc) return false; return dom.contains(doc, n) || shadowHostInDom(n); } + +export const CACHEABLE_ELEMENT_ATTRIBUTE_COMBINATIONS = new Map([ + ['IMG', new Set(['src', 'srcset'])], + ['VIDEO', new Set(['src'])], + ['AUDIO', new Set(['src'])], + ['EMBED', new Set(['src'])], + ['SOURCE', new Set(['src'])], + ['TRACK', new Set(['src'])], + ['INPUT', new Set(['src'])], + ['IFRAME', new Set(['src'])], + ['OBJECT', new Set(['src'])], +]); + +export function isAttributeCacheable(n: Element, attribute: string): boolean { + const acceptedAttributesSet = CACHEABLE_ELEMENT_ATTRIBUTE_COMBINATIONS.get( + n.nodeName, + ); + if (!acceptedAttributesSet) { + return false; + } + return acceptedAttributesSet.has(attribute); +} diff --git a/packages/rrweb/test/events/assets-mutation.ts b/packages/rrweb/test/events/assets-mutation.ts index b318183900..966c29678a 100644 --- a/packages/rrweb/test/events/assets-mutation.ts +++ b/packages/rrweb/test/events/assets-mutation.ts @@ -134,7 +134,7 @@ const events: eventWithTime[] = [ ], }, }, - timestamp: 1636379532355, + timestamp: 1636379531391, }, ]; diff --git a/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbd91056eb79bc92f28acef03c14da255a83e26 GIT binary patch literal 10796 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4F%}28J29*~C-ahlL4m>3 z#WAE}&YL?MbD0fASRLmr%(~I^Dlk+lNcF`n@gyqn}zyo0kU{f|dUH;Dd@8NGCN-U1fDUMlxeRDcP z&O=s+0Ux$Ko0EL5@*7V>?!0{r|B80stt(~+t3GhrR<3`uIz!G=s8R+3cHwnWOqr}z zU-ETLv#9L3woso{Ow47h=rLS!Y`^Oyh@B1JABkUpxRrreFXFY9dnetkI-2nv@Vp30ti;nnFfX z$Y=@y7onrABycc{Hdw&HFj}92gMpHc%4m-P6ojJ*X*3~?_5#6aU^Fj4f?>2!g@nUs ziwhhMv}$q9e|z^JC$pgdizBzp*=O^ww<@$3ust+33C*{Cc3-DUpaD9Hpu~~@iH`;j zMhOT@!HEGh2nz%qqsld$O7|k-^a2S}vVF&Ml?6vA;keFa7I6;j` z6Q=s;qs1yPC`L;WNO~A8MZn=OFr~<7y9N~aL$6)4e`D<~1_sVs zptS%A;Pf+Eo{To+hhj<|B&qk&-?NKnR>ofc`dSx~ofu?x>}8lY^ZFT^jjYfN#+?0r z?|)k+D6jb6Xfp;F6r;@;NO~A;#(=|Nv>5{qhN0J@NPr~c(d-NfhXKyc9sG^SYMU-F R%;x|($J5o%Wt~$(696>PBJ= console.log('PAGE LOG:', msg.text())); }); @@ -94,16 +96,16 @@ describe('replayer', function () { window.replayer = new Replayer([], { liveMode: true, }); - replayer.startLive(); - window.replayer.addEvent(events2[0]); - window.replayer.addEvent(events2[1]); - window.replayer.addEvent(events2[2]); + replayer.startLive(mutationEvents[0].timestamp); + window.replayer.addEvent(mutationEvents[0]); + window.replayer.addEvent(mutationEvents[1]); + window.replayer.addEvent(mutationEvents[2]); `); await waitForRAF(page); await page.evaluate(` - window.replayer.addEvent(events2[3]); + window.replayer.addEvent(mutationEvents[3]); `); await waitForRAF(page); @@ -112,6 +114,7 @@ describe('replayer', function () { expect(image).toMatchImageSnapshot(); }); + test.todo("should support work through rrelement's too"); test.todo('should support video elements'); test.todo('should support audio elements'); test.todo('should support embed elements'); diff --git a/packages/rrweb/test/replay/asset-unit.test.ts b/packages/rrweb/test/replay/asset-unit.test.ts index 0ee0470615..03d2cb6fd4 100644 --- a/packages/rrweb/test/replay/asset-unit.test.ts +++ b/packages/rrweb/test/replay/asset-unit.test.ts @@ -101,7 +101,7 @@ describe('AssetManager', () => { }); it('should execute hook when an asset is added', async () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const url = 'https://example.com/image.png'; const event: assetEvent = { type: EventType.Asset, @@ -113,9 +113,9 @@ describe('AssetManager', () => { void assetManager.add(event); const promise = assetManager.whenReady(url); - jest.spyOn(URL, 'createObjectURL').mockReturnValue('objectURL'); + vi.spyOn(URL, 'createObjectURL').mockReturnValue('objectURL'); - jest.runAllTimers(); + vi.runAllTimers(); await expect(promise).resolves.toEqual({ status: 'loaded', @@ -124,7 +124,7 @@ describe('AssetManager', () => { }); it('should send status reset to callbacks when reset', async () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const url = 'https://example.com/image.png'; const event: assetEvent = { type: EventType.Asset, @@ -137,8 +137,98 @@ describe('AssetManager', () => { const promise = assetManager.whenReady(url); assetManager.reset(); - jest.runAllTimers(); + vi.runAllTimers(); await expect(promise).resolves.toEqual({ status: 'reset' }); }); + + const validCombinations = [ + ['img', ['src', 'srcset']], + ['video', ['src']], + ['audio', ['src']], + ['embed', ['src']], + ['source', ['src']], + ['track', ['src']], + ['input', ['src']], + ['iframe', ['src']], + ['object', ['src']], + ] as const; + + const invalidCombinations = [ + ['img', ['href']], + ['script', ['href']], + ['link', ['src']], + ['video', ['href']], + ['audio', ['href']], + ['div', ['src']], + ['source', ['href']], + ['track', ['href']], + ['input', ['href']], + ['iframe', ['href']], + ['object', ['href']], + ] as const; + + validCombinations.forEach(([tagName, attributes]) => { + const element = document.createElement(tagName); + attributes.forEach((attribute) => { + it(`should correctly identify <${tagName} ${attribute}> as cacheable`, () => { + expect(assetManager.isAttributeCacheable(element, attribute)).toBe( + true, + ); + }); + }); + }); + + invalidCombinations.forEach(([tagName, attributes]) => { + const element = document.createElement(tagName); + attributes.forEach((attribute) => { + it(`should correctly identify <${tagName} ${attribute}> as NOT cacheable`, () => { + expect(assetManager.isAttributeCacheable(element, attribute)).toBe( + false, + ); + }); + }); + }); + + it("should be able to modify a node's attribute once asset is loaded", async () => { + const url = 'https://example.com/image.png'; + const event: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: examplePayload, + }, + }; + vi.spyOn(URL, 'createObjectURL').mockReturnValue('objectURL'); + + const element = document.createElement('img'); + element.setAttribute('src', url); + + const promise = assetManager.manageAttribute(element, 'src'); + + await assetManager.add(event); + await promise; + + expect(element.getAttribute('src')).toBe('objectURL'); + }); + + it("should be able to modify a node's attribute for previously loaded assets", async () => { + const url = 'https://example.com/image.png'; + const event: assetEvent = { + type: EventType.Asset, + data: { + url, + payload: examplePayload, + }, + }; + vi.spyOn(URL, 'createObjectURL').mockReturnValue('objectURL'); + await assetManager.add(event); + + const element = document.createElement('img'); + element.setAttribute('src', url); + + await assetManager.manageAttribute(element, 'src'); + + expect(element.getAttribute('src')).toBe('objectURL'); + }); }); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 7a8d2bad10..d8b5e9c0f9 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -748,6 +748,8 @@ export declare abstract class RebuildAssetManagerInterface { abstract get(url: string): RebuildAssetManagerStatus; abstract whenReady(url: string): Promise; abstract reset(): void; + abstract isAttributeCacheable(n: Element, attribute: string): boolean; + abstract manageAttribute(n: Element, attribute: string): void; } export enum NodeType { From 2a841fa5190a4b6fd2b0715dbca532b5de8c1d92 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 15:55:03 +0100 Subject: [PATCH 017/197] Fix syntax error --- packages/rrweb/src/replay/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 043519300b..03eff33725 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -2282,9 +2282,6 @@ export class Replayer { private backToNormal() { this.nextUserInteractionEvent = null; - if (this.speedService.state.matches('normal')) { - return; - } this.speedService.send({ type: 'BACK_TO_NORMAL' }); this.emitter.emit(ReplayerEvents.SkipEnd, { speed: this.speedService.state.context.normalSpeed, From 2b61f8bdb7a7c10504d2c51906592ba57c496b87 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 17:07:02 +0100 Subject: [PATCH 018/197] Move assetCapture params to @rrweb/types and add them to meta events --- docs/recipes/assets.md | 12 +-- guide.md | 62 ++++++------- packages/rrweb/src/record/index.ts | 4 +- .../src/record/observers/asset-manager.ts | 8 +- packages/rrweb/src/types.ts | 17 +--- packages/rrweb/test/record/asset.test.ts | 89 ++++++++++++------- packages/types/src/index.ts | 17 ++++ 7 files changed, 121 insertions(+), 88 deletions(-) diff --git a/docs/recipes/assets.md b/docs/recipes/assets.md index c6b08a2b01..8f57fc4539 100644 --- a/docs/recipes/assets.md +++ b/docs/recipes/assets.md @@ -10,12 +10,12 @@ The `inlineImages` configuration option is deprecated and should not be used any The `assetCapture` configuration option allows you to customize the asset capture process. It is an object with the following properties: -- `captureObjectURLs` (default: `true`): This property specifies whether to capture same-origin `blob:` assets using object URLs. Object URLs are created using the `URL.createObjectURL()` method. Setting `captureObjectURLs` to `true` enables the capture of object URLs. +- `objectURLs` (default: `true`): This property specifies whether to capture same-origin `blob:` assets using object URLs. Object URLs are created using the `URL.createObjectURL()` method. Setting `objectURLs` to `true` enables the capture of object URLs. -- `captureOrigins` (default: `false`): This property determines which origins to capture assets from. It can have the following values: +- `origins` (default: `false`): This property determines which origins to capture assets from. It can have the following values: - `false` or `[]`: Disables capturing any assets apart from object URLs. - `true`: Captures assets from all origins. - - `[origin1, origin2, ...]`: Captures assets only from the specified origins. For example, `captureOrigins: ['https://s3.example.com/']` captures all assets from the origin `https://s3.example.com/`. + - `[origin1, origin2, ...]`: Captures assets only from the specified origins. For example, `origins: ['https://s3.example.com/']` captures all assets from the origin `https://s3.example.com/`. ## TypeScript Type Definition @@ -26,14 +26,14 @@ export type recordOptions = { // Other configuration options... inlineImages?: boolean; assetCapture?: { - captureObjectURLs: boolean; - captureOrigins: string[] | true | false; + objectURLs: boolean; + origins: string[] | true | false; }; // Other configuration options... }; ``` -This type definition shows that `assetCapture` is an optional property of the `recordOptions` object. It contains the `captureObjectURLs` and `captureOrigins` properties, which have the same meanings as described above. +This type definition shows that `assetCapture` is an optional property of the `recordOptions` object. It contains the `objectURLs` and `origins` properties, which have the same meanings as described above. ## Conclusion diff --git a/guide.md b/guide.md index 4641e078ee..850022bbef 100644 --- a/guide.md +++ b/guide.md @@ -131,37 +131,37 @@ setInterval(save, 10 * 1000); The parameter of `rrweb.record` accepts the following options. -| key | default | description | -| ------------------------ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| emit | required | the callback function to get emitted events | -| checkoutEveryNth | - | take a full snapshot after every N events
refer to the [checkout](#checkout) chapter | -| checkoutEveryNms | - | take a full snapshot after every N ms
refer to the [checkout](#checkout) chapter | -| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | -| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | -| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | -| ignoreSelector | null | Use a string to configure which selector should be ignored, refer to the [privacy](#privacy) chapter | -| ignoreCSSAttributes | null | array of CSS attributes that should be ignored | -| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | -| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | -| maskAllInputs | false | mask all input content as \* | -| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | -| maskInputFn | - | customize mask input content recording logic | -| maskTextFn | - | customize mask text content recording logic | -| slimDOMOptions | {} | remove unnecessary parts of the DOM
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) | -| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data | -| inlineStylesheet | true | whether to inline the stylesheet in the events | -| hooks | {} | hooks for events
refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | -| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | -| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | -| recordCanvas | false | Whether to record the canvas element. Available options:
`false`,
`true` | -| recordCrossOriginIframes | false | Whether to record cross origin iframes. rrweb has to be injected in each child iframe for this to work. Available options:
`false`,
`true` | -| recordAfter | 'load' | If the document is not ready, then the recorder will start recording after the specified event is fired. Available options: `DOMContentLoaded`, `load` | -| inlineImages | false | whether to record the image content (deprecated, use `assetCapture` instead) | -| assetCapture | { captureObjectURLs: true, captureOrigins: false } | Configure the asset (image) capture and generates async asset events.
Refer to the [asset capture documentation](./docs/recipes/assets.md) for more info. | -| collectFonts | false | whether to collect fonts in the website | -| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) | -| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) | -| errorHandler | - | A callback that is called if something inside of rrweb throws an error. The callback receives the error as argument. | +| key | default | description | +| ------------------------ | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| emit | required | the callback function to get emitted events | +| checkoutEveryNth | - | take a full snapshot after every N events
refer to the [checkout](#checkout) chapter | +| checkoutEveryNms | - | take a full snapshot after every N ms
refer to the [checkout](#checkout) chapter | +| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | +| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | +| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | +| ignoreSelector | null | Use a string to configure which selector should be ignored, refer to the [privacy](#privacy) chapter | +| ignoreCSSAttributes | null | array of CSS attributes that should be ignored | +| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | +| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | +| maskAllInputs | false | mask all input content as \* | +| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | +| maskInputFn | - | customize mask input content recording logic | +| maskTextFn | - | customize mask text content recording logic | +| slimDOMOptions | {} | remove unnecessary parts of the DOM
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) | +| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data | +| inlineStylesheet | true | whether to inline the stylesheet in the events | +| hooks | {} | hooks for events
refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | +| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | +| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | +| recordCanvas | false | Whether to record the canvas element. Available options:
`false`,
`true` | +| recordCrossOriginIframes | false | Whether to record cross origin iframes. rrweb has to be injected in each child iframe for this to work. Available options:
`false`,
`true` | +| recordAfter | 'load' | If the document is not ready, then the recorder will start recording after the specified event is fired. Available options: `DOMContentLoaded`, `load` | +| inlineImages | false | whether to record the image content (deprecated, use `assetCapture` instead) | +| assetCapture | { objectURLs: true, origins: false } | Configure the asset (image) capture and generates async asset events.
Refer to the [asset capture documentation](./docs/recipes/assets.md) for more info. | +| collectFonts | false | whether to collect fonts in the website | +| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) | +| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) | +| errorHandler | - | A callback that is called if something inside of rrweb throws an error. The callback receives the error as argument. | #### Privacy diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 378777180a..7875ab1a44 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -99,8 +99,8 @@ function record( collectFonts = false, inlineImages = false, assetCapture = { - captureObjectURLs: true, - captureOrigins: false, + objectURLs: true, + origins: false, }, plugins, keepIframeSrcFn = () => false, diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts index c459485ebb..5f92cd1da4 100644 --- a/packages/rrweb/src/record/observers/asset-manager.ts +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -45,7 +45,7 @@ export default class AssetManager { const urlObjectMap = this.urlObjectMap; - if (this.config.captureObjectURLs) { + if (this.config.objectURLs) { try { const restoreHandler = patch( win.URL, @@ -86,15 +86,15 @@ export default class AssetManager { const urlIsBlob = url.startsWith(`blob:${window.location.origin}/`); // Check if url is a blob and we should ignore blobs - if (urlIsBlob) return !this.config.captureObjectURLs; + if (urlIsBlob) return !this.config.objectURLs; // Check if url matches any ignorable origins for (const origin of originsToIgnore) { if (url.startsWith(origin)) return true; } - // Check the captureOrigins - const captureOrigins = this.config.captureOrigins; + // Check the origins + const captureOrigins = this.config.origins; if (typeof captureOrigins === 'boolean') { return !captureOrigins; } else if (Array.isArray(captureOrigins)) { diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index eab2aa38aa..2487fa4e2c 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -38,6 +38,7 @@ import type { viewportResizeCallback, PackFn, UnpackFn, + assetCaptureParam, } from '@rrweb/types'; import type ProcessedNodeManager from './record/processed-node-manager'; import type AssetManager from './record/observers/asset-manager'; @@ -70,21 +71,7 @@ export type recordOptions = { userTriggeredOnInput?: boolean; collectFonts?: boolean; inlineImages?: boolean; - assetCapture?: { - /** - * Captures object URLs (blobs, files, media sources). - * More info: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL - */ - captureObjectURLs: boolean; - /** - * Allowlist of origins to capture object URLs from. - * [origin, origin, ...] to capture from specific origins. - * e.g. ['https://example.com', 'https://www.example.com'] - * Set to `true` capture from all origins. - * Set to `false` or `[]` to disable capturing from any origin apart from object URLs. - */ - captureOrigins: string[] | true | false; - }; + assetCapture?: assetCaptureParam; plugins?: RecordPlugin[]; // departed, please use sampling options mousemoveWait?: number; diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index 2de6f4789a..924c9efb04 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -105,13 +105,12 @@ const setup = function ( ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); if ( - options?.assetCapture?.captureOrigins && - Array.isArray(options.assetCapture.captureOrigins) + options?.assetCapture?.origins && + Array.isArray(options.assetCapture.origins) ) { - options.assetCapture.captureOrigins = - options.assetCapture.captureOrigins.map((origin) => - origin.replace(/\{SERVER_URL\}/g, ctx.serverURL), - ); + options.assetCapture.origins = options.assetCapture.origins.map( + (origin) => origin.replace(/\{SERVER_URL\}/g, ctx.serverURL), + ); } await injectRecordScript(ctx.page.mainFrame(), options); }); @@ -132,7 +131,7 @@ const setup = function ( describe('asset caching', function (this: ISuite) { vi.setConfig({ testTimeout: 100_000 }); - describe('captureObjectURLs: true with incremental snapshots', function (this: ISuite) { + describe('objectURLs: true with incremental snapshots', function (this: ISuite) { const ctx: ISuite = setup.call( this, ` @@ -143,8 +142,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureObjectURLs: true, - captureOrigins: false, + objectURLs: true, + origins: false, }, }, ); @@ -244,7 +243,7 @@ describe('asset caching', function (this: ISuite) { }); }); - describe('captureObjectURLs: true with fullSnapshot', function (this: ISuite) { + describe('objectURLs: true with fullSnapshot', function (this: ISuite) { const ctx: ISuite = setup.call( this, ` @@ -285,8 +284,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureObjectURLs: true, - captureOrigins: false, + objectURLs: true, + origins: false, }, }, ); @@ -319,7 +318,7 @@ describe('asset caching', function (this: ISuite) { expect(events[events.length - 1]).toMatchObject(expected); }); }); - describe('captureObjectURLs: false', () => { + describe('objectURLs: false', () => { const ctx: ISuite = setup.call( this, ` @@ -330,8 +329,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureObjectURLs: false, - captureOrigins: false, + objectURLs: false, + origins: false, }, }, ); @@ -397,7 +396,7 @@ describe('asset caching', function (this: ISuite) { ); }); }); - describe('captureOrigins: false', () => { + describe('origins: false', () => { const ctx: ISuite = setup.call( this, ` @@ -410,8 +409,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: false, - captureObjectURLs: false, + origins: false, + objectURLs: false, }, }, ); @@ -429,7 +428,7 @@ describe('asset caching', function (this: ISuite) { ); }); }); - describe('captureOrigins: []', () => { + describe('origins: []', () => { const ctx: ISuite = setup.call( this, ` @@ -442,8 +441,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: [], - captureObjectURLs: false, + origins: [], + objectURLs: false, }, }, ); @@ -461,7 +460,7 @@ describe('asset caching', function (this: ISuite) { ); }); }); - describe('captureOrigins: true', () => { + describe('origins: true', () => { const ctx: ISuite = setup.call( this, ` @@ -474,8 +473,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: true, - captureObjectURLs: false, + origins: true, + objectURLs: false, }, }, ); @@ -497,7 +496,7 @@ describe('asset caching', function (this: ISuite) { }); }); - describe('captureOrigins: true with invalid urls', () => { + describe('origins: true with invalid urls', () => { const ctx: ISuite = setup.call( this, ` @@ -511,8 +510,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: true, - captureObjectURLs: false, + origins: true, + objectURLs: false, }, }, ); @@ -567,7 +566,7 @@ describe('asset caching', function (this: ISuite) { }); }); - describe('captureOrigins: ["http://localhost:xxxxx/"]', () => { + describe('origins: ["http://localhost:xxxxx/"]', () => { const ctx: ISuite = setup.call( this, ` @@ -581,8 +580,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: ['{SERVER_URL}'], - captureObjectURLs: false, + origins: ['{SERVER_URL}'], + objectURLs: false, }, }, ); @@ -625,5 +624,35 @@ describe('asset caching', function (this: ISuite) { }), ); }); + + it('add recorded origins to meta event', async () => { + await ctx.page.waitForNetworkIdle({ idleTime: 100 }); + await waitForRAF(ctx.page); + + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + // expect an event to be emitted with `event.type` === EventType.Asset + expect(events).toContainEqual( + expect.objectContaining({ + type: EventType.Meta, + data: { + assetCapture: { + origins: ['{SERVER_URL}'], + objectURLs: false, + }, + }, + }), + ); + }); }); + + test.todo('should support video elements'); + test.todo('should support audio elements'); + test.todo('should support embed elements'); + test.todo('should support source elements'); + test.todo('should support track elements'); + test.todo('should support input#type=image elements'); + test.todo('should support img srcset'); }); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d8b5e9c0f9..0061cfb187 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -41,6 +41,7 @@ export type metaEvent = { href: string; width: number; height: number; + assetCapture?: assetCaptureParam; }; }; @@ -60,6 +61,22 @@ export type pluginEvent = { }; }; +export type assetCaptureParam = { + /** + * Captures object URLs (blobs, files, media sources). + * More info: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL + */ + objectURLs: boolean; + /** + * Allowlist of origins to capture object URLs from. + * [origin, origin, ...] to capture from specific origins. + * e.g. ['https://example.com', 'https://www.example.com'] + * Set to `true` capture from all origins. + * Set to `false` or `[]` to disable capturing from any origin apart from object URLs. + */ + origins: string[] | true | false; +}; + export type assetEvent = { type: EventType.Asset; data: assetParam; From 875d82654a0c6d83c83166568f6525fced5581b8 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 17:26:07 +0100 Subject: [PATCH 019/197] Add asset capture config to meta events --- packages/rrweb/src/record/index.ts | 1 + packages/rrweb/test/integration.test.ts | 6 +++--- packages/rrweb/test/record/asset.test.ts | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 7875ab1a44..797b4ddc63 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -384,6 +384,7 @@ function record( href: window.location.href, width: getWindowWidth(), height: getWindowHeight(), + assetCapture, }, }, isCheckout, diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 393b82e8bb..6e7dd19cbc 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -922,7 +922,7 @@ describe('record integration tests', function (this: ISuite) { page.setContent( getHtml.call(this, 'image-blob-url.html', { inlineImages: true, - assetCapture: { captureObjectURLs: false, captureOrigins: false }, + assetCapture: { objectURLs: false, origins: false }, }), ); await page.waitForResponse(`${serverURL}/html/assets/robot.png`); @@ -942,7 +942,7 @@ describe('record integration tests', function (this: ISuite) { await page.setContent( getHtml.call(this, 'frame-image-blob-url.html', { inlineImages: true, - assetCapture: { captureObjectURLs: false, captureOrigins: false }, + assetCapture: { objectURLs: false, origins: false }, }), ); await page.waitForResponse(`${serverURL}/html/assets/robot.png`); @@ -962,7 +962,7 @@ describe('record integration tests', function (this: ISuite) { await page.setContent( getHtml.call(this, 'frame2.html', { inlineImages: true, - assetCapture: { captureObjectURLs: false, captureOrigins: false }, + assetCapture: { objectURLs: false, origins: false }, }), ); await page.waitForSelector('iframe'); // wait for iframe to get added diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index 924c9efb04..b6b6881e10 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -638,8 +638,11 @@ describe('asset caching', function (this: ISuite) { expect.objectContaining({ type: EventType.Meta, data: { + href: expect.any(String), + width: expect.any(Number), + height: expect.any(Number), assetCapture: { - origins: ['{SERVER_URL}'], + origins: [ctx.serverURL], objectURLs: false, }, }, From 1fef8183b2222b431fd6ebea6d1aaf96d85819a0 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 23:36:42 +0100 Subject: [PATCH 020/197] NodeType was moved to @rrweb/types --- packages/rrweb-snapshot/test/utils.test.ts | 12 ++++++++++++ ...orporate-assets-streamed-later-1-snap.png} | Bin 10796 -> 10784 bytes ...ified-via-incremental-mutation-1-snap.png} | Bin 10796 -> 10784 bytes 3 files changed, 12 insertions(+) rename packages/rrweb/test/replay/__image_snapshots__/{asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png => asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png} (75%) rename packages/rrweb/test/replay/__image_snapshots__/{asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png => asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png} (75%) diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index 0a82d8c16c..b942a7c079 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -2,14 +2,26 @@ * @vitest-environment jsdom */ import { describe, it, test, expect } from 'vitest'; +<<<<<<< HEAD +||||||| parent of a5097a7cb (NodeType was moved to @rrweb/types) +import { NodeType, serializedNode } from '@rrweb/types'; +======= +import { NodeType } from '@rrweb/types'; +>>>>>>> a5097a7cb (NodeType was moved to @rrweb/types) import { escapeImportStatement, extractFileExtension, fixSafariColons, isNodeMetaEqual, } from '../src/utils'; +<<<<<<< HEAD import { NodeType } from '@rrweb/types'; import type { serializedNode, serializedNodeWithId } from '@rrweb/types'; +||||||| parent of a5097a7cb (NodeType was moved to @rrweb/types) +import type { serializedNodeWithId } from '@rrweb/types'; +======= +import type { serializedNode, serializedNodeWithId } from '@rrweb/types'; +>>>>>>> a5097a7cb (NodeType was moved to @rrweb/types) describe('utils', () => { describe('isNodeMetaEqual()', () => { diff --git a/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png similarity index 75% rename from packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png rename to packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-incorporate-assets-streamed-later-1-snap.png index 3bbd91056eb79bc92f28acef03c14da255a83e26..6ffa781491afba068bd16ec6494fac00f377bf5f 100644 GIT binary patch delta 143 zcmZ1zvLIwbrl{CK>jIP6^>JK|dDpYVx|C(JfV3LxWC8IvJSkVz z9x*U*p7V5Z4B5;mX31>QaoxN>?O9Z|?Os*}Flb#|j delta 178 zcmZ1wvL<9hrl^=Bx6IjR^RKrmv=^{FG&Tv%w|#bBr)%><@trJ!J9rOduT?K&fPjJ% ztdl=7%@)|sSn>Yv5k9CW-_Fg2;!dnQ`#09^VqoCB<>}%WvU#JZE3?U?zh@WEtc<<> z^|dZkt;~+S4D)7QKV!3z6(Z8Woc(_9e_JLfulOI(tY|i|4*teuwM`co=5v4~JYD@< J);T3K0Ra0^J}UqK diff --git a/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png similarity index 75% rename from packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png rename to packages/rrweb/test/replay/__image_snapshots__/asset-integration-test-ts-test-replay-asset-integration-test-ts-replayer-asset-should-support-urls-src-modified-via-incremental-mutation-1-snap.png index 3bbd91056eb79bc92f28acef03c14da255a83e26..6ffa781491afba068bd16ec6494fac00f377bf5f 100644 GIT binary patch delta 143 zcmZ1zvLIwbrl{CK>jIP6^>JK|dDpYVx|C(JfV3LxWC8IvJSkVz z9x*U*p7V5Z4B5;mX31>QaoxN>?O9Z|?Os*}Flb#|j delta 178 zcmZ1wvL<9hrl^=Bx6IjR^RKrmv=^{FG&Tv%w|#bBr)%><@trJ!J9rOduT?K&fPjJ% ztdl=7%@)|sSn>Yv5k9CW-_Fg2;!dnQ`#09^VqoCB<>}%WvU#JZE3?U?zh@WEtc<<> z^|dZkt;~+S4D)7QKV!3z6(Z8Woc(_9e_JLfulOI(tY|i|4*teuwM`co=5v4~JYD@< J);T3K0Ra0^J}UqK From 1d5737d1105be2345e1f7bab80519e4c9404a1ba Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 23:40:14 +0100 Subject: [PATCH 021/197] Upgrade jest and rollup in rrweb-snapshot --- packages/rrweb-snapshot/jest.setup.ts | 3 +++ packages/rrweb-snapshot/package.json | 2 +- packages/rrweb-snapshot/test/utils.test.ts | 12 ------------ yarn.lock | 8 ++++---- 4 files changed, 8 insertions(+), 17 deletions(-) create mode 100644 packages/rrweb-snapshot/jest.setup.ts diff --git a/packages/rrweb-snapshot/jest.setup.ts b/packages/rrweb-snapshot/jest.setup.ts new file mode 100644 index 0000000000..7c97c034f2 --- /dev/null +++ b/packages/rrweb-snapshot/jest.setup.ts @@ -0,0 +1,3 @@ +import { TextEncoder, TextDecoder } from 'util'; + +Object.assign(global, { TextDecoder, TextEncoder }); diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index bdab817710..39b82320e7 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -59,7 +59,7 @@ "devDependencies": { "@rrweb/types": "^2.0.0-alpha.18", "@rrweb/utils": "^2.0.0-alpha.18", - "@types/jsdom": "^20.0.0", + "@types/jsdom": "^21.1.6", "@types/node": "^18.15.11", "@types/puppeteer": "^5.4.4", "puppeteer": "^17.1.3", diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index b942a7c079..0a82d8c16c 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -2,26 +2,14 @@ * @vitest-environment jsdom */ import { describe, it, test, expect } from 'vitest'; -<<<<<<< HEAD -||||||| parent of a5097a7cb (NodeType was moved to @rrweb/types) -import { NodeType, serializedNode } from '@rrweb/types'; -======= -import { NodeType } from '@rrweb/types'; ->>>>>>> a5097a7cb (NodeType was moved to @rrweb/types) import { escapeImportStatement, extractFileExtension, fixSafariColons, isNodeMetaEqual, } from '../src/utils'; -<<<<<<< HEAD import { NodeType } from '@rrweb/types'; import type { serializedNode, serializedNodeWithId } from '@rrweb/types'; -||||||| parent of a5097a7cb (NodeType was moved to @rrweb/types) -import type { serializedNodeWithId } from '@rrweb/types'; -======= -import type { serializedNode, serializedNodeWithId } from '@rrweb/types'; ->>>>>>> a5097a7cb (NodeType was moved to @rrweb/types) describe('utils', () => { describe('isNodeMetaEqual()', () => { diff --git a/yarn.lock b/yarn.lock index 6ff3998432..4fc94b983d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2719,10 +2719,10 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" -"@types/jsdom@^20.0.0": - version "20.0.1" - resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" - integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== +"@types/jsdom@^21.1.6": + version "21.1.6" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.6.tgz#bcbc7b245787ea863f3da1ef19aa1dcfb9271a1b" + integrity sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw== dependencies: "@types/node" "*" "@types/tough-cookie" "*" From 27c382324e085bee3fee863bda8469e893a9263b Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 23:40:34 +0100 Subject: [PATCH 022/197] Add check types From 48570a801e1040458b189b260c040d25d61663b4 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 14:44:39 +0100 Subject: [PATCH 023/197] Update snapshots to include assetCapture settings --- .../test/__snapshots__/index.test.ts.snap | 12 +- .../__snapshots__/integration.test.ts.snap | 270 +++++++++++++++--- .../test/__snapshots__/record.test.ts.snap | 132 +++++++-- .../cross-origin-iframes.test.ts.snap | 90 +++++- .../record/__snapshots__/webgl.test.ts.snap | 48 +++- 5 files changed, 460 insertions(+), 92 deletions(-) diff --git a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap index cd0df64e96..001660e949 100644 --- a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap +++ b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap @@ -7,7 +7,11 @@ exports[`rrweb-plugin-console-record > should handle recursive console messages \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -191,7 +195,11 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 738f2fe8e2..51e3747b83 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -15,7 +15,11 @@ exports[`record integration tests > can correctly serialize a shader and multipl \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -429,7 +433,11 @@ exports[`record integration tests > can freeze mutations 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -609,7 +617,11 @@ exports[`record integration tests > can mask character data mutations 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -827,7 +839,11 @@ exports[`record integration tests > can mask character data mutations with regex \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1714,7 +1730,11 @@ exports[`record integration tests > can record attribute mutation 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1885,7 +1905,11 @@ exports[`record integration tests > can record character data muatations 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2063,7 +2087,11 @@ exports[`record integration tests > can record childList mutations 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2239,7 +2267,11 @@ exports[`record integration tests > can record clicks 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2526,7 +2558,11 @@ exports[`record integration tests > can record form interactions 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3309,7 +3345,11 @@ exports[`record integration tests > can record node mutations 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4284,7 +4324,11 @@ exports[`record integration tests > can record style changes compactly and prese \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4510,7 +4554,11 @@ exports[`record integration tests > can use maskInputOptions to configure which \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -5381,7 +5429,11 @@ exports[`record integration tests > handles null attribute values 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -5572,7 +5624,11 @@ exports[`record integration tests > mutations should work when blocked class is \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -6338,7 +6394,11 @@ exports[`record integration tests > should mask inputs via function call 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -7208,7 +7268,11 @@ exports[`record integration tests > should mask password value attribute with ma \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -7655,7 +7719,11 @@ exports[`record integration tests > should mask texts 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -7953,7 +8021,11 @@ exports[`record integration tests > should mask texts using maskTextFn 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -8251,7 +8323,11 @@ exports[`record integration tests > should nest record iframe 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -9011,7 +9087,11 @@ exports[`record integration tests > should not record blocked elements and its c \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -9191,7 +9271,11 @@ exports[`record integration tests > should not record blocked elements dynamical \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -9397,7 +9481,11 @@ exports[`record integration tests > should not record input events on ignored el \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -9808,7 +9896,11 @@ exports[`record integration tests > should not record input values if dynamicall \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -10166,7 +10258,11 @@ exports[`record integration tests > should not record input values if maskAllInp \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -11036,7 +11132,11 @@ exports[`record integration tests > should record DOM node movement 1 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -11303,7 +11403,11 @@ exports[`record integration tests > should record DOM node movement 2 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -11577,7 +11681,11 @@ exports[`record integration tests > should record after DOMContentLoaded event 1 \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -11664,7 +11772,11 @@ exports[`record integration tests > should record canvas mutations 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -11883,7 +11995,11 @@ exports[`record integration tests > should record dynamic CSS changes 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -12279,7 +12395,11 @@ exports[`record integration tests > should record images inside iframe with blob \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": false, + \\"origins\\": false + } } }, { @@ -12638,7 +12758,11 @@ exports[`record integration tests > should record images inside iframe with blob \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": false, + \\"origins\\": false + } } }, { @@ -13072,7 +13196,11 @@ exports[`record integration tests > should record images with blob url 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": false, + \\"origins\\": false + } } }, { @@ -13279,7 +13407,11 @@ exports[`record integration tests > should record input userTriggered values if \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -14179,7 +14311,11 @@ exports[`record integration tests > should record moved shadow DOM 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -14348,7 +14484,11 @@ exports[`record integration tests > should record moved shadow DOM 2 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -14531,7 +14671,11 @@ exports[`record integration tests > should record mutations in iframes accross p \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -14840,7 +14984,11 @@ exports[`record integration tests > should record nested iframes and shadow doms \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -15187,7 +15335,11 @@ exports[`record integration tests > should record shadow DOM 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -15639,7 +15791,11 @@ exports[`record integration tests > should record shadow DOM 2 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -15757,7 +15913,11 @@ exports[`record integration tests > should record shadow DOM 3 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -15875,7 +16035,11 @@ exports[`record integration tests > should record shadow doms polyfilled by shad \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -16121,7 +16285,11 @@ exports[`record integration tests > should record shadow doms polyfilled by synt \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -16813,7 +16981,11 @@ exports[`record integration tests > should record webgl canvas mutations 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -17028,7 +17200,11 @@ exports[`record integration tests > should unmask texts using maskTextFn 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -17326,7 +17502,11 @@ exports[`record integration tests > will serialize node before record 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index c1db08aea0..b93f880896 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -7,7 +7,11 @@ exports[`record > aggregates mutations 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -93,7 +97,11 @@ exports[`record > can add custom event 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -187,7 +195,11 @@ exports[`record > captures CORS stylesheets that are still loading 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -290,7 +302,11 @@ exports[`record > captures adopted stylesheets in nested shadow doms and iframes \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -756,7 +772,11 @@ exports[`record > captures adopted stylesheets in shadow doms and iframe 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1152,7 +1172,11 @@ exports[`record > captures adopted stylesheets of shadow doms in checkout full s \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1252,7 +1276,11 @@ exports[`record > captures adopted stylesheets of shadow doms in checkout full s \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1357,7 +1385,11 @@ exports[`record > captures inserted style text nodes correctly 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1485,7 +1517,11 @@ exports[`record > captures mutations on adopted stylesheets 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1785,7 +1821,11 @@ exports[`record > captures nested stylesheet rules 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1934,7 +1974,11 @@ exports[`record > captures style property changes 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2063,7 +2107,11 @@ exports[`record > captures stylesheet rules 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2341,7 +2389,11 @@ exports[`record > captures stylesheets in iframes with \`blob:\` url 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2485,7 +2537,11 @@ exports[`record > captures stylesheets with \`blob:\` url 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2573,7 +2629,11 @@ exports[`record > is safe to checkout during async callbacks 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2704,7 +2764,11 @@ exports[`record > is safe to checkout during async callbacks 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2829,7 +2893,11 @@ exports[`record > loading stylesheets > captures stylesheets in iframes that are \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3165,7 +3233,11 @@ exports[`record > loading stylesheets > captures stylesheets that are still load \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3345,7 +3417,11 @@ exports[`record > no need for attribute mutations on adds 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3465,7 +3541,11 @@ exports[`record > should record scroll position 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3585,7 +3665,11 @@ exports[`record > without CSSGroupingRule support > captures nested stylesheet r \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3734,7 +3818,11 @@ exports[`record iframes > captures stylesheet mutations in iframes 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 8cd9fce959..40e3497c04 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -7,7 +7,11 @@ exports[`cross origin iframes > audio.html > should emit contents of iframe once \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -345,7 +349,11 @@ exports[`cross origin iframes > blank.html > should filter out forwarded cross o \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -607,7 +615,11 @@ exports[`cross origin iframes > blank.html > should record same-origin iframe in \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -851,7 +863,11 @@ exports[`cross origin iframes > form.html > should map input events correctly 1` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1864,7 +1880,11 @@ exports[`cross origin iframes > form.html > should map scroll events correctly 1 \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2520,7 +2540,11 @@ exports[`cross origin iframes > move-node.html > captures mutations on adopted s \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2934,7 +2958,11 @@ exports[`cross origin iframes > move-node.html > captures mutations on styleshee \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3353,7 +3381,11 @@ exports[`cross origin iframes > move-node.html > should record DOM attribute cha \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3641,7 +3673,11 @@ exports[`cross origin iframes > move-node.html > should record DOM node movement \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4026,7 +4062,11 @@ exports[`cross origin iframes > move-node.html > should record DOM node removal \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4312,7 +4352,11 @@ exports[`cross origin iframes > move-node.html > should record DOM text changes \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4598,7 +4642,11 @@ exports[`cross origin iframes > move-node.html > should record canvas elements 1 \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4921,7 +4969,11 @@ exports[`cross origin iframes > move-node.html > should record custom events 1`] \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -5203,7 +5255,11 @@ exports[`same origin iframes > should emit contents of iframe once 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -5377,7 +5433,11 @@ exports[`same origin iframes > should record cross-origin iframe in same-origin \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { diff --git a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap index 8a9b0c1fc2..f40a0a68d9 100644 --- a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap @@ -7,7 +7,11 @@ exports[`record webgl > recordCanvas FPS > should record snapshots 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -100,7 +104,11 @@ exports[`record webgl > should batch events by RAF 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -261,7 +269,11 @@ exports[`record webgl > will record changes to a canvas element 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -370,7 +382,11 @@ exports[`record webgl > will record changes to a canvas element before the canva \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -514,7 +530,11 @@ exports[`record webgl > will record changes to a canvas element before the canva \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -658,7 +678,11 @@ exports[`record webgl > will record changes to a webgl2 canvas element 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -767,7 +791,11 @@ exports[`record webgl > will record webgl variables 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -896,7 +924,11 @@ exports[`record webgl > will record webgl variables in reverse order 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { From 26c14b90677695d1bf370dfbcc47a09d5058b263 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:05:06 +0100 Subject: [PATCH 024/197] Mark `inlineImages` as deprecated in favor of `assetCapture` --- docs/recipes/assets.md | 2 +- docs/recipes/assets.zh_CN.md | 40 +++++++++++++++++ guide.md | 2 +- guide.zh_CN.md | 59 +++++++++++++------------ package.json | 2 +- packages/rrweb-snapshot/src/snapshot.ts | 12 +++++ packages/rrweb/scripts/stream.js | 5 ++- packages/rrweb/src/record/mutation.ts | 4 ++ packages/rrweb/src/types.ts | 6 +++ 9 files changed, 99 insertions(+), 33 deletions(-) create mode 100644 docs/recipes/assets.zh_CN.md diff --git a/docs/recipes/assets.md b/docs/recipes/assets.md index 8f57fc4539..e364b7eae8 100644 --- a/docs/recipes/assets.md +++ b/docs/recipes/assets.md @@ -24,11 +24,11 @@ Here is the TypeScript type definition for the `recordOptions` object, which inc ```typescript export type recordOptions = { // Other configuration options... - inlineImages?: boolean; assetCapture?: { objectURLs: boolean; origins: string[] | true | false; }; + inlineImages?: boolean; // Deprecated, don't use it anymore // Other configuration options... }; ``` diff --git a/docs/recipes/assets.zh_CN.md b/docs/recipes/assets.zh_CN.md new file mode 100644 index 0000000000..fcd496b14a --- /dev/null +++ b/docs/recipes/assets.zh_CN.md @@ -0,0 +1,40 @@ +# rrweb 中的资源捕获方法和配置 + +[rrweb](https://rrweb.io/) 是一个 JavaScript 库,允许您记录并重放您网站上的用户互动。它为捕获资产(如图像)提供了各种配置选项。在本文档中,我们将探讨 rrweb 中不同的资源捕获方法及其配置选项。 + +## 内联图像(已弃用) + +`inlineImages` 配置选项已被弃用,不应再使用。它存在一些问题,即重写已经发出的事件,这可能使您错过已发送到服务器的内联图像。相反,请使用 `assetCapture` 选项来配置资源捕获。 + +## 资源捕获配置 + +`assetCapture` 配置选项允许您自定义资源捕获过程。它是一个具有以下属性的对象: + +- `objectURLs`(默认值:`true`):此属性指定是否使用对象 URL 捕获同源 `blob:` 资源。对象 URL 是使用 `URL.createObjectURL()` 方法创建的。将 `objectURLs` 设置为 `true` 可以启用对象 URL 的捕获。 + +- `origins`(默认值:`false`):此属性确定从哪些来源捕获资源。它可以有以下值: + - `false` 或 `[]`:除了对象 URL 之外,不捕获任何资源。 + - `true`:从所有来源捕获资源。 + - `[origin1, origin2, ...]`:仅从指定的来源捕获资源。例如,`origins: ['https://s3.example.com/']` 从 `https://s3.example.com/` 来源捕获所有资源。 + +## TypeScript 类型定义 + +这是 `recordOptions` 对象的 TypeScript 类型定义,其中包括资源捕获配置选项: + +```typescript +export type recordOptions = { + // 其他配置选项... + assetCapture?: { + objectURLs: boolean; + origins: string[] | true | false; + }; + inlineImages?: boolean; // 已弃用 + // 其他配置选项... +}; +``` + +这种类型定义表明 assetCapture 是 recordOptions 对象的一个可选属性。它包含 objectURLs 和 origins 属性,其含义与上述相同。 + +## 结论 + +通过在 rrweb 中配置 assetCapture 选项,您可以控制在记录过程中如何捕获像图像这样的资源。这允许您 diff --git a/guide.md b/guide.md index 850022bbef..afb7a686c3 100644 --- a/guide.md +++ b/guide.md @@ -156,7 +156,7 @@ The parameter of `rrweb.record` accepts the following options. | recordCanvas | false | Whether to record the canvas element. Available options:
`false`,
`true` | | recordCrossOriginIframes | false | Whether to record cross origin iframes. rrweb has to be injected in each child iframe for this to work. Available options:
`false`,
`true` | | recordAfter | 'load' | If the document is not ready, then the recorder will start recording after the specified event is fired. Available options: `DOMContentLoaded`, `load` | -| inlineImages | false | whether to record the image content (deprecated, use `assetCapture` instead) | +| ~inlineImages~ | false | whether to record the image content (deprecated, use `assetCapture` instead) | | assetCapture | { objectURLs: true, origins: false } | Configure the asset (image) capture and generates async asset events.
Refer to the [asset capture documentation](./docs/recipes/assets.md) for more info. | | collectFonts | false | whether to collect fonts in the website | | userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) | diff --git a/guide.zh_CN.md b/guide.zh_CN.md index 4078cb2b6a..88df0a618d 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -128,35 +128,36 @@ setInterval(save, 10 * 1000); `rrweb.record(config)` 的 config 部分接受以下参数 -| key | 默认值 | 功能 | -| ------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| emit | 必填 | 获取当前录制的数据 | -| checkoutEveryNth | - | 每 N 次事件重新制作一次全量快照
详见[“重新制作快照”](#重新制作快照)章节 | -| checkoutEveryNms | - | 每 N 毫秒重新制作一次全量快照
详见[“重新制作快照”](#重新制作快照)章节 | -| blockClass | 'rr-block' | 字符串或正则表达式,可用于自定义屏蔽元素的类名,详见[“隐私”](#隐私)章节 | -| blockSelector | null | 所有 element.matches(blockSelector)为 true 的元素都不会被录制,回放时取而代之的是一个同等宽高的占位元素 | -| ignoreClass | 'rr-ignore' | 字符串或正则表达式,可用于自定义忽略元素的类名,详见[“隐私”](#隐私)章节 | -| ignoreCSSAttributes | null | 应该被忽略的 CSS 属性数组 | -| maskTextClass | 'rr-mask' | 字符串或正则表达式,可用于自定义忽略元素 text 内容的类名,详见[“隐私”](#隐私)章节 | -| maskTextSelector | null | 所有 element.matches(maskTextSelector)为 true 的元素及其子元素的 text 内容将会被屏蔽 | -| maskAllInputs | false | 将所有输入内容记录为 \* | -| maskInputOptions | { password: true } | 选择将特定类型的输入框内容记录为 \*
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | -| maskInputFn | - | 自定义特定类型的输入框内容记录逻辑 | -| maskTextFn | - | 自定义文字内容的记录逻辑 | -| slimDOMOptions | {} | 去除 DOM 中不必要的部分
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) | -| inlineStylesheet | true | 是否将样式表内联 | -| hooks | {} | 各类事件的回调
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | -| packFn | - | 数据压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) | -| sampling | - | 数据抽样策略,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) | -| dataURLOptions | {} | Canvas 图像快照的格式和质量,这个参数将传递给 OffscreenCanvas.convertToBlob(),使用这个参数能有效减小录制数据的大小 | -| recordCanvas | false | 是否记录 canvas 内容, 可用选项:`false`, `true` | -| recordCrossOriginIframes | false | 是否记录 cross origin iframes。 必须在每个子 iframe 中注入 rrweb 才能使其工作。 可用选项:`false`, `true` | -| recordAfter | 'load' | 如果 document 还没有加载完成,recorder 将会在指定的事件触发后开始录制。可用选项: `DOMContentLoaded`, `load` | -| inlineImages | false | 是否将图片内容记内联录制 | -| collectFonts | false | 是否记录页面中的字体文件 | -| userTriggeredOnInput | false | [什么是 `userTriggered`](https://github.com/rrweb-io/rrweb/pull/495) | -| plugins | [] | 加载插件以获得额外的录制功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) | -| errorHandler | - | 一个可以定制化处理错误的回调函数,它的参数是错误对象。如果 rrweb recorder 内部的某些内容抛出错误,则会调用该回调。 | +| key | 默认值 | 功能 | +| ------------------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| emit | 必填 | 获取当前录制的数据 | +| checkoutEveryNth | - | 每 N 次事件重新制作一次全量快照
详见[“重新制作快照”](#重新制作快照)章节 | +| checkoutEveryNms | - | 每 N 毫秒重新制作一次全量快照
详见[“重新制作快照”](#重新制作快照)章节 | +| blockClass | 'rr-block' | 字符串或正则表达式,可用于自定义屏蔽元素的类名,详见[“隐私”](#隐私)章节 | +| blockSelector | null | 所有 element.matches(blockSelector)为 true 的元素都不会被录制,回放时取而代之的是一个同等宽高的占位元素 | +| ignoreClass | 'rr-ignore' | 字符串或正则表达式,可用于自定义忽略元素的类名,详见[“隐私”](#隐私)章节 | +| ignoreCSSAttributes | null | 应该被忽略的 CSS 属性数组 | +| maskTextClass | 'rr-mask' | 字符串或正则表达式,可用于自定义忽略元素 text 内容的类名,详见[“隐私”](#隐私)章节 | +| maskTextSelector | null | 所有 element.matches(maskTextSelector)为 true 的元素及其子元素的 text 内容将会被屏蔽 | +| maskAllInputs | false | 将所有输入内容记录为 \* | +| maskInputOptions | { password: true } | 选择将特定类型的输入框内容记录为 \*
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | +| maskInputFn | - | 自定义特定类型的输入框内容记录逻辑 | +| maskTextFn | - | 自定义文字内容的记录逻辑 | +| slimDOMOptions | {} | 去除 DOM 中不必要的部分
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) | +| inlineStylesheet | true | 是否将样式表内联 | +| hooks | {} | 各类事件的回调
类型详见[列表](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | +| packFn | - | 数据压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) | +| sampling | - | 数据抽样策略,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) | +| dataURLOptions | {} | Canvas 图像快照的格式和质量,这个参数将传递给 OffscreenCanvas.convertToBlob(),使用这个参数能有效减小录制数据的大小 | +| recordCanvas | false | 是否记录 canvas 内容, 可用选项:`false`, `true` | +| recordCrossOriginIframes | false | 是否记录 cross origin iframes。 必须在每个子 iframe 中注入 rrweb 才能使其工作。 可用选项:`false`, `true` | +| recordAfter | 'load' | 如果 document 还没有加载完成,recorder 将会在指定的事件触发后开始录制。可用选项: `DOMContentLoaded`, `load` | +| ~inlineImages~ | false | 是否将图片内容记内联录制 | +| assetCapture | { objectURLs: true, origins: false } | 配置资源(图像)捕获并生成异步资源事件。
有关更多信息,请参阅[资源捕获文档](./docs/recipes/assets.zh_CN.md) | +| collectFonts | false | 是否记录页面中的字体文件 | +| userTriggeredOnInput | false | [什么是 `userTriggered`](https://github.com/rrweb-io/rrweb/pull/495) | +| plugins | [] | 加载插件以获得额外的录制功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) | +| errorHandler | - | 一个可以定制化处理错误的回调函数,它的参数是错误对象。如果 rrweb recorder 内部的某些内容抛出错误,则会调用该回调。 | #### 隐私 diff --git a/package.json b/package.json index 60e7b9e5b9..755158ac57 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "test": "yarn turbo run test --concurrency=1 --continue", "test:watch": "yarn turbo run test:watch", "test:update": "yarn turbo run test:update", - "check-types": "yarn turbo run check-types --continue", + "check-types": "yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references --check' 'yarn turbo run check-types --continue'", "format": "yarn prettier --write '**/*.{ts,md}'", "format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write", "dev": "yarn turbo run dev --concurrency=18", diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 9985597284..5bef139b68 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -418,6 +418,9 @@ function serializeNode( maskTextFn: MaskTextFn | undefined; maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages: boolean; recordCanvas: boolean; keepIframeSrcFn: KeepIframeSrcFn; @@ -577,6 +580,9 @@ function serializeElementNode( maskInputOptions: MaskInputOptions; maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages: boolean; recordCanvas: boolean; keepIframeSrcFn: KeepIframeSrcFn; @@ -964,6 +970,9 @@ export function serializeNodeWithId( slimDOMOptions: SlimDOMOptions; dataURLOptions?: DataURLOptions; keepIframeSrcFn?: KeepIframeSrcFn; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages?: boolean; recordCanvas?: boolean; preserveWhiteSpace?: boolean; @@ -1284,6 +1293,9 @@ function snapshot( maskInputFn?: MaskInputFn; slimDOM?: 'all' | boolean | SlimDOMOptions; dataURLOptions?: DataURLOptions; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages?: boolean; recordCanvas?: boolean; preserveWhiteSpace?: boolean; diff --git a/packages/rrweb/scripts/stream.js b/packages/rrweb/scripts/stream.js index b4e63a7d31..6473cdbcc5 100644 --- a/packages/rrweb/scripts/stream.js +++ b/packages/rrweb/scripts/stream.js @@ -49,7 +49,10 @@ async function injectRecording(frame, serverURL) { recordCanvas: false, recordCrossOriginIframes: true, collectFonts: true, - inlineImages: true, + assetCapture: { + objectURLs: true, + origins: false, + }, }); })(); }); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index b753ad734e..9c37265c98 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -183,6 +183,9 @@ export default class MutationBuffer { private maskInputFn: observerParam['maskInputFn']; private keepIframeSrcFn: observerParam['keepIframeSrcFn']; private recordCanvas: observerParam['recordCanvas']; + /** + * @deprecated please use `assetCapture` instead + */ private inlineImages: observerParam['inlineImages']; private slimDOMOptions: observerParam['slimDOMOptions']; private dataURLOptions: observerParam['dataURLOptions']; @@ -315,6 +318,7 @@ export default class MutationBuffer { if (parentId === -1 || nextId === -1) { return addList.addNode(n); } + const sn = serializeNodeWithId(n, { doc: this.doc, mirror: this.mirror, diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 2487fa4e2c..d5ad0a77ce 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -70,6 +70,9 @@ export type recordOptions = { recordAfter?: 'DOMContentLoaded' | 'load'; userTriggeredOnInput?: boolean; collectFonts?: boolean; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages?: boolean; assetCapture?: assetCaptureParam; plugins?: RecordPlugin[]; @@ -107,6 +110,9 @@ export type observerParam = { sampling: SamplingStrategy; recordDOM: boolean; recordCanvas: boolean; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages: boolean; userTriggeredOnInput: boolean; collectFonts: boolean; From ffffddeb772d1879524ccce43ccd50b4c8bcaaf6 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:05:22 +0100 Subject: [PATCH 025/197] Fix rrdom build --- packages/rrdom-nodejs/tsconfig.json | 4 +--- packages/rrdom/jest.config.js | 1 + packages/rrdom/jest.setup.ts | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 packages/rrdom/jest.setup.ts diff --git a/packages/rrdom-nodejs/tsconfig.json b/packages/rrdom-nodejs/tsconfig.json index 05e2c1bb8f..8f1d20380a 100644 --- a/packages/rrdom-nodejs/tsconfig.json +++ b/packages/rrdom-nodejs/tsconfig.json @@ -1,8 +1,6 @@ { "extends": "../../tsconfig.base.json", - "include": [ - "src" - ], + "include": ["src"], "compilerOptions": { "rootDir": "src", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" diff --git a/packages/rrdom/jest.config.js b/packages/rrdom/jest.config.js index 50c5bb3b1b..7893d1b3b9 100644 --- a/packages/rrdom/jest.config.js +++ b/packages/rrdom/jest.config.js @@ -2,6 +2,7 @@ export default { preset: 'ts-jest', testEnvironment: 'node', + setupFiles: ['./jest.setup.ts'], /** * Keeps old (pre-jest 29) snapshot format * its a bit ugly and harder to read than the new format, diff --git a/packages/rrdom/jest.setup.ts b/packages/rrdom/jest.setup.ts new file mode 100644 index 0000000000..7c97c034f2 --- /dev/null +++ b/packages/rrdom/jest.setup.ts @@ -0,0 +1,3 @@ +import { TextEncoder, TextDecoder } from 'util'; + +Object.assign(global, { TextDecoder, TextEncoder }); From 95e7969f21be359c0f2cf93b0c3ffe8e24f1e660 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:09:57 +0100 Subject: [PATCH 026/197] Fix deprecated test --- packages/rrweb-snapshot/test/integration.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 1cc6acffea..5ea6095a8c 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -344,7 +344,7 @@ iframe.contentDocument.querySelector('center').clientHeight assert(snapshot.includes('data:image/webp;base64,')); }); - it('correctly saves blob:images in iframes offline', async () => { + it('[deprecated] correctly saves blob:images in iframes offline', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto(`${serverURL}/html/picture-blob-in-frame.html`, { @@ -359,6 +359,10 @@ iframe.contentDocument.querySelector('center').clientHeight inlineStylesheet: false, onIframeLoad: function(iframe, sn) { window.snapshot = sn; + }, + assetCapture: { + origin: false, + objectURLs: false } })`); await waitForRAF(page); From 3dfb353a128a120935ea10d2750a1c4f5a8d4143 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:47:37 +0100 Subject: [PATCH 027/197] Rename `assetCapture` to `captureAssets` Also deprecate a couple inlineImages tests --- docs/recipes/assets.md | 10 +- docs/recipes/assets.zh_CN.md | 10 +- guide.md | 4 +- guide.zh_CN.md | 2 +- .../cross-origin-iframe-packer.test.ts.snap | 6 +- .../test/__snapshots__/index.test.ts.snap | 4 +- packages/rrweb-snapshot/src/snapshot.ts | 8 +- .../rrweb-snapshot/test/integration.test.ts | 2 +- packages/rrweb/scripts/stream.js | 2 +- packages/rrweb/src/record/index.ts | 6 +- packages/rrweb/src/record/mutation.ts | 2 +- .../src/record/observers/asset-manager.ts | 8 +- packages/rrweb/src/types.ts | 8 +- .../__snapshots__/integration.test.ts.snap | 1612 +++++++++++++++-- .../test/__snapshots__/record.test.ts.snap | 382 +--- packages/rrweb/test/integration.test.ts | 75 +- .../cross-origin-iframes.test.ts.snap | 206 +-- .../record/__snapshots__/webgl.test.ts.snap | 16 +- packages/rrweb/test/utils.ts | 2 +- packages/types/src/index.ts | 4 +- 20 files changed, 1620 insertions(+), 749 deletions(-) diff --git a/docs/recipes/assets.md b/docs/recipes/assets.md index e364b7eae8..0fc0c103cd 100644 --- a/docs/recipes/assets.md +++ b/docs/recipes/assets.md @@ -4,11 +4,11 @@ ## Inline Images (Deprecated) -The `inlineImages` configuration option is deprecated and should not be used anymore. It has some issues, namely rewriting events that are already emitted which might make you miss the inlined image if the event has already been sent to the server. Instead, use the `assetCapture` option to configure asset capture. +The `inlineImages` configuration option is deprecated and should not be used anymore. It has some issues, namely rewriting events that are already emitted which might make you miss the inlined image if the event has already been sent to the server. Instead, use the `captureAssets` option to configure asset capture. ## Asset Capture Configuration -The `assetCapture` configuration option allows you to customize the asset capture process. It is an object with the following properties: +The `captureAssets` configuration option allows you to customize the asset capture process. It is an object with the following properties: - `objectURLs` (default: `true`): This property specifies whether to capture same-origin `blob:` assets using object URLs. Object URLs are created using the `URL.createObjectURL()` method. Setting `objectURLs` to `true` enables the capture of object URLs. @@ -24,7 +24,7 @@ Here is the TypeScript type definition for the `recordOptions` object, which inc ```typescript export type recordOptions = { // Other configuration options... - assetCapture?: { + captureAssets?: { objectURLs: boolean; origins: string[] | true | false; }; @@ -33,8 +33,8 @@ export type recordOptions = { }; ``` -This type definition shows that `assetCapture` is an optional property of the `recordOptions` object. It contains the `objectURLs` and `origins` properties, which have the same meanings as described above. +This type definition shows that `captureAssets` is an optional property of the `recordOptions` object. It contains the `objectURLs` and `origins` properties, which have the same meanings as described above. ## Conclusion -By configuring the `assetCapture` option in rrweb, you can control how assets like images are captured during the recording process. This allows you to customize which assets are included in the recorded interactions on your website. +By configuring the `captureAssets` option in rrweb, you can control how assets like images are captured during the recording process. This allows you to customize which assets are included in the recorded interactions on your website. diff --git a/docs/recipes/assets.zh_CN.md b/docs/recipes/assets.zh_CN.md index fcd496b14a..7f0b6a9943 100644 --- a/docs/recipes/assets.zh_CN.md +++ b/docs/recipes/assets.zh_CN.md @@ -4,11 +4,11 @@ ## 内联图像(已弃用) -`inlineImages` 配置选项已被弃用,不应再使用。它存在一些问题,即重写已经发出的事件,这可能使您错过已发送到服务器的内联图像。相反,请使用 `assetCapture` 选项来配置资源捕获。 +`inlineImages` 配置选项已被弃用,不应再使用。它存在一些问题,即重写已经发出的事件,这可能使您错过已发送到服务器的内联图像。相反,请使用 `captureAssets` 选项来配置资源捕获。 ## 资源捕获配置 -`assetCapture` 配置选项允许您自定义资源捕获过程。它是一个具有以下属性的对象: +`captureAssets` 配置选项允许您自定义资源捕获过程。它是一个具有以下属性的对象: - `objectURLs`(默认值:`true`):此属性指定是否使用对象 URL 捕获同源 `blob:` 资源。对象 URL 是使用 `URL.createObjectURL()` 方法创建的。将 `objectURLs` 设置为 `true` 可以启用对象 URL 的捕获。 @@ -24,7 +24,7 @@ ```typescript export type recordOptions = { // 其他配置选项... - assetCapture?: { + captureAssets?: { objectURLs: boolean; origins: string[] | true | false; }; @@ -33,8 +33,8 @@ export type recordOptions = { }; ``` -这种类型定义表明 assetCapture 是 recordOptions 对象的一个可选属性。它包含 objectURLs 和 origins 属性,其含义与上述相同。 +这种类型定义表明 captureAssets 是 recordOptions 对象的一个可选属性。它包含 objectURLs 和 origins 属性,其含义与上述相同。 ## 结论 -通过在 rrweb 中配置 assetCapture 选项,您可以控制在记录过程中如何捕获像图像这样的资源。这允许您 +通过在 rrweb 中配置 captureAssets 选项,您可以控制在记录过程中如何捕获像图像这样的资源。这允许您 diff --git a/guide.md b/guide.md index afb7a686c3..2b7ffb1e4a 100644 --- a/guide.md +++ b/guide.md @@ -156,8 +156,8 @@ The parameter of `rrweb.record` accepts the following options. | recordCanvas | false | Whether to record the canvas element. Available options:
`false`,
`true` | | recordCrossOriginIframes | false | Whether to record cross origin iframes. rrweb has to be injected in each child iframe for this to work. Available options:
`false`,
`true` | | recordAfter | 'load' | If the document is not ready, then the recorder will start recording after the specified event is fired. Available options: `DOMContentLoaded`, `load` | -| ~inlineImages~ | false | whether to record the image content (deprecated, use `assetCapture` instead) | -| assetCapture | { objectURLs: true, origins: false } | Configure the asset (image) capture and generates async asset events.
Refer to the [asset capture documentation](./docs/recipes/assets.md) for more info. | +| ~inlineImages~ | false | whether to record the image content (deprecated, use `captureAssets` instead) | +| captureAssets | { objectURLs: true, origins: false } | Configure the asset (image) capture and generates async asset events.
Refer to the [asset capture documentation](./docs/recipes/assets.md) for more info. | | collectFonts | false | whether to collect fonts in the website | | userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) | | plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) | diff --git a/guide.zh_CN.md b/guide.zh_CN.md index 88df0a618d..f9f6891c18 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -153,7 +153,7 @@ setInterval(save, 10 * 1000); | recordCrossOriginIframes | false | 是否记录 cross origin iframes。 必须在每个子 iframe 中注入 rrweb 才能使其工作。 可用选项:`false`, `true` | | recordAfter | 'load' | 如果 document 还没有加载完成,recorder 将会在指定的事件触发后开始录制。可用选项: `DOMContentLoaded`, `load` | | ~inlineImages~ | false | 是否将图片内容记内联录制 | -| assetCapture | { objectURLs: true, origins: false } | 配置资源(图像)捕获并生成异步资源事件。
有关更多信息,请参阅[资源捕获文档](./docs/recipes/assets.zh_CN.md) | +| captureAssets | { objectURLs: true, origins: false } | 配置资源(图像)捕获并生成异步资源事件。
有关更多信息,请参阅[资源捕获文档](./docs/recipes/assets.zh_CN.md) | | collectFonts | false | 是否记录页面中的字体文件 | | userTriggeredOnInput | false | [什么是 `userTriggered`](https://github.com/rrweb-io/rrweb/pull/495) | | plugins | [] | 加载插件以获得额外的录制功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) | diff --git a/packages/all/test/__snapshots__/cross-origin-iframe-packer.test.ts.snap b/packages/all/test/__snapshots__/cross-origin-iframe-packer.test.ts.snap index b550cf9bde..308891daac 100644 --- a/packages/all/test/__snapshots__/cross-origin-iframe-packer.test.ts.snap +++ b/packages/all/test/__snapshots__/cross-origin-iframe-packer.test.ts.snap @@ -7,7 +7,11 @@ exports[`cross origin iframes & packer > blank.html > should support packFn opti \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"captureAssets\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } }, \\"v\\": \\"v1\\" }, diff --git a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap index 001660e949..7b79e153a2 100644 --- a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap +++ b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap @@ -8,7 +8,7 @@ exports[`rrweb-plugin-console-record > should handle recursive console messages \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -196,7 +196,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 5bef139b68..cf6759a0ca 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -419,7 +419,7 @@ function serializeNode( maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages: boolean; recordCanvas: boolean; @@ -581,7 +581,7 @@ function serializeElementNode( maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages: boolean; recordCanvas: boolean; @@ -971,7 +971,7 @@ export function serializeNodeWithId( dataURLOptions?: DataURLOptions; keepIframeSrcFn?: KeepIframeSrcFn; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages?: boolean; recordCanvas?: boolean; @@ -1294,7 +1294,7 @@ function snapshot( slimDOM?: 'all' | boolean | SlimDOMOptions; dataURLOptions?: DataURLOptions; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages?: boolean; recordCanvas?: boolean; diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 5ea6095a8c..9518db54d0 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -360,7 +360,7 @@ iframe.contentDocument.querySelector('center').clientHeight onIframeLoad: function(iframe, sn) { window.snapshot = sn; }, - assetCapture: { + captureAssets: { origin: false, objectURLs: false } diff --git a/packages/rrweb/scripts/stream.js b/packages/rrweb/scripts/stream.js index 6473cdbcc5..166e288d24 100644 --- a/packages/rrweb/scripts/stream.js +++ b/packages/rrweb/scripts/stream.js @@ -49,7 +49,7 @@ async function injectRecording(frame, serverURL) { recordCanvas: false, recordCrossOriginIframes: true, collectFonts: true, - assetCapture: { + captureAssets: { objectURLs: true, origins: false, }, diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 797b4ddc63..994aab9817 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -98,7 +98,7 @@ function record( userTriggeredOnInput = false, collectFonts = false, inlineImages = false, - assetCapture = { + captureAssets = { objectURLs: true, origins: false, }, @@ -343,7 +343,7 @@ function record( assetManager = new AssetManager({ mutationCb: wrappedAssetEmit, win: window, - assetCapture, + captureAssets, }); const shadowDomManager = new ShadowDomManager({ @@ -384,7 +384,7 @@ function record( href: window.location.href, width: getWindowWidth(), height: getWindowHeight(), - assetCapture, + captureAssets, }, }, isCheckout, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 9c37265c98..adc138e796 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -184,7 +184,7 @@ export default class MutationBuffer { private keepIframeSrcFn: observerParam['keepIframeSrcFn']; private recordCanvas: observerParam['recordCanvas']; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ private inlineImages: observerParam['inlineImages']; private slimDOMOptions: observerParam['slimDOMOptions']; diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts index 5f92cd1da4..6ce88c2143 100644 --- a/packages/rrweb/src/record/observers/asset-manager.ts +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -18,7 +18,7 @@ export default class AssetManager { private resetHandlers: listenerHandler[] = []; private mutationCb: assetCallback; public readonly config: Exclude< - recordOptions['assetCapture'], + recordOptions['captureAssets'], undefined >; @@ -33,15 +33,15 @@ export default class AssetManager { constructor(options: { mutationCb: assetCallback; win: IWindow; - assetCapture: Exclude< - recordOptions['assetCapture'], + captureAssets: Exclude< + recordOptions['captureAssets'], undefined >; }) { const { win } = options; this.mutationCb = options.mutationCb; - this.config = options.assetCapture; + this.config = options.captureAssets; const urlObjectMap = this.urlObjectMap; diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index d5ad0a77ce..ce4af59648 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -38,7 +38,7 @@ import type { viewportResizeCallback, PackFn, UnpackFn, - assetCaptureParam, + captureAssetsParam, } from '@rrweb/types'; import type ProcessedNodeManager from './record/processed-node-manager'; import type AssetManager from './record/observers/asset-manager'; @@ -71,10 +71,10 @@ export type recordOptions = { userTriggeredOnInput?: boolean; collectFonts?: boolean; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages?: boolean; - assetCapture?: assetCaptureParam; + captureAssets?: captureAssetsParam; plugins?: RecordPlugin[]; // departed, please use sampling options mousemoveWait?: number; @@ -111,7 +111,7 @@ export type observerParam = { recordDOM: boolean; recordCanvas: boolean; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages: boolean; userTriggeredOnInput: boolean; diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 51e3747b83..fc13c501db 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -1,5 +1,1014 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`record integration tests > [DEPRECATED] should record images inside iframe with blob url 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080, + \\"captureAssets\\": { + \\"objectURLs\\": false, + \\"origins\\": false + } + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Frame with image\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"four\\", + \\"frameborder\\": \\"0\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 20 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 21, + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Image with blob:url\\", + \\"rootId\\": 21, + \\"id\\": 33 + } + ], + \\"rootId\\": 21, + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 34 + } + ], + \\"rootId\\": 21, + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 37 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 21, + \\"id\\": 39 + } + ], + \\"rootId\\": 21, + \\"id\\": 38 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 21, + \\"id\\": 40 + } + ], + \\"rootId\\": 21, + \\"id\\": 36 + } + ], + \\"rootId\\": 21, + \\"id\\": 23 + } + ], + \\"id\\": 21 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 36, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:xxxx/...\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 41 + } + } + ] + } + } +]" +`; + +exports[`record integration tests > [DEPRECATED] should record images inside iframe with blob url after iframe was reloaded 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080, + \\"captureAssets\\": { + \\"objectURLs\\": false, + \\"origins\\": false + } + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Frame 2\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 21 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"five\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 26 + } + ], + \\"rootId\\": 23, + \\"id\\": 24 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 23 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 27, + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 34 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 36 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 37 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Image with blob:url\\", + \\"rootId\\": 27, + \\"id\\": 39 + } + ], + \\"rootId\\": 27, + \\"id\\": 38 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 40 + } + ], + \\"rootId\\": 27, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 43 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 27, + \\"id\\": 45 + } + ], + \\"rootId\\": 27, + \\"id\\": 44 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 27, + \\"id\\": 46 + } + ], + \\"rootId\\": 27, + \\"id\\": 42 + } + ], + \\"rootId\\": 27, + \\"id\\": 29 + } + ], + \\"id\\": 27 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 42, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:xxxx/...\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 47 + } + } + ] + } + } +]" +`; + +exports[`record integration tests > [DEPRECATED] should record images with blob url 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080, + \\"captureAssets\\": { + \\"objectURLs\\": false, + \\"origins\\": false + } + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Image with blob:url\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 23 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:xxxx/...\\" + }, + \\"childNodes\\": [], + \\"id\\": 24 + } + } + ] + } + } +]" +`; + exports[`record integration tests > can correctly serialize a shader and multiple webgl contexts 1`] = ` "[ { @@ -16,7 +1025,7 @@ exports[`record integration tests > can correctly serialize a shader and multipl \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -434,7 +1443,7 @@ exports[`record integration tests > can freeze mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -618,7 +1627,7 @@ exports[`record integration tests > can mask character data mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -840,7 +1849,7 @@ exports[`record integration tests > can mask character data mutations with regex \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1731,7 +2740,7 @@ exports[`record integration tests > can record attribute mutation 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1906,7 +2915,7 @@ exports[`record integration tests > can record character data muatations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2088,7 +3097,7 @@ exports[`record integration tests > can record childList mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2268,7 +3277,7 @@ exports[`record integration tests > can record clicks 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2559,7 +3568,7 @@ exports[`record integration tests > can record form interactions 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3346,7 +4355,7 @@ exports[`record integration tests > can record node mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4279,9 +5288,241 @@ exports[`record integration tests > can record node mutations 1`] = ` { \\"type\\": 3, \\"data\\": { - \\"source\\": 2, - \\"type\\": 0, - \\"id\\": 70 + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 70 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 36, + \\"attributes\\": { + \\"style\\": { + \\"color\\": [ + \\"black\\", + \\"important\\" + ] + } + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests > can record style changes compactly and preserve css var() functions 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080, + \\"captureAssets\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 4, + \\"attributes\\": { + \\"style\\": \\"background: var(--mystery)\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 4, + \\"attributes\\": { + \\"style\\": \\"background: var(--mystery); background-color: black\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 4, + \\"attributes\\": { + \\"style\\": \\"\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 4, + \\"attributes\\": { + \\"style\\": \\"display:block\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 4, + \\"attributes\\": { + \\"style\\": { + \\"color\\": \\"var(--mystery-color)\\" + } + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 4, + \\"attributes\\": { + \\"style\\": \\"color:var(--mystery-color);display:block;margin:10px\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 4, + \\"attributes\\": { + \\"style\\": { + \\"margin-left\\": \\"Npx\\" + } + } + } + ], + \\"removes\\": [], + \\"adds\\": [] } }, { @@ -4291,13 +5532,11 @@ exports[`record integration tests > can record node mutations 1`] = ` \\"texts\\": [], \\"attributes\\": [ { - \\"id\\": 36, + \\"id\\": 4, \\"attributes\\": { \\"style\\": { - \\"color\\": [ - \\"black\\", - \\"important\\" - ] + \\"margin-top\\": \\"Npx\\", + \\"color\\": false } } } @@ -4309,7 +5548,7 @@ exports[`record integration tests > can record node mutations 1`] = ` ]" `; -exports[`record integration tests > can record style changes compactly and preserve css var() functions 1`] = ` +exports[`record integration tests > can record textarea mutations correctly 1`] = ` "[ { \\"type\\": 0, @@ -4325,7 +5564,7 @@ exports[`record integration tests > can record style changes compactly and prese \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4337,17 +5576,84 @@ exports[`record integration tests > can record style changes compactly and prese \\"node\\": { \\"type\\": 0, \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, { \\"type\\": 2, \\"tagName\\": \\"html\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, \\"childNodes\\": [ { \\"type\\": 2, \\"tagName\\": \\"head\\", \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 3 + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Empty\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 }, { \\"type\\": 2, @@ -4357,7 +5663,21 @@ exports[`record integration tests > can record style changes compactly and prese { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 5 + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"one\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 17 }, { \\"type\\": 2, @@ -4367,24 +5687,23 @@ exports[`record integration tests > can record style changes compactly and prese { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 7 + \\"id\\": 19 } ], - \\"id\\": 6 + \\"id\\": 18 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 8 + \\"id\\": 20 } ], - \\"id\\": 4 + \\"id\\": 14 } ], - \\"id\\": 2 + \\"id\\": 3 } ], - \\"compatMode\\": \\"BackCompat\\", \\"id\\": 1 }, \\"initialOffset\\": { @@ -4398,33 +5717,23 @@ exports[`record integration tests > can record style changes compactly and prese \\"data\\": { \\"source\\": 0, \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 4, - \\"attributes\\": { - \\"style\\": \\"background: var(--mystery)\\" - } - } - ], + \\"attributes\\": [], \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ + \\"adds\\": [ { - \\"id\\": 4, - \\"attributes\\": { - \\"style\\": \\"background: var(--mystery); background-color: black\\" + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"value\\": \\"pre value\\" + }, + \\"childNodes\\": [], + \\"id\\": 21 } } - ], - \\"removes\\": [], - \\"adds\\": [] + ] } }, { @@ -4434,9 +5743,9 @@ exports[`record integration tests > can record style changes compactly and prese \\"texts\\": [], \\"attributes\\": [ { - \\"id\\": 4, + \\"id\\": 21, \\"attributes\\": { - \\"style\\": \\"\\" + \\"value\\": \\"ok\\" } } ], @@ -4451,9 +5760,9 @@ exports[`record integration tests > can record style changes compactly and prese \\"texts\\": [], \\"attributes\\": [ { - \\"id\\": 4, + \\"id\\": 21, \\"attributes\\": { - \\"style\\": \\"display:block\\" + \\"value\\": \\"ok3\\" } } ], @@ -4464,37 +5773,18 @@ exports[`record integration tests > can record style changes compactly and prese { \\"type\\": 3, \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 4, - \\"attributes\\": { - \\"style\\": { - \\"color\\": \\"var(--mystery-color)\\" - } - } - } - ], - \\"removes\\": [], - \\"adds\\": [] + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 21 } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 4, - \\"attributes\\": { - \\"style\\": \\"color:var(--mystery-color);display:block;margin:10px\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] + \\"source\\": 5, + \\"text\\": \\"1ok3\\", + \\"isChecked\\": false, + \\"id\\": 21 } }, { @@ -4504,11 +5794,9 @@ exports[`record integration tests > can record style changes compactly and prese \\"texts\\": [], \\"attributes\\": [ { - \\"id\\": 4, + \\"id\\": 21, \\"attributes\\": { - \\"style\\": { - \\"margin-left\\": \\"Npx\\" - } + \\"value\\": \\"ignore\\" } } ], @@ -4519,21 +5807,10 @@ exports[`record integration tests > can record style changes compactly and prese { \\"type\\": 3, \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 4, - \\"attributes\\": { - \\"style\\": { - \\"margin-top\\": \\"Npx\\", - \\"color\\": false - } - } - } - ], - \\"removes\\": [], - \\"adds\\": [] + \\"source\\": 5, + \\"text\\": \\"12ok3\\", + \\"isChecked\\": false, + \\"id\\": 21 } } ]" @@ -4555,7 +5832,7 @@ exports[`record integration tests > can use maskInputOptions to configure which \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -5430,7 +6707,7 @@ exports[`record integration tests > handles null attribute values 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -5625,7 +6902,7 @@ exports[`record integration tests > mutations should work when blocked class is \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -6395,7 +7672,7 @@ exports[`record integration tests > should mask inputs via function call 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -7269,7 +8546,7 @@ exports[`record integration tests > should mask password value attribute with ma \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -7720,7 +8997,7 @@ exports[`record integration tests > should mask texts 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -8022,7 +9299,7 @@ exports[`record integration tests > should mask texts using maskTextFn 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -8324,7 +9601,7 @@ exports[`record integration tests > should nest record iframe 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -9088,7 +10365,7 @@ exports[`record integration tests > should not record blocked elements and its c \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -9272,7 +10549,7 @@ exports[`record integration tests > should not record blocked elements dynamical \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -9482,7 +10759,7 @@ exports[`record integration tests > should not record input events on ignored el \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -9897,7 +11174,7 @@ exports[`record integration tests > should not record input values if dynamicall \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -10259,7 +11536,7 @@ exports[`record integration tests > should not record input values if maskAllInp \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -11133,7 +12410,7 @@ exports[`record integration tests > should record DOM node movement 1 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -11404,7 +12681,7 @@ exports[`record integration tests > should record DOM node movement 2 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -11682,7 +12959,7 @@ exports[`record integration tests > should record after DOMContentLoaded event 1 \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -11773,7 +13050,7 @@ exports[`record integration tests > should record canvas mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -11996,7 +13273,7 @@ exports[`record integration tests > should record dynamic CSS changes 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -12396,8 +13673,8 @@ exports[`record integration tests > should record images inside iframe with blob \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { - \\"objectURLs\\": false, + \\"captureAssets\\": { + \\"objectURLs\\": true, \\"origins\\": false } } @@ -12729,8 +14006,7 @@ exports[`record integration tests > should record images inside iframe with blob \\"type\\": 2, \\"tagName\\": \\"img\\", \\"attributes\\": { - \\"src\\": \\"blob:http://localhost:xxxx/...\\", - \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + \\"src\\": \\"blob:http://localhost:xxxx/...\\" }, \\"childNodes\\": [], \\"rootId\\": 21, @@ -12739,6 +14015,22 @@ exports[`record integration tests > should record images inside iframe with blob } ] } + }, + { + \\"type\\": 7, + \\"data\\": { + \\"url\\": \\"blob:http://localhost:3030/bf789919-6387-4ce8-b288-c3fe1d6273b3\\", + \\"payload\\": { + \\"rr_type\\": \\"Blob\\", + \\"type\\": \\"text/plain\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"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\\" + } + ] + } + } } ]" `; @@ -12759,8 +14051,8 @@ exports[`record integration tests > should record images inside iframe with blob \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { - \\"objectURLs\\": false, + \\"captureAssets\\": { + \\"objectURLs\\": true, \\"origins\\": false } } @@ -13167,8 +14459,7 @@ exports[`record integration tests > should record images inside iframe with blob \\"type\\": 2, \\"tagName\\": \\"img\\", \\"attributes\\": { - \\"src\\": \\"blob:http://localhost:xxxx/...\\", - \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + \\"src\\": \\"blob:http://localhost:xxxx/...\\" }, \\"childNodes\\": [], \\"rootId\\": 27, @@ -13177,6 +14468,22 @@ exports[`record integration tests > should record images inside iframe with blob } ] } + }, + { + \\"type\\": 7, + \\"data\\": { + \\"url\\": \\"blob:http://localhost:3030/e46dcc7c-6ec5-45ec-9ab6-3957a2613701\\", + \\"payload\\": { + \\"rr_type\\": \\"Blob\\", + \\"type\\": \\"text/plain\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"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\\" + } + ] + } + } } ]" `; @@ -13197,8 +14504,8 @@ exports[`record integration tests > should record images with blob url 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { - \\"objectURLs\\": false, + \\"captureAssets\\": { + \\"objectURLs\\": true, \\"origins\\": false } } @@ -13379,8 +14686,7 @@ exports[`record integration tests > should record images with blob url 1`] = ` \\"type\\": 2, \\"tagName\\": \\"img\\", \\"attributes\\": { - \\"src\\": \\"blob:http://localhost:xxxx/...\\", - \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + \\"src\\": \\"blob:http://localhost:xxxx/...\\" }, \\"childNodes\\": [], \\"id\\": 24 @@ -13388,6 +14694,22 @@ exports[`record integration tests > should record images with blob url 1`] = ` } ] } + }, + { + \\"type\\": 7, + \\"data\\": { + \\"url\\": \\"blob:http://localhost:3030/31c3165c-82ea-4d66-ae50-f57c9e0088c4\\", + \\"payload\\": { + \\"rr_type\\": \\"Blob\\", + \\"type\\": \\"text/plain\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"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\\" + } + ] + } + } } ]" `; @@ -13408,7 +14730,7 @@ exports[`record integration tests > should record input userTriggered values if \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -14312,7 +15634,7 @@ exports[`record integration tests > should record moved shadow DOM 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -14485,7 +15807,7 @@ exports[`record integration tests > should record moved shadow DOM 2 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -14672,7 +15994,7 @@ exports[`record integration tests > should record mutations in iframes accross p \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -14985,7 +16307,7 @@ exports[`record integration tests > should record nested iframes and shadow doms \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -15336,7 +16658,7 @@ exports[`record integration tests > should record shadow DOM 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -15792,7 +17114,7 @@ exports[`record integration tests > should record shadow DOM 2 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -15914,7 +17236,7 @@ exports[`record integration tests > should record shadow DOM 3 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -16036,7 +17358,7 @@ exports[`record integration tests > should record shadow doms polyfilled by shad \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -16286,7 +17608,7 @@ exports[`record integration tests > should record shadow doms polyfilled by synt \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -16982,7 +18304,7 @@ exports[`record integration tests > should record webgl canvas mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -17201,7 +18523,7 @@ exports[`record integration tests > should unmask texts using maskTextFn 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -17503,7 +18825,7 @@ exports[`record integration tests > will serialize node before record 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index b93f880896..094ee5de44 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -8,7 +8,7 @@ exports[`record > aggregates mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -98,7 +98,7 @@ exports[`record > can add custom event 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -196,7 +196,7 @@ exports[`record > captures CORS stylesheets that are still loading 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -303,7 +303,7 @@ exports[`record > captures adopted stylesheets in nested shadow doms and iframes \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -773,7 +773,7 @@ exports[`record > captures adopted stylesheets in shadow doms and iframe 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1173,7 +1173,7 @@ exports[`record > captures adopted stylesheets of shadow doms in checkout full s \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1277,7 +1277,7 @@ exports[`record > captures adopted stylesheets of shadow doms in checkout full s \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1386,7 +1386,7 @@ exports[`record > captures inserted style text nodes correctly 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1518,7 +1518,7 @@ exports[`record > captures mutations on adopted stylesheets 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1822,7 +1822,7 @@ exports[`record > captures nested stylesheet rules 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1975,7 +1975,7 @@ exports[`record > captures style property changes 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2108,7 +2108,7 @@ exports[`record > captures stylesheet rules 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2390,7 +2390,7 @@ exports[`record > captures stylesheets in iframes with \`blob:\` url 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2538,7 +2538,7 @@ exports[`record > captures stylesheets with \`blob:\` url 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2630,7 +2630,7 @@ exports[`record > is safe to checkout during async callbacks 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2765,7 +2765,7 @@ exports[`record > is safe to checkout during async callbacks 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2886,346 +2886,6 @@ exports[`record > is safe to checkout during async callbacks 1`] = ` ]" `; -exports[`record > loading stylesheets > captures stylesheets in iframes that are still loading 1`] = ` -"[ - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 1920, - \\"height\\": 1080, - \\"assetCapture\\": { - \\"objectURLs\\": true, - \\"origins\\": false - } - } - }, - { - \\"type\\": 2, - \\"data\\": { - \\"node\\": { - \\"type\\": 0, - \\"childNodes\\": [ - { - \\"type\\": 1, - \\"name\\": \\"html\\", - \\"publicId\\": \\"\\", - \\"systemId\\": \\"\\", - \\"id\\": 2 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"html\\", - \\"attributes\\": { - \\"lang\\": \\"en\\" - }, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"head\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 5 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"charset\\": \\"UTF-8\\" - }, - \\"childNodes\\": [], - \\"id\\": 6 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 7 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"http-equiv\\": \\"X-UA-Compatible\\", - \\"content\\": \\"IE=edge\\" - }, - \\"childNodes\\": [], - \\"id\\": 8 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 9 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"name\\": \\"viewport\\", - \\"content\\": \\"width=device-width, initial-scale=1.0\\" - }, - \\"childNodes\\": [], - \\"id\\": 10 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 11 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"title\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"Hello World!\\", - \\"id\\": 13 - } - ], - \\"id\\": 12 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 14 - } - ], - \\"id\\": 4 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 15 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\", - \\"id\\": 17 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"iframe\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 18 - } - ], - \\"id\\": 16 - } - ], - \\"id\\": 3 - } - ], - \\"id\\": 1 - }, - \\"initialOffset\\": { - \\"left\\": 0, - \\"top\\": 0 - } - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"adds\\": [ - { - \\"parentId\\": 18, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 0, - \\"childNodes\\": [ - { - \\"type\\": 1, - \\"name\\": \\"html\\", - \\"publicId\\": \\"\\", - \\"systemId\\": \\"\\", - \\"rootId\\": 19, - \\"id\\": 20 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"html\\", - \\"attributes\\": { - \\"lang\\": \\"en\\" - }, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"head\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 19, - \\"id\\": 23 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"charset\\": \\"UTF-8\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 19, - \\"id\\": 24 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 19, - \\"id\\": 25 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"http-equiv\\": \\"X-UA-Compatible\\", - \\"content\\": \\"IE=edge\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 19, - \\"id\\": 26 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 19, - \\"id\\": 27 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"name\\": \\"viewport\\", - \\"content\\": \\"width=device-width, initial-scale=1.0\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 19, - \\"id\\": 28 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 19, - \\"id\\": 29 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"title\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"Hello World!\\", - \\"rootId\\": 19, - \\"id\\": 31 - } - ], - \\"rootId\\": 19, - \\"id\\": 30 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 19, - \\"id\\": 32 - } - ], - \\"rootId\\": 19, - \\"id\\": 22 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 19, - \\"id\\": 33 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\", - \\"rootId\\": 19, - \\"id\\": 35 - } - ], - \\"rootId\\": 19, - \\"id\\": 34 - } - ], - \\"rootId\\": 19, - \\"id\\": 21 - } - ], - \\"id\\": 19 - } - } - ], - \\"removes\\": [], - \\"texts\\": [], - \\"attributes\\": [], - \\"isAttachIframe\\": true - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ - { - \\"parentId\\": 22, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"link\\", - \\"attributes\\": { - \\"rel\\": \\"stylesheet\\", - \\"href\\": \\"http://localhost:3030/html/assets/style.css\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 19, - \\"id\\": 36 - } - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"adds\\": [], - \\"removes\\": [], - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 36, - \\"attributes\\": { - \\"_cssText\\": \\"body { color: pink; }\\" - } - } - ] - } - } -]" -`; - exports[`record > loading stylesheets > captures stylesheets that are still loading 1`] = ` "[ { @@ -3234,7 +2894,7 @@ exports[`record > loading stylesheets > captures stylesheets that are still load \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3418,7 +3078,7 @@ exports[`record > no need for attribute mutations on adds 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3542,7 +3202,7 @@ exports[`record > should record scroll position 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3666,7 +3326,7 @@ exports[`record > without CSSGroupingRule support > captures nested stylesheet r \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3819,7 +3479,7 @@ exports[`record iframes > captures stylesheet mutations in iframes 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 6e7dd19cbc..2ad678aba2 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -915,14 +915,79 @@ describe('record integration tests', function (this: ISuite) { await assertSnapshot(snapshots); }); - it('should record images with blob url', async () => { + it('[DEPRECATED] should record images with blob url', async () => { const page: puppeteer.Page = await browser.newPage(); page.on('console', (msg) => console.log(msg.text())); await page.goto(`${serverURL}/html`); page.setContent( getHtml.call(this, 'image-blob-url.html', { inlineImages: true, - assetCapture: { objectURLs: false, origins: false }, + captureAssets: { objectURLs: false, origins: false }, + }), + ); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); + await page.waitForSelector('img'); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + + it('[DEPRECATED] should record images inside iframe with blob url', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'frame-image-blob-url.html', { + inlineImages: true, + captureAssets: { objectURLs: false, origins: false }, + }), + ); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); + await page.waitForTimeout(50); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + + it('[DEPRECATED] should record images inside iframe with blob url after iframe was reloaded', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'frame2.html', { + inlineImages: true, + captureAssets: { objectURLs: false, origins: false }, + }), + ); + await page.waitForSelector('iframe'); // wait for iframe to get added + await waitForRAF(page); // wait for iframe to load + page.evaluate(() => { + const iframe = document.querySelector('iframe')!; + iframe.setAttribute('src', '/html/image-blob-url.html'); + }); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); // wait for image to get loaded + await page.waitForTimeout(50); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + + it('should record images with blob url', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + page.setContent( + getHtml.call(this, 'image-blob-url.html', { + captureAssets: { objectURLs: true, origins: false }, }), ); await page.waitForResponse(`${serverURL}/html/assets/robot.png`); @@ -941,8 +1006,7 @@ describe('record integration tests', function (this: ISuite) { await page.goto(`${serverURL}/html`); await page.setContent( getHtml.call(this, 'frame-image-blob-url.html', { - inlineImages: true, - assetCapture: { objectURLs: false, origins: false }, + captureAssets: { objectURLs: true, origins: false }, }), ); await page.waitForResponse(`${serverURL}/html/assets/robot.png`); @@ -961,8 +1025,7 @@ describe('record integration tests', function (this: ISuite) { await page.goto(`${serverURL}/html`); await page.setContent( getHtml.call(this, 'frame2.html', { - inlineImages: true, - assetCapture: { objectURLs: false, origins: false }, + captureAssets: { objectURLs: true, origins: false }, }), ); await page.waitForSelector('iframe'); // wait for iframe to get added diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 40e3497c04..bdd70e6c76 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -8,7 +8,7 @@ exports[`cross origin iframes > audio.html > should emit contents of iframe once \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -350,7 +350,7 @@ exports[`cross origin iframes > blank.html > should filter out forwarded cross o \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -616,7 +616,7 @@ exports[`cross origin iframes > blank.html > should record same-origin iframe in \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -864,7 +864,7 @@ exports[`cross origin iframes > form.html > should map input events correctly 1` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1881,7 +1881,7 @@ exports[`cross origin iframes > form.html > should map scroll events correctly 1 \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2541,7 +2541,7 @@ exports[`cross origin iframes > move-node.html > captures mutations on adopted s \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2959,7 +2959,7 @@ exports[`cross origin iframes > move-node.html > captures mutations on styleshee \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3382,7 +3382,7 @@ exports[`cross origin iframes > move-node.html > should record DOM attribute cha \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3674,7 +3674,7 @@ exports[`cross origin iframes > move-node.html > should record DOM node movement \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4063,7 +4063,7 @@ exports[`cross origin iframes > move-node.html > should record DOM node removal \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4353,7 +4353,7 @@ exports[`cross origin iframes > move-node.html > should record DOM text changes \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4643,7 +4643,7 @@ exports[`cross origin iframes > move-node.html > should record canvas elements 1 \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4970,7 +4970,7 @@ exports[`cross origin iframes > move-node.html > should record custom events 1`] \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -5248,184 +5248,6 @@ exports[`cross origin iframes > move-node.html > should record custom events 1`] ]" `; -exports[`same origin iframes > should emit contents of iframe once 1`] = ` -"[ - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 1920, - \\"height\\": 1080, - \\"assetCapture\\": { - \\"objectURLs\\": true, - \\"origins\\": false - } - } - }, - { - \\"type\\": 2, - \\"data\\": { - \\"node\\": { - \\"type\\": 0, - \\"childNodes\\": [ - { - \\"type\\": 1, - \\"name\\": \\"html\\", - \\"publicId\\": \\"\\", - \\"systemId\\": \\"\\", - \\"id\\": 2 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"html\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"head\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": { - \\"type\\": \\"text/javascript\\" - }, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 6 - } - ], - \\"id\\": 5 - } - ], - \\"id\\": 4 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 8 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"iframe\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 9 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", - \\"id\\": 10 - } - ], - \\"id\\": 7 - } - ], - \\"id\\": 3 - } - ], - \\"id\\": 1 - }, - \\"initialOffset\\": { - \\"left\\": 0, - \\"top\\": 0 - } - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"adds\\": [ - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 0, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"html\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"head\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"rootId\\": 11, - \\"id\\": 13 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"rootId\\": 11, - \\"id\\": 14 - } - ], - \\"rootId\\": 11, - \\"id\\": 12 - } - ], - \\"compatMode\\": \\"BackCompat\\", - \\"id\\": 11 - } - } - ], - \\"removes\\": [], - \\"texts\\": [], - \\"attributes\\": [], - \\"isAttachIframe\\": true - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ - { - \\"parentId\\": 13, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": { - \\"type\\": \\"text/javascript\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 11, - \\"id\\": 15 - } - }, - { - \\"parentId\\": 15, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"rootId\\": 11, - \\"id\\": 16 - } - } - ] - } - } -]" -`; - exports[`same origin iframes > should record cross-origin iframe in same-origin iframe 1`] = ` "[ { @@ -5434,7 +5256,7 @@ exports[`same origin iframes > should record cross-origin iframe in same-origin \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } diff --git a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap index f40a0a68d9..e97419473f 100644 --- a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap @@ -8,7 +8,7 @@ exports[`record webgl > recordCanvas FPS > should record snapshots 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -105,7 +105,7 @@ exports[`record webgl > should batch events by RAF 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -270,7 +270,7 @@ exports[`record webgl > will record changes to a canvas element 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -383,7 +383,7 @@ exports[`record webgl > will record changes to a canvas element before the canva \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -531,7 +531,7 @@ exports[`record webgl > will record changes to a canvas element before the canva \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -679,7 +679,7 @@ exports[`record webgl > will record changes to a webgl2 canvas element 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -792,7 +792,7 @@ exports[`record webgl > will record webgl variables 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -925,7 +925,7 @@ exports[`record webgl > will record webgl variables in reverse order 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 790430a921..695f05c004 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -802,7 +802,7 @@ export function generateRecordSnippet(options: recordOptions) { recordAfter: '${options.recordAfter || 'load'}', inlineImages: ${options.inlineImages}, plugins: ${options.plugins}, - assetCapture: ${JSON.stringify(options.assetCapture)}, + captureAssets: ${JSON.stringify(options.captureAssets)}, }); `; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0061cfb187..a5c40dc974 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -41,7 +41,7 @@ export type metaEvent = { href: string; width: number; height: number; - assetCapture?: assetCaptureParam; + captureAssets?: captureAssetsParam; }; }; @@ -61,7 +61,7 @@ export type pluginEvent = { }; }; -export type assetCaptureParam = { +export type captureAssetsParam = { /** * Captures object URLs (blobs, files, media sources). * More info: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL From 5199c548f8e1da72fa8b25b0700804ad674610c9 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:48:41 +0100 Subject: [PATCH 028/197] Remove console.log statements from AssetManager class --- packages/rrweb/src/record/observers/asset-manager.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts index 6ce88c2143..1f465b557e 100644 --- a/packages/rrweb/src/record/observers/asset-manager.ts +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -116,7 +116,6 @@ export default class AssetManager { try { const response = await fetch(url); const blob = await response.blob(); - console.log('getURLObject', url, blob); return blob; } catch (e) { console.warn(`getURLObject failed for ${url}`); @@ -127,7 +126,6 @@ export default class AssetManager { public capture(url: string): { status: 'capturing' | 'captured' | 'error' | 'refused'; } { - console.log('capture', url, this.shouldIgnore(url)); if (this.shouldIgnore(url)) return { status: 'refused' }; if (this.capturedURLs.has(url)) { @@ -138,10 +136,8 @@ export default class AssetManager { return { status: 'error' }; } this.capturingURLs.add(url); - console.log('capturing'); void this.getURLObject(url) .then(async (object) => { - console.log('captured', url); if (object) { let payload: SerializedCanvasArg; if (object instanceof File || object instanceof Blob) { From 69e053d96d1d848a0e2de874394c09aa16a48f13 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:51:36 +0100 Subject: [PATCH 029/197] Rename assetCapture to captureAssets in rrweb test --- packages/rrweb/test/record/asset.test.ts | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index b6b6881e10..27374f95de 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -36,7 +36,7 @@ interface IWindow extends Window { snapshots: eventWithTime[]; } type ExtraOptions = { - assetCapture?: recordOptions['assetCapture']; + captureAssets?: recordOptions['captureAssets']; }; const BASE64_PNG_RECTANGLE = @@ -54,7 +54,7 @@ async function injectRecordScript( (window as unknown as IWindow).snapshots = []; const { record, pack } = (window as unknown as IWindow).rrweb; const config: recordOptions = { - assetCapture: options.assetCapture, + captureAssets: options.captureAssets, emit(event) { (window as unknown as IWindow).snapshots.push(event); (window as unknown as IWindow).emit(event); @@ -105,10 +105,10 @@ const setup = function ( ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); if ( - options?.assetCapture?.origins && - Array.isArray(options.assetCapture.origins) + options?.captureAssets?.origins && + Array.isArray(options.captureAssets.origins) ) { - options.assetCapture.origins = options.assetCapture.origins.map( + options.captureAssets.origins = options.captureAssets.origins.map( (origin) => origin.replace(/\{SERVER_URL\}/g, ctx.serverURL), ); } @@ -141,7 +141,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { objectURLs: true, origins: false, }, @@ -283,7 +283,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { objectURLs: true, origins: false, }, @@ -328,7 +328,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { objectURLs: false, origins: false, }, @@ -408,7 +408,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: false, objectURLs: false, }, @@ -440,7 +440,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: [], objectURLs: false, }, @@ -472,7 +472,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: true, objectURLs: false, }, @@ -509,7 +509,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: true, objectURLs: false, }, @@ -579,7 +579,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: ['{SERVER_URL}'], objectURLs: false, }, @@ -641,7 +641,7 @@ describe('asset caching', function (this: ISuite) { href: expect.any(String), width: expect.any(Number), height: expect.any(Number), - assetCapture: { + captureAssets: { origins: [ctx.serverURL], objectURLs: false, }, From 570310339531ed476a8401a9b7bf3d38fbed76e2 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 30 Nov 2023 16:42:27 +0100 Subject: [PATCH 030/197] Check if url is of cacheable origin --- packages/rrweb-snapshot/src/rebuild.ts | 4 +- packages/rrweb/src/replay/assets/index.ts | 30 +++++++++ packages/rrweb/src/replay/index.ts | 12 +--- packages/rrweb/test/replay/asset-unit.test.ts | 62 +++++++++++++++++-- packages/types/src/index.ts | 2 + 5 files changed, 90 insertions(+), 20 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 6ae854e9ff..7a32cb667e 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -331,9 +331,7 @@ function buildNode( } else { node.setAttribute(name, value.toString()); - if (options.assetManager?.isAttributeCacheable(node, name)) { - options.assetManager.manageAttribute(node, name); - } + options.assetManager?.manageAttribute(node, name); } } catch (error) { // skip invalid attribute diff --git a/packages/rrweb/src/replay/assets/index.ts b/packages/rrweb/src/replay/assets/index.ts index 2c9488ec5c..50f71b78e1 100644 --- a/packages/rrweb/src/replay/assets/index.ts +++ b/packages/rrweb/src/replay/assets/index.ts @@ -3,6 +3,7 @@ import type { RebuildAssetManagerInterface, RebuildAssetManagerStatus, assetEvent, + captureAssetsParam, } from '@rrweb/types'; import { deserializeArg } from '../canvas/deserialize-args'; import { isAttributeCacheable } from '../../utils'; @@ -17,6 +18,11 @@ export default class AssetManager implements RebuildAssetManagerInterface { string, Array<(status: RebuildAssetManagerFinalStatus) => void> > = new Map(); + private config: captureAssetsParam; + + constructor(config: captureAssetsParam) { + this.config = config; + } public async add(event: assetEvent) { const { data } = event; @@ -114,10 +120,32 @@ export default class AssetManager implements RebuildAssetManagerInterface { return isAttributeCacheable(n as Element, attribute); } + public isURLOfCacheableOrigin(url: string): boolean { + if (url.startsWith('data:')) return false; + + const { origins: cachedOrigins, objectURLs } = this.config; + if (objectURLs && url.startsWith(`blob:`)) { + return true; + } + + if (Array.isArray(cachedOrigins)) { + try { + const { origin } = new URL(url); + return cachedOrigins.some((o) => o === origin); + } catch { + return false; + } + } + + return cachedOrigins; + } + public async manageAttribute( node: RRElement | Element, attribute: string, ): Promise { + if (!this.isAttributeCacheable(node, attribute)) return false; + const originalValue = node.getAttribute(attribute); if (!originalValue) return false; @@ -128,6 +156,8 @@ export default class AssetManager implements RebuildAssetManagerInterface { ? getSourcesFromSrcset(originalValue) : [originalValue]; for (const value of values) { + if (!this.isURLOfCacheableOrigin(value)) continue; + promises.push( this.whenReady(value).then((status) => { if ( diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 03eff33725..8f205fcc33 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1871,17 +1871,7 @@ export class Replayer { } else { const targetEl = target as Element | RRElement; targetEl.setAttribute(attributeName, value); - if ( - this.assetManager.isAttributeCacheable( - targetEl, - attributeName, - ) - ) { - void this.assetManager.manageAttribute( - targetEl, - attributeName, - ); - } + void this.assetManager.manageAttribute(targetEl, attributeName); } if ( diff --git a/packages/rrweb/test/replay/asset-unit.test.ts b/packages/rrweb/test/replay/asset-unit.test.ts index 03d2cb6fd4..d6cc70bada 100644 --- a/packages/rrweb/test/replay/asset-unit.test.ts +++ b/packages/rrweb/test/replay/asset-unit.test.ts @@ -3,7 +3,12 @@ */ import AssetManager from '../../src/replay/assets'; -import { EventType, SerializedBlobArg, assetEvent } from '@rrweb/types'; +import { + EventType, + SerializedBlobArg, + assetEvent, + captureAssetsParam, +} from '@rrweb/types'; import { vi } from 'vitest'; describe('AssetManager', () => { @@ -29,7 +34,10 @@ describe('AssetManager', () => { }); beforeEach(() => { - assetManager = new AssetManager(); + assetManager = new AssetManager({ + origins: true, + objectURLs: true, + }); }); afterEach(() => { @@ -142,7 +150,7 @@ describe('AssetManager', () => { await expect(promise).resolves.toEqual({ status: 'reset' }); }); - const validCombinations = [ + const validAttributeCombinations = [ ['img', ['src', 'srcset']], ['video', ['src']], ['audio', ['src']], @@ -154,7 +162,7 @@ describe('AssetManager', () => { ['object', ['src']], ] as const; - const invalidCombinations = [ + const invalidAttributeCombinations = [ ['img', ['href']], ['script', ['href']], ['link', ['src']], @@ -168,7 +176,7 @@ describe('AssetManager', () => { ['object', ['href']], ] as const; - validCombinations.forEach(([tagName, attributes]) => { + validAttributeCombinations.forEach(([tagName, attributes]) => { const element = document.createElement(tagName); attributes.forEach((attribute) => { it(`should correctly identify <${tagName} ${attribute}> as cacheable`, () => { @@ -179,7 +187,7 @@ describe('AssetManager', () => { }); }); - invalidCombinations.forEach(([tagName, attributes]) => { + invalidAttributeCombinations.forEach(([tagName, attributes]) => { const element = document.createElement(tagName); attributes.forEach((attribute) => { it(`should correctly identify <${tagName} ${attribute}> as NOT cacheable`, () => { @@ -190,6 +198,48 @@ describe('AssetManager', () => { }); }); + const validOriginCombinations: Array< + [captureAssetsParam['origins'], string[]] + > = [ + [['http://example.com'], ['http://example.com/image.png']], + [['https://example.com'], ['https://example.com/image.png']], + [['https://example.com:80'], ['https://example.com:80/cgi-bin/image.png']], + [true, ['https://example.com:80/cgi-bin/image.png']], + ]; + + const invalidOriginCombinations: Array< + [captureAssetsParam['origins'], string[]] + > = [ + [['http://example.com'], ['https://example.com/image.png']], + [['https://example.com'], ['https://example.org/image.png']], + [['https://example.com:80'], ['https://example.com:81/image.png']], + [false, ['https://example.com:81/image.png']], + ]; + + validOriginCombinations.forEach(([origin, urls]) => { + const assetManager = new AssetManager({ + origins: origin, + objectURLs: false, + }); + urls.forEach((url) => { + it(`should correctly identify ${url} as cacheable for origin ${origin}`, () => { + expect(assetManager.isURLOfCacheableOrigin(url)).toBe(true); + }); + }); + }); + + invalidOriginCombinations.forEach(([origin, urls]) => { + const assetManager = new AssetManager({ + origins: origin, + objectURLs: false, + }); + urls.forEach((url) => { + it(`should correctly identify ${url} as NOT cacheable for origin ${origin}`, () => { + expect(assetManager.isURLOfCacheableOrigin(url)).toBe(false); + }); + }); + }); + it("should be able to modify a node's attribute once asset is loaded", async () => { const url = 'https://example.com/image.png'; const event: assetEvent = { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a5c40dc974..1ba8e31d99 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -761,11 +761,13 @@ export type RebuildAssetManagerStatus = | RebuildAssetManagerFinalStatus; export declare abstract class RebuildAssetManagerInterface { + constructor(config: captureAssetsParam); abstract add(event: assetEvent): Promise; abstract get(url: string): RebuildAssetManagerStatus; abstract whenReady(url: string): Promise; abstract reset(): void; abstract isAttributeCacheable(n: Element, attribute: string): boolean; + abstract isURLOfCacheableOrigin(url: string): boolean; abstract manageAttribute(n: Element, attribute: string): void; } From 07864ac83191c2f4a1332cced5d3ca0233cae90c Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 30 Nov 2023 16:43:42 +0100 Subject: [PATCH 031/197] Test: Add captureAssets config to meta event --- packages/rrweb/test/record/asset.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index 27374f95de..f16b706750 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -479,6 +479,30 @@ describe('asset caching', function (this: ISuite) { }, ); + it('adds captureAssets config to meta event', async () => { + await ctx.page.waitForNetworkIdle({ idleTime: 100 }); + await waitForRAF(ctx.page); + + const events = await ctx.page?.evaluate( + () => (window as unknown as IWindow).snapshots, + ); + + expect(events).toContainEqual( + expect.objectContaining({ + type: EventType.Meta, + data: { + href: expect.any(String), + width: expect.any(Number), + height: expect.any(Number), + captureAssets: { + origins: true, + objectURLs: false, + }, + }, + }), + ); + }); + it('capture all urls', async () => { await ctx.page.waitForNetworkIdle({ idleTime: 100 }); await waitForRAF(ctx.page); From 8d50cd9f3d8bf2cd1736a85f64ac826d0a3d5c93 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 30 Nov 2023 16:56:11 +0100 Subject: [PATCH 032/197] object urls aren't stable so lets remove them --- packages/rrweb/test/__snapshots__/integration.test.ts.snap | 6 +++--- packages/rrweb/test/utils.ts | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index fc13c501db..2123408378 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -14019,7 +14019,7 @@ exports[`record integration tests > should record images inside iframe with blob { \\"type\\": 7, \\"data\\": { - \\"url\\": \\"blob:http://localhost:3030/bf789919-6387-4ce8-b288-c3fe1d6273b3\\", + \\"url\\": \\"blob:http://localhost:3030/...\\", \\"payload\\": { \\"rr_type\\": \\"Blob\\", \\"type\\": \\"text/plain\\", @@ -14472,7 +14472,7 @@ exports[`record integration tests > should record images inside iframe with blob { \\"type\\": 7, \\"data\\": { - \\"url\\": \\"blob:http://localhost:3030/e46dcc7c-6ec5-45ec-9ab6-3957a2613701\\", + \\"url\\": \\"blob:http://localhost:3030/...\\", \\"payload\\": { \\"rr_type\\": \\"Blob\\", \\"type\\": \\"text/plain\\", @@ -14698,7 +14698,7 @@ exports[`record integration tests > should record images with blob url 1`] = ` { \\"type\\": 7, \\"data\\": { - \\"url\\": \\"blob:http://localhost:3030/31c3165c-82ea-4d66-ae50-f57c9e0088c4\\", + \\"url\\": \\"blob:http://localhost:3030/...\\", \\"payload\\": { \\"rr_type\\": \\"Blob\\", \\"type\\": \\"text/plain\\", diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 695f05c004..4b3525f686 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -140,6 +140,9 @@ export function stringifySnapshots(snapshots: eventWithTime[]): string { delete (s.data as Optional).x; delete (s.data as Optional).y; } + if (s.type === EventType.Asset) { + s.data.url = s.data.url.replace(/\/[a-f0-9\-]+$/, '/...'); + } if ( s.type === EventType.IncrementalSnapshot && s.data.source === IncrementalSource.Mutation From 0038b3fc62d0d82dfa419495f5acd90edbac0526 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 30 Nov 2023 16:57:01 +0100 Subject: [PATCH 033/197] inlineImages should work next to captureAssets for now --- packages/rrweb-snapshot/src/snapshot.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index cf6759a0ca..78d1fdb378 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -740,9 +740,10 @@ function serializeElementNode( if (attributes.srcset) { assets.push(...getUrlsFromSrcset(attributes.srcset.toString())); } - // TODO: decide if inlineImages should still be supported, - // and if so if it should be moved into `rrweb` package. - } else if (tagName === 'img' && inlineImages) { + } + + // `inlineImages` is deprecated and will be removed in rrweb 3.x. + if (tagName === 'img' && inlineImages) { if (!canvasService) { canvasService = doc.createElement('canvas'); canvasCtx = canvasService.getContext('2d'); From 8ac642bcfca7934bb1d58157be5ca6f7d204cdfa Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 1 Dec 2023 10:55:29 +0100 Subject: [PATCH 034/197] Fix asset caching bug --- packages/rrweb-snapshot/src/snapshot.ts | 23 +++---- packages/rrweb-snapshot/src/utils.ts | 22 +++++++ .../src/record/observers/asset-manager.ts | 4 +- packages/rrweb/src/replay/assets/index.ts | 20 +++--- packages/rrweb/src/replay/index.ts | 8 ++- packages/rrweb/src/utils.ts | 22 ------- packages/rrweb/test/events/assets-mutation.ts | 8 ++- packages/rrweb/test/html/assets/subtitles.vtt | 16 +++++ packages/rrweb/test/record/asset.test.ts | 65 +++++++++++++------ .../test/replay/asset-integration.test.ts | 2 + packages/types/src/index.ts | 4 +- 11 files changed, 123 insertions(+), 71 deletions(-) create mode 100644 packages/rrweb/test/html/assets/subtitles.vtt diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 78d1fdb378..9e3197ec9a 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -31,6 +31,7 @@ import { absolutifyURLs, markCssSplits, getUrlsFromSrcset, + isAttributeCacheable, } from './utils'; import dom from '@rrweb/utils'; @@ -623,12 +624,21 @@ function serializeElementNode( for (let i = 0; i < len; i++) { const attr = n.attributes[i]; if (!ignoreAttribute(tagName, attr.name, attr.value)) { - attributes[attr.name] = transformAttribute( + const value = (attributes[attr.name] = transformAttribute( doc, tagName, toLowerCase(attr.name), attr.value, - ); + )); + + // save assets offline + if (value && onAssetDetected && isAttributeCacheable(n, attr.name)) { + if (attr.name === 'srcset') { + assets.push(...getUrlsFromSrcset(value)); + } else { + assets.push(value); + } + } } } // remote css @@ -732,15 +742,6 @@ function serializeElementNode( } } } - // save image offline - if (tagName === 'img' && onAssetDetected) { - if (attributes.src) { - assets.push(attributes.src.toString()); - } - if (attributes.srcset) { - assets.push(...getUrlsFromSrcset(attributes.srcset.toString())); - } - } // `inlineImages` is deprecated and will be removed in rrweb 3.x. if (tagName === 'img' && inlineImages) { diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 5693cd99ee..d62fb2dc84 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -613,3 +613,25 @@ export function getUrlsFromSrcset(srcset: string): string[] { } return urls; } + +export const CACHEABLE_ELEMENT_ATTRIBUTE_COMBINATIONS = new Map([ + ['IMG', new Set(['src', 'srcset'])], + ['VIDEO', new Set(['src'])], + ['AUDIO', new Set(['src'])], + ['EMBED', new Set(['src'])], + ['SOURCE', new Set(['src'])], + ['TRACK', new Set(['src'])], + ['INPUT', new Set(['src'])], + ['IFRAME', new Set(['src'])], + ['OBJECT', new Set(['src'])], +]); + +export function isAttributeCacheable(n: Element, attribute: string): boolean { + const acceptedAttributesSet = CACHEABLE_ELEMENT_ATTRIBUTE_COMBINATIONS.get( + n.nodeName, + ); + if (!acceptedAttributesSet) { + return false; + } + return acceptedAttributesSet.has(attribute); +} diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts index 1f465b557e..3dea10ddd1 100644 --- a/packages/rrweb/src/record/observers/asset-manager.ts +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -7,8 +7,10 @@ import type { import type { assetCallback } from '@rrweb/types'; import { encode } from 'base64-arraybuffer'; -import { isAttributeCacheable, patch } from '../../utils'; +import { patch } from '../../utils'; + import type { recordOptions } from '../../types'; +import { isAttributeCacheable } from 'rrweb-snapshot'; export default class AssetManager { private urlObjectMap = new Map(); diff --git a/packages/rrweb/src/replay/assets/index.ts b/packages/rrweb/src/replay/assets/index.ts index 50f71b78e1..5c4cad1123 100644 --- a/packages/rrweb/src/replay/assets/index.ts +++ b/packages/rrweb/src/replay/assets/index.ts @@ -6,8 +6,7 @@ import type { captureAssetsParam, } from '@rrweb/types'; import { deserializeArg } from '../canvas/deserialize-args'; -import { isAttributeCacheable } from '../../utils'; -import { getSourcesFromSrcset } from 'rrweb-snapshot'; +import { getSourcesFromSrcset, isAttributeCacheable } from 'rrweb-snapshot'; import type { RRElement } from 'rrdom'; export default class AssetManager implements RebuildAssetManagerInterface { @@ -18,9 +17,9 @@ export default class AssetManager implements RebuildAssetManagerInterface { string, Array<(status: RebuildAssetManagerFinalStatus) => void> > = new Map(); - private config: captureAssetsParam; + private config: captureAssetsParam | undefined; - constructor(config: captureAssetsParam) { + constructor(config: captureAssetsParam | undefined) { this.config = config; } @@ -123,7 +122,8 @@ export default class AssetManager implements RebuildAssetManagerInterface { public isURLOfCacheableOrigin(url: string): boolean { if (url.startsWith('data:')) return false; - const { origins: cachedOrigins, objectURLs } = this.config; + const { origins: cachedOrigins = false, objectURLs = false } = + this.config || {}; if (objectURLs && url.startsWith(`blob:`)) { return true; } @@ -149,14 +149,13 @@ export default class AssetManager implements RebuildAssetManagerInterface { const originalValue = node.getAttribute(attribute); if (!originalValue) return false; - const promises = []; + const promises: Promise[] = []; const values = attribute === 'srcset' ? getSourcesFromSrcset(originalValue) : [originalValue]; - for (const value of values) { - if (!this.isURLOfCacheableOrigin(value)) continue; + values.forEach((value) => { promises.push( this.whenReady(value).then((status) => { @@ -170,11 +169,12 @@ export default class AssetManager implements RebuildAssetManagerInterface { } }), ); - } + }); return Promise.all(promises); } - public reset(): void { + public reset(config: captureAssetsParam | undefined): void { + this.config = config; this.originalToObjectURLMap.forEach((objectURL) => { URL.revokeObjectURL(objectURL); }); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 8f205fcc33..f2d5f7e723 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -400,7 +400,9 @@ export class Replayer { (e) => e.type === EventType.FullSnapshot, ); if (firstMeta) { - const { width, height } = firstMeta.data as metaEvent['data']; + const { width, height, captureAssets } = + firstMeta.data as metaEvent['data']; + this.assetManager.reset(captureAssets); setTimeout(() => { this.emitter.emit(ReplayerEvents.Resize, { width, @@ -682,11 +684,13 @@ export class Replayer { }; break; case EventType.Meta: - castFn = () => + castFn = () => { + this.assetManager.reset(event.data.captureAssets); this.emitter.emit(ReplayerEvents.Resize, { width: event.data.width, height: event.data.height, }); + }; break; case EventType.FullSnapshot: castFn = () => { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index a559778e84..13b15ec6c1 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -543,25 +543,3 @@ export function inDom(n: Node): boolean { if (!doc) return false; return dom.contains(doc, n) || shadowHostInDom(n); } - -export const CACHEABLE_ELEMENT_ATTRIBUTE_COMBINATIONS = new Map([ - ['IMG', new Set(['src', 'srcset'])], - ['VIDEO', new Set(['src'])], - ['AUDIO', new Set(['src'])], - ['EMBED', new Set(['src'])], - ['SOURCE', new Set(['src'])], - ['TRACK', new Set(['src'])], - ['INPUT', new Set(['src'])], - ['IFRAME', new Set(['src'])], - ['OBJECT', new Set(['src'])], -]); - -export function isAttributeCacheable(n: Element, attribute: string): boolean { - const acceptedAttributesSet = CACHEABLE_ELEMENT_ATTRIBUTE_COMBINATIONS.get( - n.nodeName, - ); - if (!acceptedAttributesSet) { - return false; - } - return acceptedAttributesSet.has(attribute); -} diff --git a/packages/rrweb/test/events/assets-mutation.ts b/packages/rrweb/test/events/assets-mutation.ts index 966c29678a..dda8274f84 100644 --- a/packages/rrweb/test/events/assets-mutation.ts +++ b/packages/rrweb/test/events/assets-mutation.ts @@ -7,6 +7,10 @@ const events: eventWithTime[] = [ href: '', width: 1600, height: 900, + captureAssets: { + origins: ['ftp://example.com'], + objectURLs: false, + }, }, timestamp: 1636379531385, }, @@ -109,7 +113,7 @@ const events: eventWithTime[] = [ { id: 16, attributes: { - src: 'httpx://example.com/image.png', + src: 'ftp://example.com/image.png', }, }, ], @@ -121,7 +125,7 @@ const events: eventWithTime[] = [ { type: EventType.Asset, data: { - url: 'httpx://example.com/image.png', + url: 'ftp://example.com/image.png', payload: { rr_type: 'Blob', type: 'image/png', diff --git a/packages/rrweb/test/html/assets/subtitles.vtt b/packages/rrweb/test/html/assets/subtitles.vtt new file mode 100644 index 0000000000..c56d8d687d --- /dev/null +++ b/packages/rrweb/test/html/assets/subtitles.vtt @@ -0,0 +1,16 @@ +WEBVTT + +00:00:00.000 --> 00:00:00.999 line:80% +Hildy! + +00:00:01.000 --> 00:00:01.499 line:80% +How are you? + +00:00:01.500 --> 00:00:02.999 line:80% +Tell me, is the lord of the universe in? + +00:00:03.000 --> 00:00:04.299 line:80% +Yes, he's in - in a bad humor + +00:00:04.300 --> 00:00:06.000 line:80% +Somebody must've stolen the crown jewels diff --git a/packages/rrweb/test/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index f16b706750..5bb54f0b0f 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -597,8 +597,15 @@ describe('asset caching', function (this: ISuite) { - - + + +