From 63da3e2425e3e013cbf5788f90d9340ee031960e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 8 Jun 2023 16:12:54 +0200 Subject: [PATCH 001/102] Add Asset event type and capture assets --- packages/rrweb-snapshot/package.json | 2 +- packages/rrweb-snapshot/src/snapshot.ts | 53 +- 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, 979 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 14eb0fe0d9..e92c170a19 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -58,6 +58,6 @@ "rollup-plugin-typescript2": "^0.31.2", "ts-jest": "^27.0.5", "ts-node": "^7.0.1", - "tslib": "^1.9.3" + "tslib": "^2.5.3" } } diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index d323e1af8c..fe9e27edf4 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -25,6 +25,7 @@ import { getInputType, toLowerCase, extractFileExtension, + getUrlsFromSrcset, } from './utils'; let _id = 1; @@ -447,6 +448,13 @@ function serializeNode( * `newlyAddedElement: true` skips scrollTop and scrollLeft check */ newlyAddedElement?: 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 { @@ -464,6 +472,7 @@ function serializeNode( recordCanvas, keepIframeSrcFn, newlyAddedElement = false, + onAssetDetected, } = options; // Only record root id when document object is not the base document const rootId = getRootId(doc, mirror); @@ -503,6 +512,7 @@ function serializeNode( keepIframeSrcFn, newlyAddedElement, rootId, + onAssetDetected, }); case n.TEXT_NODE: return serializeTextNode(n as Text, { @@ -604,6 +614,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 { @@ -619,10 +636,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]; @@ -729,7 +748,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'); @@ -811,6 +839,10 @@ function serializeElementNode( // In case old browsers don't support customElements } + if (assets.length && onAssetDetected) { + onAssetDetected({ urls: assets }); + } + return { type: NodeType.Element, tagName, @@ -958,6 +990,13 @@ export function serializeNodeWithId( node: serializedElementNodeWithId, ) => unknown; stylesheetLoadTimeout?: number; + /** + * 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 { @@ -983,6 +1022,7 @@ export function serializeNodeWithId( stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, + onAssetDetected, } = options; let { needsMask } = options; let { preserveWhiteSpace = true } = options; @@ -1016,6 +1056,7 @@ export function serializeNodeWithId( recordCanvas, keepIframeSrcFn, newlyAddedElement, + onAssetDetected, }); if (!_serializedNode) { // TODO: dev only @@ -1096,6 +1137,7 @@ export function serializeNodeWithId( onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn, + onAssetDetected, }; if ( @@ -1264,6 +1306,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 { @@ -1286,6 +1335,7 @@ function snapshot( iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, + onAssetDetected, keepIframeSrcFn = () => false, } = options || {}; const maskInputOptions: MaskInputOptions = @@ -1355,6 +1405,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 5ccc9082ed..e650fc5316 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -351,3 +351,22 @@ export function extractFileExtension( const match = url.pathname.match(regex); return match?.[1] ?? null; } + +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 aa4bb428ee..faf23c9c86 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -205,7 +205,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; @@ -257,3 +257,87 @@ describe('form', () => { expect(sel?.childNodes).toEqual([]); // shouldn't be stored in childNodes while in transit }); }); + +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 7be978199d..e4c34f2ba6 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -27,6 +27,7 @@ import { scrollCallback, canvasMutationParam, adoptedStyleSheetParam, + assetParam, } from '@rrweb/types'; import type { CrossOriginIframeMessageEventContent } from '../types'; import { IframeManager } from './iframe-manager'; @@ -39,11 +40,13 @@ import { registerErrorHandler, unregisterErrorHandler, } from './error-handler'; +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; const mirror = createMirror(); @@ -80,6 +83,10 @@ function record( userTriggeredOnInput = false, collectFonts = false, inlineImages = false, + assetCaptureConfig = { + captureObjectURLs: true, + captureOrigins: false, + }, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), @@ -258,6 +265,12 @@ function record( }, }); + const wrappedAssetEmit = (p: assetParam) => + wrappedEmit({ + type: EventType.Asset, + data: p, + }); + const wrappedAdoptedStyleSheetEmit = (a: adoptedStyleSheetParam) => wrappedEmit({ type: EventType.IncrementalSnapshot, @@ -306,6 +319,12 @@ function record( dataURLOptions, }); + assetManager = new AssetManager({ + mutationCb: wrappedAssetEmit, + win: window, + assetCaptureConfig, + }); + const shadowDomManager = new ShadowDomManager({ mutationCb: wrappedMutationEmit, scrollCb: wrappedScrollEmit, @@ -328,6 +347,7 @@ function record( canvasManager, keepIframeSrcFn, processedNodeManager, + assetManager, }, mirror, }); @@ -385,6 +405,11 @@ function record( onStylesheetLoad: (linkEl, childSn) => { stylesheetManager.attachLinkElement(linkEl, childSn); }, + onAssetDetected: (assets) => { + assets.urls.forEach((url) => { + assetManager.capture(url); + }); + }, keepIframeSrcFn, }); @@ -529,6 +554,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 f4267af340..8189bfb36e 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -191,6 +191,7 @@ export default class MutationBuffer { private canvasManager: observerParam['canvasManager']; private processedNodeManager: observerParam['processedNodeManager']; private unattachedDoc: HTMLDocument; + private assetManager: observerParam['assetManager']; public init(options: MutationBufferParam) { ( @@ -216,6 +217,7 @@ export default class MutationBuffer { 'shadowDomManager', 'canvasManager', 'processedNodeManager', + 'assetManager', ] as const ).forEach((key) => { // just a type trick, the runtime result is correct @@ -336,6 +338,11 @@ export default class MutationBuffer { onStylesheetLoad: (link, childSn) => { this.stylesheetManager.attachLinkElement(link, childSn); }, + onAssetDetected: (assets) => { + assets.urls.forEach((url) => { + this.assetManager.capture(url); + }); + }, }); if (sn) { adds.push({ @@ -595,6 +602,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 10ffb62b43..d53ab90e15 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -39,6 +39,7 @@ import type { viewportResizeCallback, } 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; @@ -68,6 +69,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; @@ -115,6 +131,7 @@ export type observerParam = { shadowDomManager: ShadowDomManager; canvasManager: CanvasManager; processedNodeManager: ProcessedNodeManager; + assetManager: AssetManager; ignoreCSSAttributes: Set; plugins: Array<{ observer: ( @@ -150,6 +167,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 1b0d00f2d0..2d243fa95f 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 { @@ -77,10 +79,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 0476ce2d68..43f9be4d1f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -13,6 +13,7 @@ export enum EventType { Meta, Custom, Plugin, + Asset, } export type domContentLoadedEvent = { @@ -66,6 +67,14 @@ export type pluginEvent = { }; }; +export type assetEvent = { + type: EventType.Asset; + data: { + url: string; + payload: SerializedCanvasArg; + }; +}; + export enum IncrementalSource { Mutation, MouseMove, @@ -170,7 +179,8 @@ export type eventWithoutTime = | incrementalSnapshotEvent | metaEvent | customEvent - | pluginEvent; + | pluginEvent + | assetEvent; /** * @deprecated intended for internal use @@ -615,6 +625,13 @@ export type customElementParam = { export type customElementCallback = (c: customElementParam) => void; +export type assetParam = { + url: string; + payload: SerializedCanvasArg; +}; + +export type assetCallback = (d: assetParam) => void; + export type DeprecatedMirror = { map: { [key: number]: INode; diff --git a/yarn.lock b/yarn.lock index 40f8fd25f3..e35b5dfdce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14379,7 +14379,7 @@ tslib@2.4.0, tslib@^2.0.3, tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -14399,6 +14399,11 @@ tslib@^2.3.1: resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +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.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" From ddab1da59baab718ade5baf9ace2e94e11955c86 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 8 Jun 2023 16:57:50 +0200 Subject: [PATCH 002/102] 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 16d89dba2dd39130ed68463aebf6b84135952a95 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 9 Jun 2023 15:27:52 +0200 Subject: [PATCH 003/102] 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 e4c34f2ba6..5b6d6959de 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -83,7 +83,7 @@ function record( userTriggeredOnInput = false, collectFonts = false, inlineImages = false, - assetCaptureConfig = { + assetCapture = { captureObjectURLs: true, captureOrigins: false, }, @@ -322,7 +322,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 d53ab90e15..cf86304b7e 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -69,7 +69,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 2986546b16b97022bb8948d385ee179996900b35 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 9 Jun 2023 15:28:09 +0200 Subject: [PATCH 004/102] Document `assetCapture` config --- docs/recipes/assets.md | 40 ++++++++++++++++++++++++++++ guide.md | 59 +++++++++++++++++++++--------------------- 2 files changed, 70 insertions(+), 29 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 5693b26d29..93636881ff 100644 --- a/guide.md +++ b/guide.md @@ -135,36 +135,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 | +| 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. | +| 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 401b2fdaa9380d91657203de7b9b9ad9d3519ad9 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 13 Jun 2023 15:39:26 +0200 Subject: [PATCH 005/102] Create asset event for assets that failed to load --- .../__snapshots__/integration.test.ts.snap | 6 ++ .../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 +++--- 6 files changed, 121 insertions(+), 16 deletions(-) diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index f537fc7b29..39c8c49ee1 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -364,6 +364,12 @@ exports[`integration tests [html file]: picture-in-frame.html 1`] = ` " `; +exports[`integration tests [html file]: picture-with-inline-onload.html 1`] = ` +" + \\"This + " +`; + exports[`integration tests [html file]: preload.html 1`] = ` " 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 616b6e91cb..0525bd04e8 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -77,7 +77,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'); @@ -836,7 +836,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 @@ -853,7 +856,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 @@ -870,7 +876,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 30a36abfcb..2be7531830 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -701,7 +701,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 43f9be4d1f..186fe084bb 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -69,10 +69,7 @@ export type pluginEvent = { export type assetEvent = { type: EventType.Asset; - data: { - url: string; - payload: SerializedCanvasArg; - }; + data: assetParam; }; export enum IncrementalSource { @@ -625,10 +622,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 0ffb6199bd7c3728e2186928784d6635632e4a88 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 13 Jun 2023 15:40:27 +0200 Subject: [PATCH 006/102] Allow other `yarn dev` commands to depend on @rrweb/types --- packages/types/package.json | 2 +- packages/types/vite.config.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/types/package.json b/packages/types/package.json index 2d61372168..4fa83988f5 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -9,7 +9,7 @@ "@rrweb/types" ], "scripts": { - "dev": "vite", + "dev": "vite build -w", "build": "tsc -noEmit && vite build", "check-types": "tsc -noEmit", "prepublish": "npm run build", diff --git a/packages/types/vite.config.js b/packages/types/vite.config.js index f84b8f9fde..c4cd2de445 100644 --- a/packages/types/vite.config.js +++ b/packages/types/vite.config.js @@ -16,6 +16,9 @@ export default { minify: false, sourcemap: true, + rollupOptions: { + input: path.resolve(__dirname, 'src/index.ts'), + }, }, plugins: [dts()], }; From 2fab181d5e521dc957253eb980e6018347d5a626 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 14 Jun 2023 00:05:52 +0200 Subject: [PATCH 007/102] Move types from rrweb-snapshot to @rrweb/types - Fix declaration emit with vite Workaround for https://github.com/qmhc/vite-plugin-dts/issues/193 --- .changeset/yellow-vans-protect.md | 7 + .vscode/rrweb-monorepo.code-workspace | 3 +- packages/rrdom-nodejs/src/document-nodejs.ts | 2 +- packages/rrdom-nodejs/tsconfig.json | 2 +- packages/rrdom/src/diff.ts | 7 +- packages/rrdom/src/document.ts | 2 +- packages/rrdom/src/index.ts | 12 +- packages/rrdom/test/diff.test.ts | 9 +- packages/rrweb-snapshot/package.json | 14 +- packages/rrweb-snapshot/rollup.config.js | 11 +- packages/rrweb-snapshot/src/rebuild.ts | 9 +- packages/rrweb-snapshot/src/snapshot.ts | 12 +- packages/rrweb-snapshot/src/types.ts | 106 +--------------- packages/rrweb-snapshot/src/utils.ts | 8 +- packages/rrweb-snapshot/tsconfig.json | 6 +- .../src/plugins/canvas-webrtc/record/index.ts | 9 +- packages/rrweb/src/record/iframe-manager.ts | 11 +- .../record/observers/canvas/canvas-manager.ts | 3 +- .../rrweb/src/record/stylesheet-manager.ts | 3 +- .../workers/image-bitmap-data-url-worker.ts | 2 +- packages/rrweb/src/replay/canvas/2d.ts | 2 +- .../src/replay/canvas/deserialize-args.ts | 29 ++++- packages/rrweb/src/replay/canvas/webgl.ts | 12 +- packages/rrweb/src/replay/index.ts | 26 ++-- packages/rrweb/src/types.ts | 2 +- packages/rrweb/src/utils.ts | 3 +- packages/types/package.json | 12 +- packages/types/src/index.ts | 120 ++++++++++++++++-- packages/types/tsconfig.json | 6 +- packages/types/vite.config.js | 7 +- yarn.lock | 28 ++-- 31 files changed, 268 insertions(+), 217 deletions(-) create mode 100644 .changeset/yellow-vans-protect.md diff --git a/.changeset/yellow-vans-protect.md b/.changeset/yellow-vans-protect.md new file mode 100644 index 0000000000..8a1c706956 --- /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/.vscode/rrweb-monorepo.code-workspace b/.vscode/rrweb-monorepo.code-workspace index ee31ea35ee..aa6a759381 100644 --- a/.vscode/rrweb-monorepo.code-workspace +++ b/.vscode/rrweb-monorepo.code-workspace @@ -36,6 +36,7 @@ " rrweb monorepo", "rrweb-player (package)", "@rrweb/types" - ] + ], + "typescript.tsdk": " rrweb monorepo/node_modules/typescript/lib" } } diff --git a/packages/rrdom-nodejs/src/document-nodejs.ts b/packages/rrdom-nodejs/src/document-nodejs.ts index 1d13970d34..15a805d6fd 100644 --- a/packages/rrdom-nodejs/src/document-nodejs.ts +++ b/packages/rrdom-nodejs/src/document-nodejs.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { NodeType as RRNodeType } from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from '@rrweb/types'; import type { NWSAPI } from 'nwsapi'; import type { CSSStyleDeclaration as CSSStyleDeclarationType } from 'cssstyle'; import { diff --git a/packages/rrdom-nodejs/tsconfig.json b/packages/rrdom-nodejs/tsconfig.json index 0c2f119853..e4e98a64bd 100644 --- a/packages/rrdom-nodejs/tsconfig.json +++ b/packages/rrdom-nodejs/tsconfig.json @@ -28,7 +28,7 @@ "path": "../rrdom" }, { - "path": "../rrweb-snapshot" + "path": "../types" } ] } diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 8ccf81f8c6..56f9414c62 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,9 +1,7 @@ +import type { Mirror as NodeMirror } from 'rrweb-snapshot'; import { NodeType as RRNodeType, - Mirror as NodeMirror, elementNode, -} from 'rrweb-snapshot'; -import type { canvasMutationData, canvasEventWithTime, inputData, @@ -540,6 +538,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/document.ts b/packages/rrdom/src/document.ts index befdadb86d..35487e5116 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -1,4 +1,4 @@ -import { NodeType as RRNodeType } from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from '@rrweb/types'; import { parseCSSText, camelize, toCSSText } from './style'; export interface IRRNode { parentElement: IRRNode | null; diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index eeddb03e1c..019a16e582 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -1,13 +1,9 @@ -import { - NodeType as RRNodeType, - createMirror as createNodeMirror, -} from 'rrweb-snapshot'; +import { createMirror as createNodeMirror } from 'rrweb-snapshot'; +import type { Mirror as NodeMirror } from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from '@rrweb/types'; import type { - Mirror as NodeMirror, IMirror, serializedNodeWithId, -} from 'rrweb-snapshot'; -import type { canvasMutationData, canvasEventWithTime, inputData, @@ -451,6 +447,8 @@ export function getDefaultSN(node: IRRNode, id: number): serializedNodeWithId { type: node.RRNodeType, textContent: '', }; + default: + throw new Error(`Unknown node type`); } } diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index fc76f48bd9..551a76fe0f 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -3,12 +3,7 @@ */ import * as path from 'path'; import * as puppeteer from 'puppeteer'; -import { - NodeType as RRNodeType, - serializedNodeWithId, - createMirror, - Mirror as NodeMirror, -} from 'rrweb-snapshot'; +import { createMirror, Mirror as NodeMirror } from 'rrweb-snapshot'; import { buildFromDom, getDefaultSN, @@ -27,6 +22,8 @@ import { import type { IRRElement, IRRNode } from '../src/document'; import { Replayer } from 'rrweb'; import type { + NodeType as RRNodeType, + serializedNodeWithId, eventWithTime, canvasMutationData, styleDeclarationData, diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index e92c170a19..097264fd58 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -11,8 +11,8 @@ "test:update": "jest --updateSnapshot", "bundle": "rollup --config", "bundle:es-only": "cross-env ES_ONLY=true rollup --config", - "dev": "yarn bundle:es-only --watch", - "typings": "tsc -d --declarationDir typings", + "dev": "yarn bundle:es-only --watch & yarn typings -w", + "typings": "tsc -d --declarationDir typings --emitDeclarationOnly", "prepublish": "yarn typings && yarn bundle", "lint": "yarn eslint src" }, @@ -42,6 +42,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.8" + }, "devDependencies": { "@types/chai": "^4.1.4", "@types/jest": "^27.0.2", @@ -53,11 +56,12 @@ "jest-snapshot": "^23.6.0", "jsdom": "^16.4.0", "puppeteer": "^17.1.3", - "rollup": "^2.45.2", + "rollup": "^2.46.0", "rollup-plugin-terser": "^7.0.2", - "rollup-plugin-typescript2": "^0.31.2", + "rollup-plugin-typescript2": "^0.34.1", "ts-jest": "^27.0.5", "ts-node": "^7.0.1", - "tslib": "^2.5.3" + "tslib": "^2.5.3", + "typescript": "^4.7.3" } } diff --git a/packages/rrweb-snapshot/rollup.config.js b/packages/rrweb-snapshot/rollup.config.js index b13f447864..1e07a43575 100644 --- a/packages/rrweb-snapshot/rollup.config.js +++ b/packages/rrweb-snapshot/rollup.config.js @@ -1,4 +1,5 @@ import typescript from 'rollup-plugin-typescript2'; +import resolve from '@rollup/plugin-node-resolve'; import { terser } from 'rollup-plugin-terser'; import pkg from './package.json'; @@ -10,7 +11,7 @@ let configs = [ // ES module - for building rrweb { input: './src/index.ts', - plugins: [typescript()], + plugins: [resolve(), typescript()], output: [ { format: 'esm', @@ -23,7 +24,7 @@ let extra_configs = [ // browser { input: './src/index.ts', - plugins: [typescript()], + plugins: [resolve(), typescript()], output: [ { name: 'rrwebSnapshot', @@ -34,7 +35,7 @@ let extra_configs = [ }, { input: './src/index.ts', - plugins: [typescript(), terser()], + plugins: [resolve(), typescript(), terser()], output: [ { name: 'rrwebSnapshot', @@ -47,7 +48,7 @@ let extra_configs = [ // CommonJS { input: './src/index.ts', - plugins: [typescript()], + plugins: [resolve(), typescript()], output: [ { format: 'cjs', @@ -58,7 +59,7 @@ let extra_configs = [ // ES module (packed) { input: './src/index.ts', - plugins: [typescript(), terser()], + plugins: [resolve(), typescript(), terser()], output: [ { format: 'esm', diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 7c6ed948e6..a52d8a9b4b 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -1,12 +1,11 @@ -import { Rule, Media, NodeWithRules, parse } from './css'; import { serializedNodeWithId, - NodeType, - tagMap, elementNode, - BuildCache, legacyAttributes, -} from './types'; + NodeType, +} from '@rrweb/types'; +import { Rule, Media, NodeWithRules, parse } from './css'; +import { tagMap, BuildCache } from './types'; import { isElement, Mirror, isNodeMetaEqual } from './utils'; const tagMap: tagMap = { diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index fe9e27edf4..5fe8e56f2b 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -1,11 +1,6 @@ import { - serializedNode, - serializedNodeWithId, - NodeType, - attributes, MaskInputOptions, SlimDOMOptions, - DataURLOptions, MaskTextFn, MaskInputFn, KeepIframeSrcFn, @@ -14,6 +9,13 @@ import { serializedElementNodeWithId, type mediaAttributes, } from './types'; +import { + serializedNode, + serializedNodeWithId, + NodeType, + attributes, + DataURLOptions, +} from '@rrweb/types'; import { Mirror, is2DCanvasBlank, diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index 1abfe4d6c0..8e4caea835 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -1,77 +1,4 @@ -export enum NodeType { - Document, - DocumentType, - Element, - Text, - CDATA, - Comment, -} - -export type documentNode = { - type: NodeType.Document; - childNodes: serializedNodeWithId[]; - compatMode?: string; -}; - -export type documentTypeNode = { - type: NodeType.DocumentType; - name: string; - publicId: string; - systemId: string; -}; - -export type attributes = { - [key: string]: string | number | true | null; -}; -export type legacyAttributes = { - /** - * @deprecated old bug in rrweb was causing these to always be set - * @see https://github.com/rrweb-io/rrweb/pull/651 - */ - selected: false; -}; - -export type elementNode = { - type: NodeType.Element; - tagName: string; - attributes: attributes; - childNodes: serializedNodeWithId[]; - isSVG?: true; - needBlock?: boolean; - // This is a custom element or not. - isCustom?: true; -}; - -export type textNode = { - type: NodeType.Text; - textContent: string; - isStyle?: true; -}; - -export type cdataNode = { - type: NodeType.CDATA; - textContent: ''; -}; - -export type commentNode = { - type: NodeType.Comment; - textContent: string; -}; - -export type serializedNode = ( - | documentNode - | documentTypeNode - | elementNode - | textNode - | cdataNode - | commentNode -) & { - rootId?: number; - isShadowHost?: boolean; - isShadow?: boolean; -}; - -export type serializedNodeWithId = serializedNode & { id: number }; +import { serializedNodeWithId, NodeType } from '@rrweb/types'; export type serializedElementNodeWithId = Extract< serializedNodeWithId, @@ -103,7 +30,9 @@ export type mediaAttributes = { rr_mediaVolume?: number; }; -// @deprecated +/** + * @deprecated + */ export interface INode extends Node { __sn: serializedNodeWithId; } @@ -112,28 +41,6 @@ export interface ICanvas extends HTMLCanvasElement { __context: string; } -export interface IMirror { - getId(n: TNode | undefined | null): number; - - getNode(id: number): TNode | null; - - getIds(): number[]; - - getMeta(n: TNode): serializedNodeWithId | null; - - removeNodeFromMap(n: TNode): void; - - has(id: number): boolean; - - hasNode(node: TNode): boolean; - - add(n: TNode, meta: serializedNodeWithId): void; - - replace(id: number, n: TNode): void; - - reset(): void; -} - export type idNodeMap = Map; export type nodeMetaMap = WeakMap; @@ -171,11 +78,6 @@ export type SlimDOMOptions = Partial<{ headMetaVerification: boolean; }>; -export type DataURLOptions = Partial<{ - type: string; - quality: number; -}>; - export type MaskTextFn = (text: string, element: HTMLElement | null) => string; export type MaskInputFn = (text: string, element: HTMLElement) => string; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index e650fc5316..99fab105f6 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -1,8 +1,6 @@ +import { MaskInputFn, MaskInputOptions, idNodeMap, nodeMetaMap } from './types'; + import { - idNodeMap, - MaskInputFn, - MaskInputOptions, - nodeMetaMap, IMirror, serializedNodeWithId, serializedNode, @@ -11,7 +9,7 @@ import { documentTypeNode, textNode, elementNode, -} from './types'; +} from '@rrweb/types'; export function isElement(n: Node): n is Element { return n.nodeType === n.ELEMENT_NODE; diff --git a/packages/rrweb-snapshot/tsconfig.json b/packages/rrweb-snapshot/tsconfig.json index 58577aa6a9..035c8baa15 100644 --- a/packages/rrweb-snapshot/tsconfig.json +++ b/packages/rrweb-snapshot/tsconfig.json @@ -14,5 +14,9 @@ }, "exclude": ["test"], "include": ["src"], - "references": [] + "references": [ + { + "path": "../types" + } + ] } diff --git a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts index 03aad10c9c..7074ac4056 100644 --- a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts +++ b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts @@ -1,6 +1,9 @@ -import type { Mirror } from 'rrweb-snapshot'; import SimplePeer from 'simple-peer-light'; -import type { RecordPlugin, ICrossOriginIframeMirror } from '@rrweb/types'; +import type { + RecordPlugin, + ICrossOriginIframeMirror, + IMirror, +} from '@rrweb/types'; import type { WebRTCDataChannel } from '../types'; export const PLUGIN_NAME = 'rrweb/canvas-webrtc@1'; @@ -25,7 +28,7 @@ export type CrossOriginIframeMessageEventContent = { export class RRWebPluginCanvasWebRTCRecord { private peer: SimplePeer.Instance | null = null; - private mirror: Mirror; + private mirror: IMirror; private crossOriginIframeMirror: ICrossOriginIframeMirror; private streamMap: Map = new Map(); private incomingStreams = new Set(); diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 2d9205c0cb..c82a8e6563 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -1,9 +1,12 @@ -import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; -import { genId, NodeType } from 'rrweb-snapshot'; +import type { Mirror } from 'rrweb-snapshot'; +import { genId } from 'rrweb-snapshot'; import type { CrossOriginIframeMessageEvent } from '../types'; import CrossOriginIframeMirror from './cross-origin-iframe-mirror'; -import { EventType, IncrementalSource } from '@rrweb/types'; -import type { +import { + EventType, + NodeType, + IncrementalSource, + serializedNodeWithId, eventWithTime, eventWithoutTime, mutationCallBack, diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index f825877f69..6e367ed970 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -1,4 +1,4 @@ -import type { ICanvas, Mirror, DataURLOptions } from 'rrweb-snapshot'; +import type { ICanvas, Mirror } from 'rrweb-snapshot'; import type { blockClass, canvasManagerMutationCallback, @@ -8,6 +8,7 @@ import type { IWindow, listenerHandler, CanvasArg, + DataURLOptions, } from '@rrweb/types'; import { isBlocked } from '../../../utils'; import { CanvasContext } from '@rrweb/types'; diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index 6e0a8077b4..4e4e00221c 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -1,6 +1,7 @@ -import type { elementNode, serializedNodeWithId } from 'rrweb-snapshot'; import { stringifyRule } from 'rrweb-snapshot'; import type { + elementNode, + serializedNodeWithId, adoptedStyleSheetCallback, adoptedStyleSheetParam, attributeMutation, diff --git a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts index 374edfe1b0..b5aed49634 100644 --- a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts +++ b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts @@ -1,6 +1,6 @@ import { encode } from 'base64-arraybuffer'; -import type { DataURLOptions } from 'rrweb-snapshot'; import type { + DataURLOptions, ImageBitmapDataURLWorkerParams, ImageBitmapDataURLWorkerResponse, } from '@rrweb/types'; diff --git a/packages/rrweb/src/replay/canvas/2d.ts b/packages/rrweb/src/replay/canvas/2d.ts index 93d852399d..e797ba518f 100644 --- a/packages/rrweb/src/replay/canvas/2d.ts +++ b/packages/rrweb/src/replay/canvas/2d.ts @@ -28,7 +28,7 @@ export default async function canvasMutation({ return Promise.all(mutation.args.map(deserializeArg(imageMap, ctx))); }, ); - const args = await Promise.all(mutationArgsPromises); + const args = (await Promise.all(mutationArgsPromises)) as unknown[]; // step 2 apply all mutations args.forEach((args, index) => { const mutation = mutations[index]; 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 85ff81f8d6..fec72b4b09 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 89aa66d933..30f824f16f 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1,12 +1,10 @@ import { rebuild, buildNodeWithSN, - NodeType, BuildCache, createCache, Mirror, createMirror, - attributes, serializedElementNodeWithId, toLowerCase, } from 'rrweb-snapshot'; @@ -34,6 +32,8 @@ import { Timer } from './timer'; import { createPlayerService, createSpeedService } from './machine'; import type { playerConfig, missingNodeMap } from '../types'; import { + NodeType, + attributes, EventType, IncrementalSource, fullSnapshotEvent, @@ -81,6 +81,7 @@ import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; import { MediaManager } from './media'; +import AssetManager from './assets'; const SKIP_TIME_INTERVAL = 5 * 1000; @@ -855,6 +856,7 @@ export class Replayer { if (this.config.UNSAFE_replayCanvas) { void this.preloadAllImages(); } + void this.preloadAllAssets(); } private insertStyleRules( @@ -1022,16 +1024,24 @@ 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 */ private async preloadAllImages(): Promise { - let beforeLoadState = this.service.state; - const stateHandler = () => { - beforeLoadState = this.service.state; - }; - this.emitter.on(ReplayerEvents.Start, stateHandler); - this.emitter.on(ReplayerEvents.Pause, stateHandler); const promises: Promise[] = []; for (const event of this.service.state.context.events) { if ( diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index cf86304b7e..2b2a4a5611 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -4,7 +4,6 @@ import type { SlimDOMOptions, MaskInputFn, MaskTextFn, - DataURLOptions, } from 'rrweb-snapshot'; import type { PackFn, UnpackFn } from './packer/base'; import type { IframeManager } from './record/iframe-manager'; @@ -14,6 +13,7 @@ import type { RRNode } from 'rrdom'; import type { CanvasManager } from './record/observers/canvas/canvas-manager'; import type { StylesheetManager } from './record/stylesheet-manager'; import type { + DataURLOptions, addedNodeMutation, blockClass, canvasMutationCallback, diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index f426689d2f..7f74c4cc06 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -8,8 +8,9 @@ import type { IWindow, DeprecatedMirror, textMutation, + IMirror, } from '@rrweb/types'; -import type { IMirror, Mirror } from 'rrweb-snapshot'; +import type { Mirror } from 'rrweb-snapshot'; import { isShadowRoot, IGNORED_NODE, classMatchesRegex } from 'rrweb-snapshot'; import type { RRNode, RRIFrameElement } from 'rrdom'; diff --git a/packages/types/package.json b/packages/types/package.json index 4fa83988f5..a5b7ac61a1 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -9,9 +9,10 @@ "@rrweb/types" ], "scripts": { - "dev": "vite build -w", - "build": "tsc -noEmit && vite build", + "dev": "vite build -w & yarn typings -w", + "build": "yarn typings && vite build", "check-types": "tsc -noEmit", + "typings": "tsc -d --declarationDir typings --emitDeclarationOnly", "prepublish": "npm run build", "lint": "yarn eslint src/**/*.ts" }, @@ -27,7 +28,7 @@ "type": "module", "main": "./dist/types.umd.cjs", "module": "./dist/types.js", - "typings": "dist/index.d.ts", + "typings": "typings/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", @@ -36,16 +37,13 @@ } }, "files": [ - "build", + "typings", "dist" ], "devDependencies": { "vite": "^3.2.0-beta.2", "vite-plugin-dts": "^1.7.3" }, - "dependencies": { - "rrweb-snapshot": "^2.0.0-alpha.13" - }, "browserslist": [ "supports es6-class" ] diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 186fe084bb..5fdb43f228 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,10 +1,3 @@ -import type { - serializedNodeWithId, - Mirror, - INode, - DataURLOptions, -} from 'rrweb-snapshot'; - export enum EventType { DomContentLoaded, Load, @@ -261,7 +254,7 @@ export type RecordPlugin = { ) => listenerHandler; eventProcessor?: (event: eventWithTime) => eventWithTime & TExtend; getMirror?: (mirrors: { - nodeMirror: Mirror; + nodeMirror: IMirror; crossOriginIframeMirror: ICrossOriginIframeMirror; crossOriginIframeStyleMirror: ICrossOriginIframeMirror; }) => void; @@ -637,6 +630,13 @@ export type assetParam = export type assetCallback = (d: assetParam) => void; +/** + * @deprecated + */ +interface INode extends Node { + __sn: serializedNodeWithId; +} + export type DeprecatedMirror = { map: { [key: number]: INode; @@ -726,3 +726,107 @@ export type TakeTypedKeyValues = Pick< Obj, TakeTypeHelper[keyof TakeTypeHelper] >; + + +export enum NodeType { + Document, + DocumentType, + Element, + Text, + CDATA, + Comment, +} + +export type documentNode = { + type: NodeType.Document; + childNodes: serializedNodeWithId[]; + compatMode?: string; +}; + +export type documentTypeNode = { + type: NodeType.DocumentType; + name: string; + publicId: string; + systemId: string; +}; + +export type attributes = { + [key: string]: string | number | true | null; +}; + +export type legacyAttributes = { + /** + * @deprecated old bug in rrweb was causing these to always be set + * @see https://github.com/rrweb-io/rrweb/pull/651 + */ + selected: false; +}; + +export type elementNode = { + type: NodeType.Element; + tagName: string; + attributes: attributes; + childNodes: serializedNodeWithId[]; + isSVG?: true; + needBlock?: boolean; + // This is a custom element or not. + isCustom?: true; +}; + +export type textNode = { + type: NodeType.Text; + textContent: string; + isStyle?: true; +}; + +export type cdataNode = { + type: NodeType.CDATA; + textContent: ''; +}; + +export type commentNode = { + type: NodeType.Comment; + textContent: string; +}; + +export type serializedNode = ( + | documentNode + | documentTypeNode + | elementNode + | textNode + | cdataNode + | commentNode +) & { + rootId?: number; + isShadowHost?: boolean; + isShadow?: boolean; +}; + +export type serializedNodeWithId = serializedNode & { id: number }; + +export interface IMirror { + getId(n: TNode | undefined | null): number; + + getNode(id: number): TNode | null; + + getIds(): number[]; + + getMeta(n: TNode): serializedNodeWithId | null; + + removeNodeFromMap(n: TNode): void; + + has(id: number): boolean; + + hasNode(node: TNode): boolean; + + add(n: TNode, meta: serializedNodeWithId): void; + + replace(id: number, n: TNode): void; + + reset(): void; +} + +export type DataURLOptions = Partial<{ + type: string; + quality: number; +}>; diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index ecde939d12..517fd48488 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -18,9 +18,5 @@ "compileOnSave": true, "exclude": ["test"], "include": ["src"], - "references": [ - { - "path": "../rrweb-snapshot" - } - ] + "references": [] } diff --git a/packages/types/vite.config.js b/packages/types/vite.config.js index c4cd2de445..1e097aa01b 100644 --- a/packages/types/vite.config.js +++ b/packages/types/vite.config.js @@ -1,5 +1,5 @@ import path from 'path'; -import dts from 'vite-plugin-dts'; +// import dts from 'vite-plugin-dts'; // disabled because of https://github.com/qmhc/vite-plugin-dts/issues/193 /** * @type {import('vite').UserConfig} */ @@ -16,9 +16,6 @@ export default { minify: false, sourcemap: true, - rollupOptions: { - input: path.resolve(__dirname, 'src/index.ts'), - }, }, - plugins: [dts()], + // plugins: [dts()], }; diff --git a/yarn.lock b/yarn.lock index e35b5dfdce..57cdd4e2b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13106,6 +13106,17 @@ rollup-plugin-typescript2@^0.31.2: resolve "^1.20.0" tslib "^2.3.1" +rollup-plugin-typescript2@^0.34.1: + version "0.34.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.34.1.tgz#c457f155a71d133c142689213fce78694e30d0be" + integrity sha512-P4cHLtGikESmqi1CA+tdMDUv8WbQV48mzPYt77TSTOPJpERyZ9TXdDgjSDix8Fkqce6soYz3+fa4lrC93IEkcw== + dependencies: + "@rollup/pluginutils" "^4.1.2" + find-cache-dir "^3.3.2" + fs-extra "^10.0.0" + semver "^7.3.7" + tslib "^2.4.0" + rollup-plugin-web-worker-loader@^1.6.1: version "1.6.1" resolved "https://registry.npmjs.org/rollup-plugin-web-worker-loader/-/rollup-plugin-web-worker-loader-1.6.1.tgz" @@ -13118,10 +13129,10 @@ rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@^2.45.2: - version "2.53.3" - resolved "https://registry.npmjs.org/rollup/-/rollup-2.53.3.tgz" - integrity sha512-79QIGP5DXz5ZHYnCPi3tLz+elOQi6gudp9YINdaJdjG0Yddubo6JRFUM//qCZ0Bap/GJrsUoEBVdSOc4AkMlRA== +rollup@^2.46.0, rollup@^2.79.1: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== optionalDependencies: fsevents "~2.3.2" @@ -13146,13 +13157,6 @@ rollup@^2.71.1: optionalDependencies: fsevents "~2.3.2" -rollup@^2.79.1: - version "2.79.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" - integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== - optionalDependencies: - fsevents "~2.3.2" - rollup@~2.78.0: version "2.78.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.1.tgz#52fe3934d9c83cb4f7c4cb5fb75d88591be8648f" @@ -14399,7 +14403,7 @@ tslib@^2.3.1: resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@^2.5.3: +tslib@^2.4.0, tslib@^2.5.3: version "2.5.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== From 57d46195a4dd012227458d11ce4946628c0d1fe4 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 4 Dec 2023 15:21:11 +0100 Subject: [PATCH 008/102] Remove unused import statement --- packages/rrweb-snapshot/src/snapshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 5fe8e56f2b..0465384a08 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -5,7 +5,6 @@ import { MaskInputFn, KeepIframeSrcFn, ICanvas, - elementNode, serializedElementNodeWithId, type mediaAttributes, } from './types'; @@ -14,6 +13,7 @@ import { serializedNodeWithId, NodeType, attributes, + elementNode, DataURLOptions, } from '@rrweb/types'; import { From 2f76faef55bbe2fd1a88b8c6ddd0df39ae997a18 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 14 Jun 2023 00:30:52 +0200 Subject: [PATCH 009/102] WIP asset manager for replay --- packages/rrweb-snapshot/src/rebuild.ts | 10 ++ packages/rrweb/src/replay/assets/index.ts | 65 ++++++++++++ ...corporate-assets-emitted-later-1-snap.png} | Bin ...sset.test.ts => asset-integration.test.ts} | 0 packages/rrweb/test/replay/asset-unit.test.ts | 100 ++++++++++++++++++ packages/types/src/index.ts | 23 +++- 6 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 packages/rrweb/src/replay/assets/index.ts rename packages/rrweb/test/replay/__image_snapshots__/{asset-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png => asset-integration-test-ts-replayer-asset-should-incorporate-assets-emitted-later-1-snap.png} (100%) 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 a52d8a9b4b..4c9d9ae00c 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -1,4 +1,5 @@ import { + RebuildAssetManagerInterface, serializedNodeWithId, elementNode, legacyAttributes, @@ -150,6 +151,7 @@ function buildNode( doc: Document; hackCss: boolean; cache: BuildCache; + assetManager?: RebuildAssetManagerInterface; }, ): Node | null { const { doc, hackCss, cache } = options; @@ -281,6 +283,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()); } @@ -404,6 +413,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/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-integration-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-integration-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-integration.test.ts similarity index 100% rename from packages/rrweb/test/replay/asset.test.ts rename to packages/rrweb/test/replay/asset-integration.test.ts diff --git a/packages/rrweb/test/replay/asset-unit.test.ts b/packages/rrweb/test/replay/asset-unit.test.ts new file mode 100644 index 0000000000..788251de2a --- /dev/null +++ b/packages/rrweb/test/replay/asset-unit.test.ts @@ -0,0 +1,100 @@ +/** + * @jest-environment jsdom + */ + +import AssetManager from '../../src/replay/assets'; +import { EventType, SerializedBlobArg, assetEvent } from '@rrweb/types'; + +describe('AssetManager', () => { + 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(() => { + jest.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 = jest + .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 = jest.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/types/src/index.ts b/packages/types/src/index.ts index 5fdb43f228..28a85f4673 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,17 @@ 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, From 56f5aa88a91574fd4f3ee1b19eaee01cc7984d5a Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 14 Jun 2023 00:31:04 +0200 Subject: [PATCH 010/102] NodeType was moved to @rrweb/types --- packages/rrweb/test/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 2be7531830..27b721dda4 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -1,4 +1,4 @@ -import { NodeType } from 'rrweb-snapshot'; +import { NodeType } from '@rrweb/types'; import { EventType, IncrementalSource, From cbeeb01b109ff50edec986327190b2bdbd532d78 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 14:29:15 +0100 Subject: [PATCH 011/102] 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 a18d0e6183aea586843f518ac4135b2fed07b670 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 15:44:50 +0100 Subject: [PATCH 012/102] 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 4c9d9ae00c..beadb213cb 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -283,9 +283,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 ae020d71ad739deb1226062fd38aba6aaa91bda9 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 17:52:35 +0100 Subject: [PATCH 013/102] 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 e4e98a64bd..0c2f119853 100644 --- a/packages/rrdom-nodejs/tsconfig.json +++ b/packages/rrdom-nodejs/tsconfig.json @@ -28,7 +28,7 @@ "path": "../rrdom" }, { - "path": "../types" + "path": "../rrweb-snapshot" } ] } From d3c7b7e7380a70d5d4fa3d9aa22d9a4abfdf9a77 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 17:55:50 +0100 Subject: [PATCH 014/102] 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 beadb213cb..f9d9533f90 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -289,8 +289,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()); } @@ -424,6 +438,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. @@ -438,7 +453,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; } @@ -490,6 +505,7 @@ export function buildNodeWithSN( hackCss, afterAppend, cache, + assetManager, }); if (!childNode) { console.warn('Failed to rebuild', childN); @@ -578,6 +594,7 @@ function rebuild( afterAppend?: (n: Node, id: number) => unknown; cache: BuildCache; mirror: Mirror; + assetManager?: RebuildAssetManagerInterface; }, ): Node | null { const { @@ -587,6 +604,7 @@ function rebuild( afterAppend, cache, mirror = new Mirror(), + assetManager, } = options; const node = buildNodeWithSN(n, { doc, @@ -595,6 +613,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 30f824f16f..5082c187dd 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -136,6 +136,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(); @@ -738,6 +741,11 @@ export class Replayer { } }; break; + case EventType.Asset: + castFn = () => { + void this.assetManager.add(event); + }; + break; default: } const wrappedCastFn = () => { @@ -817,6 +825,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. @@ -833,6 +843,7 @@ export class Replayer { afterAppend, cache: this.cache, mirror: this.mirror, + assetManager: this.assetManager, }); afterAppend(this.iframe.contentDocument, event.data.node.id); @@ -856,7 +867,6 @@ export class Replayer { if (this.config.UNSAFE_replayCanvas) { void this.preloadAllImages(); } - void this.preloadAllAssets(); } private insertStyleRules( @@ -1028,11 +1038,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 788251de2a..62a0ecc32e 100644 --- a/packages/rrweb/test/replay/asset-unit.test.ts +++ b/packages/rrweb/test/replay/asset-unit.test.ts @@ -33,6 +33,7 @@ describe('AssetManager', () => { afterEach(() => { jest.restoreAllMocks(); + jest.useRealTimers(); }); afterAll(() => { @@ -97,4 +98,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 28a85f4673..e887b7ee48 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 841cd8c170532ad839d9caf12e9ce6e8482ba920 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 23 Nov 2023 17:56:15 +0100 Subject: [PATCH 015/102] 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 e9060a2021..8d5b609d0f 100644 --- a/packages/rrweb/test/replay/asset-integration.test.ts +++ b/packages/rrweb/test/replay/asset-integration.test.ts @@ -62,7 +62,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; @@ -86,6 +85,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 607ce6db559d917b1097bbcd74f7ab7532ee2efd Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 24 Nov 2023 12:31:33 +0100 Subject: [PATCH 016/102] Add asset mutation events for replayer test --- 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 +++- 3 files changed, 166 insertions(+), 1 deletion(-) 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/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())); }); @@ -85,7 +87,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 e4ecc0ab41b51896c409c0a46aa989463ae602bb Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 24 Nov 2023 17:38:29 +0100 Subject: [PATCH 017/102] 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 | 25 ++++- 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 | 14 ++- 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 | 90 ++++++++++++++++++ 12 files changed, 227 insertions(+), 48 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 f9d9533f90..5815ea60c4 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -284,29 +284,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 0465384a08..81369e37ee 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -125,7 +125,11 @@ export function absoluteToStylesheet( 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 @@ -162,13 +166,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) { @@ -199,6 +203,21 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { return output.join(', '); } +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() === '') { return attributeValue; diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 8189bfb36e..9d72c74b5a 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 { @@ -602,12 +603,6 @@ export default class MutationBuffer { } else { return; } - } else if ( - target.tagName === 'IMG' && - (attributeName === 'src' || attributeName === 'srcset') && - value - ) { - this.assetManager.capture(value); } if (!item) { item = { @@ -632,12 +627,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 5082c187dd..f8bf1c5809 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -950,6 +950,7 @@ export class Replayer { skipChild: false, afterAppend, cache: this.cache, + assetManager: this.assetManager, }); afterAppend(iframeEl.contentDocument! as Document, mutation.node.id); @@ -1565,6 +1566,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 @@ -1779,6 +1781,7 @@ export class Replayer { skipChild: true, hackCss: true, cache: this.cache, + assetManager: this.assetManager, }); const siblingNode = target.nextSibling; const parentNode = target.parentNode; @@ -1795,6 +1798,7 @@ export class Replayer { // for safe } } + const targetEl = target as Element | RRElement; if (attributeName === 'value' && target.nodeName === 'TEXTAREA') { // this may or may not have an effect on the value property (which is what is displayed) // depending on whether the textarea has been modified by the user yet @@ -1808,10 +1812,12 @@ export class Replayer { textarea.appendChild(tn as TNode); } } else { - (target as Element | RRElement).setAttribute( - attributeName, - value, - ); + targetEl.setAttribute(attributeName, value); + } + if ( + this.assetManager.isAttributeCacheable(targetEl, attributeName) + ) { + void this.assetManager.manageAttribute(targetEl, attributeName); } } catch (error) { this.warn( diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 7f74c4cc06..d289e69638 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -590,3 +590,25 @@ export function inDom(n: Node): boolean { if (!doc) return false; return doc.contains(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())); }); @@ -93,16 +95,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); @@ -111,6 +113,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 62a0ecc32e..2e821f01e5 100644 --- a/packages/rrweb/test/replay/asset-unit.test.ts +++ b/packages/rrweb/test/replay/asset-unit.test.ts @@ -140,4 +140,94 @@ describe('AssetManager', () => { 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, + }, + }; + jest.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, + }, + }; + jest.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'); + }); }); From 12c18e6f5c83deb3cd7d0bd57cd7bbd948a0462d Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 15:56:41 +0100 Subject: [PATCH 018/102] Add isAttributeCacheable and manageAttribute methods to RebuildAssetManagerInterface --- packages/types/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index e887b7ee48..2f6e47cb40 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 99d2b21a348628082717b6b5c5c2dc20dcd6f72d Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 17:07:02 +0100 Subject: [PATCH 019/102] Move assetCapture params to @rrweb/types and add them to meta events --- docs/recipes/assets.md | 12 +-- guide.md | 2 +- 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, 91 insertions(+), 58 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 93636881ff..5881f3b86f 100644 --- a/guide.md +++ b/guide.md @@ -161,7 +161,7 @@ The parameter of `rrweb.record` accepts the following options. | 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. | +| 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) | diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 5b6d6959de..21b0e24f83 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -84,8 +84,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 2b2a4a5611..ba1f8c05aa 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -37,6 +37,7 @@ import type { styleDeclarationCallback, styleSheetRuleCallback, viewportResizeCallback, + assetCaptureParam, } from '@rrweb/types'; import type ProcessedNodeManager from './record/processed-node-manager'; import type AssetManager from './record/observers/asset-manager'; @@ -69,21 +70,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 dafd7648d0..8aa54d6261 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -104,13 +104,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); }); @@ -131,7 +130,7 @@ const setup = function ( describe('asset caching', function (this: ISuite) { jest.setTimeout(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, ` @@ -142,8 +141,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureObjectURLs: true, - captureOrigins: false, + objectURLs: true, + origins: false, }, }, ); @@ -243,7 +242,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, ` @@ -284,8 +283,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureObjectURLs: true, - captureOrigins: false, + objectURLs: true, + origins: false, }, }, ); @@ -318,7 +317,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, ` @@ -329,8 +328,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureObjectURLs: false, - captureOrigins: false, + objectURLs: false, + origins: false, }, }, ); @@ -396,7 +395,7 @@ describe('asset caching', function (this: ISuite) { ); }); }); - describe('captureOrigins: false', () => { + describe('origins: false', () => { const ctx: ISuite = setup.call( this, ` @@ -409,8 +408,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: false, - captureObjectURLs: false, + origins: false, + objectURLs: false, }, }, ); @@ -428,7 +427,7 @@ describe('asset caching', function (this: ISuite) { ); }); }); - describe('captureOrigins: []', () => { + describe('origins: []', () => { const ctx: ISuite = setup.call( this, ` @@ -441,8 +440,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: [], - captureObjectURLs: false, + origins: [], + objectURLs: false, }, }, ); @@ -460,7 +459,7 @@ describe('asset caching', function (this: ISuite) { ); }); }); - describe('captureOrigins: true', () => { + describe('origins: true', () => { const ctx: ISuite = setup.call( this, ` @@ -473,8 +472,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: true, - captureObjectURLs: false, + origins: true, + objectURLs: false, }, }, ); @@ -496,7 +495,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, ` @@ -510,8 +509,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: true, - captureObjectURLs: false, + origins: true, + objectURLs: false, }, }, ); @@ -566,7 +565,7 @@ describe('asset caching', function (this: ISuite) { }); }); - describe('captureOrigins: ["http://localhost:xxxxx/"]', () => { + describe('origins: ["http://localhost:xxxxx/"]', () => { const ctx: ISuite = setup.call( this, ` @@ -580,8 +579,8 @@ describe('asset caching', function (this: ISuite) { `, { assetCapture: { - captureOrigins: ['{SERVER_URL}'], - captureObjectURLs: false, + origins: ['{SERVER_URL}'], + objectURLs: false, }, }, ); @@ -624,5 +623,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 2f6e47cb40..92200402b2 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 645c75a57549467ffb23abd7bb9d9461bc8e63f0 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 17:26:07 +0100 Subject: [PATCH 020/102] Add asset capture config to meta events --- packages/rrweb/src/record/index.ts | 1 + packages/rrweb/test/integration.test.ts | 10 +++++----- packages/rrweb/test/record/asset.test.ts | 5 ++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 21b0e24f83..1410c45396 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -363,6 +363,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 0525bd04e8..24953ac938 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -13,8 +13,8 @@ import { ISuite, } from './utils'; import type { recordOptions } from '../src/types'; -import { eventWithTime, EventType, RecordPlugin } from '@rrweb/types'; -import { visitSnapshot, NodeType } from 'rrweb-snapshot'; +import { eventWithTime, NodeType, EventType, RecordPlugin } from '@rrweb/types'; +import { visitSnapshot } from 'rrweb-snapshot'; describe('record integration tests', function (this: ISuite) { jest.setTimeout(10_000); @@ -838,7 +838,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`); @@ -858,7 +858,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`); @@ -878,7 +878,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 8aa54d6261..956ef2958e 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -637,8 +637,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 046d179a1b22bfc809dc8edbd0f310784623ac64 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 17:28:09 +0100 Subject: [PATCH 021/102] NodeType was moved to @rrweb/types --- packages/rrdom-nodejs/test/document-nodejs.test.ts | 2 +- packages/rrdom/test/document.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrdom-nodejs/test/document-nodejs.test.ts b/packages/rrdom-nodejs/test/document-nodejs.test.ts index ba3c6144d8..ac6088165f 100644 --- a/packages/rrdom-nodejs/test/document-nodejs.test.ts +++ b/packages/rrdom-nodejs/test/document-nodejs.test.ts @@ -3,7 +3,7 @@ */ import * as fs from 'fs'; import * as path from 'path'; -import { NodeType as RRNodeType } from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from '@rrweb/types'; import { RRCanvasElement, RRCDATASection, diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index ad3b02134e..91ccb1ece1 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -1,7 +1,7 @@ /** * @jest-environment jsdom */ -import { NodeType as RRNodeType } from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from '@rrweb/types'; import { BaseRRDocumentImpl, BaseRRDocumentTypeImpl, From d3694f780dd036d7b84efc7da71c4bb4e2fc924e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 23:36:42 +0100 Subject: [PATCH 022/102] NodeType was moved to @rrweb/types --- packages/rrdom-nodejs/package.json | 3 ++- packages/rrdom/test/diff.test.ts | 7 +++++-- packages/rrdom/test/virtual-dom.test.ts | 4 ++-- packages/rrweb-snapshot/package.json | 2 +- packages/rrweb-snapshot/test/integration.test.ts | 12 +++++++----- packages/rrweb-snapshot/test/rebuild.test.ts | 2 +- packages/rrweb-snapshot/test/snapshot.test.ts | 2 +- packages/rrweb-snapshot/test/utils.test.ts | 3 +-- 8 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/rrdom-nodejs/package.json b/packages/rrdom-nodejs/package.json index 09a69a7ffd..51a82993df 100644 --- a/packages/rrdom-nodejs/package.json +++ b/packages/rrdom-nodejs/package.json @@ -49,7 +49,8 @@ "cssstyle": "^2.3.0", "nwsapi": "^2.2.0", "rrdom": "^2.0.0-alpha.13", - "rrweb-snapshot": "^2.0.0-alpha.13" + "rrweb-snapshot": "^2.0.0-alpha.13", + "@rrweb/types": "^2.0.0-alpha.13" }, "browserslist": [ "supports es6-class" diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 551a76fe0f..77e5717904 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -22,14 +22,17 @@ import { import type { IRRElement, IRRNode } from '../src/document'; import { Replayer } from 'rrweb'; import type { - NodeType as RRNodeType, serializedNodeWithId, eventWithTime, canvasMutationData, styleDeclarationData, styleSheetRuleData, } from '@rrweb/types'; -import { EventType, IncrementalSource } from '@rrweb/types'; +import { + NodeType as RRNodeType, + EventType, + IncrementalSource, +} from '@rrweb/types'; import { compileTSCode } from './utils'; const elementSn = { diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index b99aca5ae4..701ec1f207 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -5,17 +5,17 @@ import * as fs from 'fs'; import * as path from 'path'; import * as puppeteer from 'puppeteer'; import { JSDOM } from 'jsdom'; +import { Mirror } from 'rrweb-snapshot'; import { cdataNode, commentNode, documentNode, documentTypeNode, elementNode, - Mirror, NodeType, NodeType as RRNodeType, textNode, -} from 'rrweb-snapshot'; +} from '@rrweb/types'; import { buildFromDom, buildFromNode, diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 097264fd58..cc66419722 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -43,7 +43,7 @@ }, "homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme", "dependencies": { - "@rrweb/types": "^2.0.0-alpha.8" + "@rrweb/types": "^2.0.0-alpha.11" }, "devDependencies": { "@types/chai": "^4.1.4", diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index dcc6a3ec0b..10a3a38eed 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -4,10 +4,12 @@ import * as http from 'http'; import * as url from 'url'; import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; -import * as typescript from 'rollup-plugin-typescript2'; -import * as assert from 'assert'; +import resolve from '@rollup/plugin-node-resolve'; +import typescript from 'rollup-plugin-typescript2'; +import assert from 'assert'; import { waitForRAF } from './utils'; +const _resolve = resolve as unknown as () => rollup.Plugin; const _typescript = typescript as unknown as () => rollup.Plugin; const htmlFolder = path.join(__dirname, 'html'); @@ -74,7 +76,7 @@ describe('integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [_typescript()], + plugins: [_resolve(), _typescript()], }); const { output: [{ code: _code }], @@ -341,7 +343,7 @@ describe('iframe integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [_typescript()], + plugins: [_resolve(), _typescript()], }); const { output: [{ code: _code }], @@ -389,7 +391,7 @@ describe('shadow DOM integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [_typescript()], + plugins: [_resolve(), _typescript()], }); const { output: [{ code: _code }], diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 097ff0989a..8287285596 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -8,7 +8,7 @@ import { buildNodeWithSN, createCache, } from '../src/rebuild'; -import { NodeType } from '../src/types'; +import { NodeType } from '@rrweb/types'; import { createMirror, Mirror } from '../src/utils'; function getDuration(hrtime: [number, number]) { diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index faf23c9c86..325bfff9a5 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -7,7 +7,7 @@ import { serializeNodeWithId, _isBlockedElement, } from '../src/snapshot'; -import { serializedNodeWithId, elementNode } from '../src/types'; +import { serializedNodeWithId, elementNode } from '@rrweb/types'; import { Mirror } from '../src/utils'; describe('absolute url to stylesheet', () => { diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index afbdda2f42..051853fea0 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -1,9 +1,8 @@ /** * @jest-environment jsdom */ -import { NodeType, serializedNode } from '../src/types'; import { extractFileExtension, isNodeMetaEqual } from '../src/utils'; -import { serializedNodeWithId } from 'rrweb-snapshot'; +import { NodeType, serializedNode, serializedNodeWithId } from '@rrweb/types'; describe('utils', () => { describe('isNodeMetaEqual()', () => { From 66e13e0bc9da2918f52595d94a4ca002d406acc8 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 23:40:14 +0100 Subject: [PATCH 023/102] Upgrade jest and rollup in rrweb-snapshot --- packages/rrweb-snapshot/jest.config.js | 12 +- packages/rrweb-snapshot/jest.setup.ts | 3 + packages/rrweb-snapshot/package.json | 16 +- packages/rrweb-snapshot/tsconfig.json | 1 + yarn.lock | 1358 +++++++++++++----------- 5 files changed, 755 insertions(+), 635 deletions(-) create mode 100644 packages/rrweb-snapshot/jest.setup.ts diff --git a/packages/rrweb-snapshot/jest.config.js b/packages/rrweb-snapshot/jest.config.js index e6cdf180ee..da04b18de6 100644 --- a/packages/rrweb-snapshot/jest.config.js +++ b/packages/rrweb-snapshot/jest.config.js @@ -1,6 +1,16 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ export default { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/**.test.ts'], + setupFiles: ['./jest.setup.ts'], + /** + * Keeps old (pre-jest 29) snapshot format + * its a bit ugly and harder to read than the new format, + * so we might want to remove this in its own PR + */ + snapshotFormat: { + escapeString: true, + printBasicPrototype: true, + }, }; 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 cc66419722..5229e9e364 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -12,6 +12,7 @@ "bundle": "rollup --config", "bundle:es-only": "cross-env ES_ONLY=true rollup --config", "dev": "yarn bundle:es-only --watch & yarn typings -w", + "check-types": "tsc --noEmit", "typings": "tsc -d --declarationDir typings --emitDeclarationOnly", "prepublish": "yarn typings && yarn bundle", "lint": "yarn eslint src" @@ -47,19 +48,20 @@ }, "devDependencies": { "@types/chai": "^4.1.4", - "@types/jest": "^27.0.2", - "@types/jsdom": "^20.0.0", + "@types/jest": "^29.5.10", + "@types/jsdom": "^21.1.6", "@types/node": "^18.15.11", "@types/puppeteer": "^1.12.4", "cross-env": "^5.2.0", - "jest": "^27.2.4", + "jest": "^29.7.0", "jest-snapshot": "^23.6.0", - "jsdom": "^16.4.0", + "jest-environment-jsdom": "^29.7.0", + "jsdom": "^23.0.0", "puppeteer": "^17.1.3", - "rollup": "^2.46.0", + "rollup": "^2.47.0", "rollup-plugin-terser": "^7.0.2", - "rollup-plugin-typescript2": "^0.34.1", - "ts-jest": "^27.0.5", + "rollup-plugin-typescript2": "^0.36.0", + "ts-jest": "^29.1.1", "ts-node": "^7.0.1", "tslib": "^2.5.3", "typescript": "^4.7.3" diff --git a/packages/rrweb-snapshot/tsconfig.json b/packages/rrweb-snapshot/tsconfig.json index 035c8baa15..000278dddc 100644 --- a/packages/rrweb-snapshot/tsconfig.json +++ b/packages/rrweb-snapshot/tsconfig.json @@ -4,6 +4,7 @@ "module": "ESNext", "target": "ES6", "moduleResolution": "Node", + "esModuleInterop": true, "noImplicitAny": true, "strictNullChecks": true, "removeComments": true, diff --git a/yarn.lock b/yarn.lock index 57cdd4e2b5..f830c341c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -71,7 +71,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== -"@babel/core@^7.1.0", "@babel/core@^7.7.2", "@babel/core@^7.7.5", "@babel/core@^7.8.0": +"@babel/core@^7.1.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0": version "7.15.5" resolved "https://registry.npmjs.org/@babel/core/-/core-7.15.5.tgz" integrity sha512-pYgXxiwAgQpgM1bNkZsDEq85f0ggXMA5L7c+o3tskGMh2BunCI9QUwB9Z4jpvXUOuMdyGKiGKQiRe11VS6Jzvg== @@ -636,7 +636,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.15.4", "@babel/parser@^7.15.5", "@babel/parser@^7.7.2": +"@babel/parser@^7.1.0", "@babel/parser@^7.15.4", "@babel/parser@^7.15.5": version "7.15.7" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz" integrity sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g== @@ -850,7 +850,7 @@ "@babel/parser" "^7.22.5" "@babel/types" "^7.22.5" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.15.4", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.15.4", "@babel/traverse@^7.7.2": version "7.15.4" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz" integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA== @@ -2174,18 +2174,6 @@ resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/console/-/console-27.2.4.tgz" - integrity sha512-94znCKynPZpDpYHQ6esRJSc11AmONrVkBOBZiD7S+bSubHhrUfbS95EY5HIOxhm4PQO7cnvZkL3oJcY0oMA+Wg== - dependencies: - "@jest/types" "^27.2.4" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^27.2.4" - jest-util "^27.2.4" - slash "^3.0.0" - "@jest/console@^27.5.1": version "27.5.1" resolved "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz" @@ -2210,39 +2198,17 @@ jest-util "^29.6.2" slash "^3.0.0" -"@jest/core@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/core/-/core-27.2.4.tgz" - integrity sha512-UNQLyy+rXoojNm2MGlapgzWhZD1CT1zcHZQYeiD0xE7MtJfC19Q6J5D/Lm2l7i4V97T30usKDoEtjI8vKwWcLg== +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== dependencies: - "@jest/console" "^27.2.4" - "@jest/reporters" "^27.2.4" - "@jest/test-result" "^27.2.4" - "@jest/transform" "^27.2.4" - "@jest/types" "^27.2.4" + "@jest/types" "^29.6.3" "@types/node" "*" - ansi-escapes "^4.2.1" chalk "^4.0.0" - emittery "^0.8.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-changed-files "^27.2.4" - jest-config "^27.2.4" - jest-haste-map "^27.2.4" - jest-message-util "^27.2.4" - jest-regex-util "^27.0.6" - jest-resolve "^27.2.4" - jest-resolve-dependencies "^27.2.4" - jest-runner "^27.2.4" - jest-runtime "^27.2.4" - jest-snapshot "^27.2.4" - jest-util "^27.2.4" - jest-validate "^27.2.4" - jest-watcher "^27.2.4" - micromatch "^4.0.4" - rimraf "^3.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" slash "^3.0.0" - strip-ansi "^6.0.0" "@jest/core@^27.5.1": version "27.5.1" @@ -2312,15 +2278,39 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/environment/-/environment-27.2.4.tgz" - integrity sha512-wkuui5yr3SSQW0XD0Qm3TATUbL/WE3LDEM3ulC+RCQhMf2yxhci8x7svGkZ4ivJ6Pc94oOzpZ6cdHBAMSYd1ew== +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== dependencies: - "@jest/fake-timers" "^27.2.4" - "@jest/types" "^27.2.4" + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-mock "^27.2.4" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" "@jest/environment@^27.5.1": version "27.5.1" @@ -2342,6 +2332,16 @@ "@types/node" "*" jest-mock "^29.6.2" +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + "@jest/expect-utils@^29.6.2": version "29.6.2" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.2.tgz#1b97f290d0185d264dd9fdec7567a14a38a90534" @@ -2349,6 +2349,13 @@ dependencies: jest-get-type "^29.4.3" +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + "@jest/expect@^29.6.2": version "29.6.2" resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.6.2.tgz#5a2ad58bb345165d9ce0a1845bbf873c480a4b28" @@ -2357,17 +2364,13 @@ expect "^29.6.2" jest-snapshot "^29.6.2" -"@jest/fake-timers@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.2.4.tgz" - integrity sha512-cs/TzvwWUM7kAA6Qm/890SK6JJ2pD5RfDNM3SSEom6BmdyV6OiWP1qf/pqo6ts6xwpcM36oN0wSEzcZWc6/B6w== +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== dependencies: - "@jest/types" "^27.2.4" - "@sinonjs/fake-timers" "^8.0.1" - "@types/node" "*" - jest-message-util "^27.2.4" - jest-mock "^27.2.4" - jest-util "^27.2.4" + expect "^29.7.0" + jest-snapshot "^29.7.0" "@jest/fake-timers@^27.5.1": version "27.5.1" @@ -2393,14 +2396,17 @@ jest-mock "^29.6.2" jest-util "^29.6.2" -"@jest/globals@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/globals/-/globals-27.2.4.tgz" - integrity sha512-DRsRs5dh0i+fA9mGHylTU19+8fhzNJoEzrgsu+zgJoZth3x8/0juCQ8nVVdW1er4Cqifb/ET7/hACYVPD0dBEA== +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== dependencies: - "@jest/environment" "^27.2.4" - "@jest/types" "^27.2.4" - expect "^27.2.4" + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" "@jest/globals@^27.5.1": version "27.5.1" @@ -2421,35 +2427,15 @@ "@jest/types" "^29.6.1" jest-mock "^29.6.2" -"@jest/reporters@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-27.2.4.tgz" - integrity sha512-LHeSdDnDZkDnJ8kvnjcqV8P1Yv/32yL4d4XfR5gBiy3xGO0onwll1QEbvtW96fIwhx2nejug0GTaEdNDoyr3fQ== +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.2.4" - "@jest/test-result" "^27.2.4" - "@jest/transform" "^27.2.4" - "@jest/types" "^27.2.4" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.2.4" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^27.2.4" - jest-resolve "^27.2.4" - jest-util "^27.2.4" - jest-worker "^27.2.4" - slash "^3.0.0" - source-map "^0.6.0" - string-length "^4.0.1" - terminal-link "^2.0.0" - v8-to-istanbul "^8.1.0" + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" "@jest/reporters@^27.5.1": version "27.5.1" @@ -2512,6 +2498,36 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + "@jest/schemas@^29.6.0": version "29.6.0" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040" @@ -2519,14 +2535,12 @@ dependencies: "@sinclair/typebox" "^0.27.8" -"@jest/source-map@^27.0.6": - version "27.0.6" - resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-27.0.6.tgz" - integrity sha512-Fek4mi5KQrqmlY07T23JRi0e7Z9bXTOOD86V/uS0EIW4PClvPDqZOyFlLpNJheS6QI0FNX1CgmPjtJ4EA/2M+g== +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== dependencies: - callsites "^3.0.0" - graceful-fs "^4.2.4" - source-map "^0.6.0" + "@sinclair/typebox" "^0.27.8" "@jest/source-map@^27.5.1": version "27.5.1" @@ -2546,15 +2560,14 @@ callsites "^3.0.0" graceful-fs "^4.2.9" -"@jest/test-result@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-27.2.4.tgz" - integrity sha512-eU+PRo0+lIS01b0dTmMdVZ0TtcRSxEaYquZTRFMQz6CvsehGhx9bRzi9Zdw6VROviJyv7rstU+qAMX5pNBmnfQ== +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== dependencies: - "@jest/console" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" "@jest/test-result@^27.5.1": version "27.5.1" @@ -2576,15 +2589,15 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.2.4.tgz" - integrity sha512-fpk5eknU3/DXE2QCCG1wv/a468+cfPo3Asu6d6yUtM9LOPh709ubZqrhuUOYfM8hXMrIpIdrv1CdCrWWabX0rQ== +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== dependencies: - "@jest/test-result" "^27.2.4" - graceful-fs "^4.2.4" - jest-haste-map "^27.2.4" - jest-runtime "^27.2.4" + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" "@jest/test-sequencer@^27.5.1": version "27.5.1" @@ -2606,26 +2619,15 @@ jest-haste-map "^29.6.2" slash "^3.0.0" -"@jest/transform@^27.2.4": - version "27.2.4" - resolved "https://registry.npmjs.org/@jest/transform/-/transform-27.2.4.tgz" - integrity sha512-n5FlX2TH0oQGwyVDKPxdJ5nI2sO7TJBFe3u3KaAtt7TOiV4yL+Y+rSFDl+Ic5MpbiA/eqXmLAQxjnBmWgS2rEA== +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== dependencies: - "@babel/core" "^7.1.0" - "@jest/types" "^27.2.4" - babel-plugin-istanbul "^6.0.0" - chalk "^4.0.0" - convert-source-map "^1.4.0" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^27.2.4" - jest-regex-util "^27.0.6" - jest-util "^27.2.4" - micromatch "^4.0.4" - pirates "^4.0.1" + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" slash "^3.0.0" - source-map "^0.6.1" - write-file-atomic "^3.0.0" "@jest/transform@^27.5.1": version "27.5.1" @@ -2669,6 +2671,27 @@ slash "^3.0.0" write-file-atomic "^4.0.2" +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + "@jest/types@^27.2.4": version "27.2.4" resolved "https://registry.npmjs.org/@jest/types/-/types-27.2.4.tgz" @@ -2703,6 +2726,18 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" @@ -3372,7 +3407,7 @@ "@types/pixelmatch" "*" ssim.js "^3.1.1" -"@types/jest@*", "@types/jest@^27.0.2": +"@types/jest@*": version "27.0.2" resolved "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz" integrity sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA== @@ -3396,6 +3431,14 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jest@^29.5.10": + version "29.5.10" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.10.tgz#a10fc5bab9e426081c12b2ef73d24d4f0c9b7f50" + integrity sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/jsdom@^20.0.0": version "20.0.0" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.0.tgz#4414fb629465167f8b7b3804b9e067bdd99f1791" @@ -3405,6 +3448,15 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" +"@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" "*" + parse5 "^7.0.0" + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" @@ -4280,20 +4332,6 @@ b4a@^1.6.4: resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9" integrity sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw== -babel-jest@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-27.2.4.tgz" - integrity sha512-f24OmxyWymk5jfgLdlCMu4fTs4ldxFBIdn5sJdhvGC1m08rSkJ5hYbWkNmfBSvE/DjhCVNSHXepxsI6THGfGsg== - dependencies: - "@jest/transform" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^27.2.0" - chalk "^4.0.0" - graceful-fs "^4.2.4" - slash "^3.0.0" - babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz" @@ -4321,16 +4359,18 @@ babel-jest@^29.6.2: graceful-fs "^4.2.9" slash "^3.0.0" -babel-plugin-istanbul@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz" - integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ== +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^4.0.0" - test-exclude "^6.0.0" + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" babel-plugin-istanbul@^6.1.1: version "6.1.1" @@ -4343,16 +4383,6 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^27.2.0: - version "27.2.0" - resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.2.0.tgz" - integrity sha512-TOux9khNKdi64mW+0OIhcmbAn75tTlzKhxmiNXevQaPbrBYK7YKjP1jl6NHTJ6XR5UgUrJbCnWlKVnJn29dfjw== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.0.0" - "@types/babel__traverse" "^7.0.6" - babel-plugin-jest-hoist@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz" @@ -4373,6 +4403,16 @@ babel-plugin-jest-hoist@^29.5.0: "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -4400,14 +4440,6 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^27.2.0: - version "27.2.0" - resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.2.0.tgz" - integrity sha512-z7MgQ3peBwN5L5aCqBKnF6iqdlvZvFUQynEhu0J+X9nHLU72jO3iY331lcYrg+AssJ8q7xsv5/3AICzVmJ/wvg== - dependencies: - babel-plugin-jest-hoist "^27.2.0" - babel-preset-current-node-syntax "^1.0.0" - babel-preset-jest@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz" @@ -4424,6 +4456,14 @@ babel-preset-jest@^29.5.0: babel-plugin-jest-hoist "^29.5.0" babel-preset-current-node-syntax "^1.0.0" +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz" @@ -5320,6 +5360,19 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" @@ -5580,6 +5633,13 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +cssstyle@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-3.0.0.tgz#17ca9c87d26eac764bb8cfd00583cff21ce0277a" + integrity sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg== + dependencies: + rrweb-cssom "^0.6.0" + csstype@^3.0.11, csstype@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" @@ -5640,6 +5700,14 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + dataloader@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" @@ -5699,7 +5767,7 @@ decimal.js@^10.2.1: resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== -decimal.js@^10.4.2: +decimal.js@^10.4.2, decimal.js@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== @@ -5851,6 +5919,11 @@ diff-sequences@^29.4.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + diff@^3.1.0, diff@^3.2.0: version "3.5.0" resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" @@ -6910,18 +6983,6 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -expect@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/expect/-/expect-27.2.4.tgz" - integrity sha512-gOtuonQ8TCnbNNCSw2fhVzRf8EFYDII4nB5NmG4IEV0rbUnW1I5zXvoTntU4iicB/Uh0oZr20NGlOLdJiwsOZA== - dependencies: - "@jest/types" "^27.2.4" - ansi-styles "^5.0.0" - jest-get-type "^27.0.6" - jest-matcher-utils "^27.2.4" - jest-message-util "^27.2.4" - jest-regex-util "^27.0.6" - expect@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz" @@ -6944,6 +7005,17 @@ expect@^29.0.0, expect@^29.6.2: jest-message-util "^29.6.2" jest-util "^29.6.2" +expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + express@^4.16.4: version "4.17.1" resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz" @@ -7972,6 +8044,13 @@ html-encoding-sniffer@^3.0.0: dependencies: whatwg-encoding "^2.0.0" +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" @@ -8073,6 +8152,14 @@ https-proxy-agent@^7.0.0: agent-base "^7.0.2" debug "4" +https-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz#e2645b846b90e96c6e6f347fb5b2e41f1590b09b" + integrity sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA== + dependencies: + agent-base "^7.0.2" + debug "4" + human-id@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/human-id/-/human-id-1.0.2.tgz#e654d4b2b0d8b07e45da9f6020d8af17ec0a5df3" @@ -8780,16 +8867,6 @@ istanbul-lib-coverage@^3.2.0: resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== - dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" - istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz" @@ -8801,6 +8878,17 @@ istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: istanbul-lib-coverage "^3.2.0" semver "^6.3.0" +istanbul-lib-instrument@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf" + integrity sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + istanbul-lib-report@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" @@ -8819,14 +8907,6 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - istanbul-reports@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.3.tgz" @@ -8840,15 +8920,6 @@ jed@1.1.1: resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4" integrity sha512-z35ZSEcXHxLW4yumw0dF6L464NT36vmx3wxJw8MDpraBcWuNVgUPZgPJKcu1HekNgwlMFNqol7i/IpSbjhqwqA== -jest-changed-files@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.2.4.tgz" - integrity sha512-eeO1C1u4ex7pdTroYXezr+rbr957myyVoKGjcY4R1TJi3A+9v+4fu1Iv9J4eLq1bgFyT3O3iRWU9lZsEE7J72Q== - dependencies: - "@jest/types" "^27.2.4" - execa "^5.0.0" - throat "^6.0.1" - jest-changed-files@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz" @@ -8866,30 +8937,14 @@ jest-changed-files@^29.5.0: execa "^5.0.0" p-limit "^3.1.0" -jest-circus@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-27.2.4.tgz" - integrity sha512-TtheheTElrGjlsY9VxkzUU1qwIx05ItIusMVKnvNkMt4o/PeegLRcjq3Db2Jz0GGdBalJdbzLZBgeulZAJxJWA== +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== dependencies: - "@jest/environment" "^27.2.4" - "@jest/test-result" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^0.7.0" - expect "^27.2.4" - is-generator-fn "^2.0.0" - jest-each "^27.2.4" - jest-matcher-utils "^27.2.4" - jest-message-util "^27.2.4" - jest-runtime "^27.2.4" - jest-snapshot "^27.2.4" - jest-util "^27.2.4" - pretty-format "^27.2.4" - slash "^3.0.0" - stack-utils "^2.0.3" - throat "^6.0.1" + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" jest-circus@^27.5.1: version "27.5.1" @@ -8942,23 +8997,31 @@ jest-circus@^29.6.2: slash "^3.0.0" stack-utils "^2.0.3" -jest-cli@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-27.2.4.tgz" - integrity sha512-4kpQQkg74HYLaXo3nzwtg4PYxSLgL7puz1LXHj5Tu85KmlIpxQFjRkXlx4V47CYFFIDoyl3rHA/cXOxUWyMpNg== +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== dependencies: - "@jest/core" "^27.2.4" - "@jest/test-result" "^27.2.4" - "@jest/types" "^27.2.4" + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.4" - import-local "^3.0.2" - jest-config "^27.2.4" - jest-util "^27.2.4" - jest-validate "^27.2.4" - prompts "^2.0.1" - yargs "^16.2.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" jest-cli@^27.5.1: version "27.5.1" @@ -8996,32 +9059,22 @@ jest-cli@^29.6.2: prompts "^2.0.1" yargs "^17.3.1" -jest-config@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-config/-/jest-config-27.2.4.tgz" - integrity sha512-tWy0UxhdzqiKyp4l5Vq4HxLyD+gH5td+GCF3c22/DJ0bYAOsMo+qi2XtbJI6oYMH5JOJQs9nLW/r34nvFCehjA== +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^27.2.4" - "@jest/types" "^27.2.4" - babel-jest "^27.2.4" + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" chalk "^4.0.0" - deepmerge "^4.2.2" - glob "^7.1.1" - graceful-fs "^4.2.4" - is-ci "^3.0.0" - jest-circus "^27.2.4" - jest-environment-jsdom "^27.2.4" - jest-environment-node "^27.2.4" - jest-get-type "^27.0.6" - jest-jasmine2 "^27.2.4" - jest-regex-util "^27.0.6" - jest-resolve "^27.2.4" - jest-runner "^27.2.4" - jest-util "^27.2.4" - jest-validate "^27.2.4" - micromatch "^4.0.4" - pretty-format "^27.2.4" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" jest-config@^27.5.1: version "27.5.1" @@ -9081,6 +9134,34 @@ jest-config@^29.6.2: slash "^3.0.0" strip-json-comments "^3.1.1" +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + jest-diff@^23.6.0: version "23.6.0" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz" @@ -9091,7 +9172,7 @@ jest-diff@^23.6.0: jest-get-type "^22.1.0" pretty-format "^23.6.0" -jest-diff@^27.0.0, jest-diff@^27.2.4: +jest-diff@^27.0.0: version "27.2.4" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.2.4.tgz" integrity sha512-bLAVlDSCR3gqUPGv+4nzVpEXGsHh98HjUL7Vb2hVyyuBDoQmja8eJb0imUABsuxBeUVmf47taJSAd9nDrwWKEg== @@ -9121,12 +9202,15 @@ jest-diff@^29.6.2: jest-get-type "^29.4.3" pretty-format "^29.6.2" -jest-docblock@^27.0.6: - version "27.0.6" - resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.0.6.tgz" - integrity sha512-Fid6dPcjwepTFraz0YxIMCi7dejjJ/KL9FBjPYhBp4Sv1Y9PdhImlKZqYU555BlN4TQKaTc+F2Av1z+anVyGkA== +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== dependencies: - detect-newline "^3.0.0" + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" jest-docblock@^27.5.1: version "27.5.1" @@ -9142,16 +9226,12 @@ jest-docblock@^29.4.3: dependencies: detect-newline "^3.0.0" -jest-each@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-each/-/jest-each-27.2.4.tgz" - integrity sha512-w9XVc+0EDBUTJS4xBNJ7N2JCcWItFd006lFjz77OarAQcQ10eFDBMrfDv2GBJMKlXe9aq0HrIIF51AXcZrRJyg== +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== dependencies: - "@jest/types" "^27.2.4" - chalk "^4.0.0" - jest-get-type "^27.0.6" - jest-util "^27.2.4" - pretty-format "^27.2.4" + detect-newline "^3.0.0" jest-each@^27.5.1: version "27.5.1" @@ -9175,18 +9255,16 @@ jest-each@^29.6.2: jest-util "^29.6.2" pretty-format "^29.6.2" -jest-environment-jsdom@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.2.4.tgz" - integrity sha512-X70pTXFSypD7AIzKT1mLnDi5hP9w9mdTRcOGOmoDoBrNyNEg4rYm6d4LQWFLc9ps1VnMuDOkFSG0wjSNYGjkng== +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== dependencies: - "@jest/environment" "^27.2.4" - "@jest/fake-timers" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/node" "*" - jest-mock "^27.2.4" - jest-util "^27.2.4" - jsdom "^16.6.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" jest-environment-jsdom@^27.5.1: version "27.5.1" @@ -9215,17 +9293,19 @@ jest-environment-jsdom@^29.6.0: jest-util "^29.6.2" jsdom "^20.0.0" -jest-environment-node@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.2.4.tgz" - integrity sha512-ZbVbFSnbzTvhLOIkqh5lcLuGCCFvtG4xTXIRPK99rV2KzQT3kNg16KZwfTnLNlIiWCE8do960eToeDfcqmpSAw== +jest-environment-jsdom@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz#d206fa3551933c3fd519e5dfdb58a0f5139a837f" + integrity sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA== dependencies: - "@jest/environment" "^27.2.4" - "@jest/fake-timers" "^27.2.4" - "@jest/types" "^27.2.4" + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/jsdom" "^20.0.0" "@types/node" "*" - jest-mock "^27.2.4" - jest-util "^27.2.4" + jest-mock "^29.7.0" + jest-util "^29.7.0" + jsdom "^20.0.0" jest-environment-node@^27.5.1: version "27.5.1" @@ -9251,6 +9331,18 @@ jest-environment-node@^29.6.2: jest-mock "^29.6.2" jest-util "^29.6.2" +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + jest-get-type@^22.1.0: version "22.4.3" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz" @@ -9271,25 +9363,10 @@ jest-get-type@^29.4.3: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== -jest-haste-map@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.2.4.tgz" - integrity sha512-bkJ4bT00T2K+1NZXbRcyKnbJ42I6QBvoDNMTAQQDBhaGNnZreiQKUNqax0e6hLTx7E75pKDeltVu3V1HAdu+YA== - dependencies: - "@jest/types" "^27.2.4" - "@types/graceful-fs" "^4.1.2" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^27.0.6" - jest-serializer "^27.0.6" - jest-util "^27.2.4" - jest-worker "^27.2.4" - micromatch "^4.0.4" - walker "^1.0.7" - optionalDependencies: - fsevents "^2.3.2" +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== jest-haste-map@^27.5.1: version "27.5.1" @@ -9330,6 +9407,25 @@ jest-haste-map@^29.6.2: optionalDependencies: fsevents "^2.3.2" +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + jest-image-snapshot@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/jest-image-snapshot/-/jest-image-snapshot-6.2.0.tgz#b6bfc3e1585ec14b2b0595daf537ae1b4d7f8d13" @@ -9344,30 +9440,6 @@ jest-image-snapshot@^6.2.0: rimraf "^2.6.2" ssim.js "^3.1.1" -jest-jasmine2@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.2.4.tgz" - integrity sha512-fcffjO/xLWLVnW2ct3No4EksxM5RyPwHDYu9QU+90cC+/eSMLkFAxS55vkqsxexOO5zSsZ3foVpMQcg/amSeIQ== - dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^27.2.4" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - expect "^27.2.4" - is-generator-fn "^2.0.0" - jest-each "^27.2.4" - jest-matcher-utils "^27.2.4" - jest-message-util "^27.2.4" - jest-runtime "^27.2.4" - jest-snapshot "^27.2.4" - jest-util "^27.2.4" - pretty-format "^27.2.4" - throat "^6.0.1" - jest-jasmine2@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz" @@ -9391,14 +9463,6 @@ jest-jasmine2@^27.5.1: pretty-format "^27.5.1" throat "^6.0.1" -jest-leak-detector@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.2.4.tgz" - integrity sha512-SrcHWbe0EHg/bw2uBjVoHacTo5xosl068x2Q0aWsjr2yYuW2XwqrSkZV4lurUop0jhv1709ymG4or+8E4sH27Q== - dependencies: - jest-get-type "^27.0.6" - pretty-format "^27.2.4" - jest-leak-detector@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz" @@ -9415,6 +9479,14 @@ jest-leak-detector@^29.6.2: jest-get-type "^29.4.3" pretty-format "^29.6.2" +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-matcher-utils@^23.6.0: version "23.6.0" resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz" @@ -9434,16 +9506,6 @@ jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-matcher-utils@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.2.4.tgz" - integrity sha512-nQeLfFAIPPkyhkDfifAPfP/U5wm1x0fLtAzqXZSSKckXDNuk2aaOfQiDYv1Mgf5GY6yOsxfUnvNm3dDjXM+BXw== - dependencies: - chalk "^4.0.0" - jest-diff "^27.2.4" - jest-get-type "^27.0.6" - pretty-format "^27.2.4" - jest-matcher-utils@^29.6.2: version "29.6.2" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz#39de0be2baca7a64eacb27291f0bd834fea3a535" @@ -9454,6 +9516,16 @@ jest-matcher-utils@^29.6.2: jest-get-type "^29.4.3" pretty-format "^29.6.2" +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-message-util@^23.4.0: version "23.4.0" resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz" @@ -9465,21 +9537,6 @@ jest-message-util@^23.4.0: slash "^1.0.0" stack-utils "^1.0.1" -jest-message-util@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.2.4.tgz" - integrity sha512-wbKT/BNGnBVB9nzi+IoaLkXt6fbSvqUxx+IYY66YFh96J3goY33BAaNG3uPqaw/Sh/FR9YpXGVDfd5DJdbh4nA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.2.4" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.4" - pretty-format "^27.2.4" - slash "^3.0.0" - stack-utils "^2.0.3" - jest-message-util@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz" @@ -9510,13 +9567,20 @@ jest-message-util@^29.6.2: slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-27.2.4.tgz" - integrity sha512-iVRU905rutaAoUcrt5Tm1JoHHWi24YabqEGXjPJI4tAyA6wZ7mzDi3GrZ+M7ebgWBqUkZE93GAx1STk7yCMIQA== +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== dependencies: - "@jest/types" "^27.2.4" - "@types/node" "*" + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" jest-mock@^27.5.1: version "27.5.1" @@ -9535,16 +9599,20 @@ jest-mock@^29.6.2: "@types/node" "*" jest-util "^29.6.2" +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== -jest-regex-util@^27.0.6: - version "27.0.6" - resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz" - integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ== - jest-regex-util@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz" @@ -9555,14 +9623,10 @@ jest-regex-util@^29.4.3: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== -jest-resolve-dependencies@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.2.4.tgz" - integrity sha512-i5s7Uh9B3Q6uwxLpMhNKlgBf6pcemvWaORxsW1zNF/YCY3jd5EftvnGBI+fxVwJ1CBxkVfxqCvm1lpZkbaoGmg== - dependencies: - "@jest/types" "^27.2.4" - jest-regex-util "^27.0.6" - jest-snapshot "^27.2.4" +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== jest-resolve-dependencies@^27.5.1: version "27.5.1" @@ -9581,6 +9645,14 @@ jest-resolve-dependencies@^29.6.2: jest-regex-util "^29.4.3" jest-snapshot "^29.6.2" +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + jest-resolve@^23.6.0: version "23.6.0" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.6.0.tgz" @@ -9590,22 +9662,6 @@ jest-resolve@^23.6.0: chalk "^2.0.1" realpath-native "^1.0.0" -jest-resolve@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.2.4.tgz" - integrity sha512-IsAO/3+3BZnKjI2I4f3835TBK/90dxR7Otgufn3mnrDFTByOSXclDi3G2XJsawGV4/18IMLARJ+V7Wm7t+J89Q== - dependencies: - "@jest/types" "^27.2.4" - chalk "^4.0.0" - escalade "^3.1.1" - graceful-fs "^4.2.4" - jest-haste-map "^27.2.4" - jest-pnp-resolver "^1.2.2" - jest-util "^27.2.4" - jest-validate "^27.2.4" - resolve "^1.20.0" - slash "^3.0.0" - jest-resolve@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz" @@ -9637,33 +9693,20 @@ jest-resolve@^29.6.2: resolve.exports "^2.0.0" slash "^3.0.0" -jest-runner@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-27.2.4.tgz" - integrity sha512-hIo5PPuNUyVDidZS8EetntuuJbQ+4IHWxmHgYZz9FIDbG2wcZjrP6b52uMDjAEQiHAn8yn8ynNe+TL8UuGFYKg== +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== dependencies: - "@jest/console" "^27.2.4" - "@jest/environment" "^27.2.4" - "@jest/test-result" "^27.2.4" - "@jest/transform" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/node" "*" chalk "^4.0.0" - emittery "^0.8.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-docblock "^27.0.6" - jest-environment-jsdom "^27.2.4" - jest-environment-node "^27.2.4" - jest-haste-map "^27.2.4" - jest-leak-detector "^27.2.4" - jest-message-util "^27.2.4" - jest-resolve "^27.2.4" - jest-runtime "^27.2.4" - jest-util "^27.2.4" - jest-worker "^27.2.4" - source-map-support "^0.5.6" - throat "^6.0.1" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" jest-runner@^27.5.1: version "27.5.1" @@ -9719,38 +9762,32 @@ jest-runner@^29.6.2: p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.2.4.tgz" - integrity sha512-ICKzzYdjIi70P17MZsLLIgIQFCQmIjMFf+xYww3aUySiUA/QBPUTdUqo5B2eg4HOn9/KkUsV0z6GVgaqAPBJvg== - dependencies: - "@jest/console" "^27.2.4" - "@jest/environment" "^27.2.4" - "@jest/fake-timers" "^27.2.4" - "@jest/globals" "^27.2.4" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.2.4" - "@jest/transform" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/yargs" "^16.0.0" +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - execa "^5.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.4" - jest-haste-map "^27.2.4" - jest-message-util "^27.2.4" - jest-mock "^27.2.4" - jest-regex-util "^27.0.6" - jest-resolve "^27.2.4" - jest-snapshot "^27.2.4" - jest-util "^27.2.4" - jest-validate "^27.2.4" - slash "^3.0.0" - strip-bom "^4.0.0" - yargs "^16.2.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" jest-runtime@^27.5.1: version "27.5.1" @@ -9808,13 +9845,33 @@ jest-runtime@^29.6.2: slash "^3.0.0" strip-bom "^4.0.0" -jest-serializer@^27.0.6: - version "27.0.6" - resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.0.6.tgz" - integrity sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA== - dependencies: +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" - graceful-fs "^4.2.4" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" jest-serializer@^27.5.1: version "27.5.1" @@ -9840,36 +9897,6 @@ jest-snapshot@^23.6.0: pretty-format "^23.6.0" semver "^5.5.0" -jest-snapshot@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.2.4.tgz" - integrity sha512-5DFxK31rYS8X8C6WXsFx8XxrxW3PGa6+9IrUcZdTLg1aEyXDGIeiBh4jbwvh655bg/9vTETbEj/njfZicHTZZw== - dependencies: - "@babel/core" "^7.7.2" - "@babel/generator" "^7.7.2" - "@babel/parser" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/traverse" "^7.7.2" - "@babel/types" "^7.0.0" - "@jest/transform" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.1.5" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^27.2.4" - graceful-fs "^4.2.4" - jest-diff "^27.2.4" - jest-get-type "^27.0.6" - jest-haste-map "^27.2.4" - jest-matcher-utils "^27.2.4" - jest-message-util "^27.2.4" - jest-resolve "^27.2.4" - jest-util "^27.2.4" - natural-compare "^1.4.0" - pretty-format "^27.2.4" - semver "^7.3.2" - jest-snapshot@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz" @@ -9924,7 +9951,33 @@ jest-snapshot@^29.6.2: pretty-format "^29.6.2" semver "^7.5.3" -jest-util@^27.0.0, jest-util@^27.2.4: +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +jest-util@^27.0.0: version "27.2.4" resolved "https://registry.npmjs.org/jest-util/-/jest-util-27.2.4.tgz" integrity sha512-mW++4u+fSvAt3YBWm5IpbmRAceUqa2B++JlUZTiuEt2AmNYn0Yw5oay4cP17TGsMINRNPSGiJ2zNnX60g+VbFg== @@ -9960,17 +10013,17 @@ jest-util@^29.0.0, jest-util@^29.6.2: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-27.2.4.tgz" - integrity sha512-VMtbxbkd7LHnIH7PChdDtrluCFRJ4b1YV2YJzNwwsASMWftq/HgqiqjvptBOWyWOtevgO3f14wPxkPcLlVBRog== +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== dependencies: - "@jest/types" "^27.2.4" - camelcase "^6.2.0" + "@jest/types" "^29.6.3" + "@types/node" "*" chalk "^4.0.0" - jest-get-type "^27.0.6" - leven "^3.1.0" - pretty-format "^27.2.4" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" jest-validate@^27.5.1: version "27.5.1" @@ -9996,18 +10049,17 @@ jest-validate@^29.6.2: leven "^3.1.0" pretty-format "^29.6.2" -jest-watcher@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.2.4.tgz" - integrity sha512-LXC/0+dKxhK7cfF7reflRYlzDIaQE+fL4ynhKhzg8IMILNMuI4xcjXXfUJady7OR4/TZeMg7X8eHx8uan9vqaQ== +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== dependencies: - "@jest/test-result" "^27.2.4" - "@jest/types" "^27.2.4" - "@types/node" "*" - ansi-escapes "^4.2.1" + "@jest/types" "^29.6.3" + camelcase "^6.2.0" chalk "^4.0.0" - jest-util "^27.2.4" - string-length "^4.0.1" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" jest-watcher@^27.5.1: version "27.5.1" @@ -10036,6 +10088,20 @@ jest-watcher@^29.6.2: jest-util "^29.6.2" string-length "^4.0.1" +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + jest-worker@^26.2.1: version "26.6.2" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" @@ -10045,15 +10111,6 @@ jest-worker@^26.2.1: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.2.4.tgz" - integrity sha512-Zq9A2Pw59KkVjBBKD1i3iE2e22oSjXhUKKuAK1HGX8flGwkm6NMozyEYzKd41hXc64dbd/0eWFeEEuxqXyhM+g== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - jest-worker@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" @@ -10073,14 +10130,15 @@ jest-worker@^29.6.2: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^27.2.4: - version "27.2.4" - resolved "https://registry.npmjs.org/jest/-/jest-27.2.4.tgz" - integrity sha512-h4uqb1EQLfPulWyUFFWv9e9Nn8sCqsJ/j3wk/KCY0p4s4s0ICCfP3iMf6hRf5hEhsDyvyrCgKiZXma63gMz16A== +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== dependencies: - "@jest/core" "^27.2.4" - import-local "^3.0.2" - jest-cli "^27.2.4" + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" jest@^27.5.1: version "27.5.1" @@ -10101,6 +10159,16 @@ jest@^29.6.0: import-local "^3.0.2" jest-cli "^29.6.2" +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + jju@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" @@ -10136,39 +10204,6 @@ jsbn@~0.1.0: resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdom@^16.4.0: - version "16.6.0" - resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.6.0.tgz" - integrity sha512-Ty1vmF4NHJkolaEmdjtxTfSfkdb8Ywarwf63f+F8/mDD1uLSSWDxDuMiZxiPhwunLrn9LOSVItWj4bLYsLN3Dg== - dependencies: - abab "^2.0.5" - acorn "^8.2.4" - acorn-globals "^6.0.0" - cssom "^0.4.4" - cssstyle "^2.3.0" - data-urls "^2.0.0" - decimal.js "^10.2.1" - domexception "^2.0.1" - escodegen "^2.0.0" - form-data "^3.0.0" - html-encoding-sniffer "^2.0.1" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.0" - parse5 "6.0.1" - saxes "^5.0.1" - symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" - w3c-xmlserializer "^2.0.0" - webidl-conversions "^6.1.0" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.5.0" - ws "^7.4.5" - xml-name-validator "^3.0.0" - jsdom@^16.6.0: version "16.7.0" resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz" @@ -10234,6 +10269,33 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" +jsdom@^23.0.0: + version "23.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-23.0.0.tgz#7c8bac82e32e1ac3eef29096ea59d519c72ce4eb" + integrity sha512-cbL/UCtohJguhFC7c2/hgW6BeZCNvP7URQGnx9tSJRYKCdnfbfWOrtuLTMfiB2VxKsx5wPHVsh/J0aBy9lIIhQ== + dependencies: + cssstyle "^3.0.0" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.2" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.7" + parse5 "^7.1.2" + rrweb-cssom "^0.6.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.3" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.14.2" + xml-name-validator "^5.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" @@ -10618,7 +10680,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.x, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0, lodash@~4.17.15: +lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0, lodash@~4.17.15: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -11166,11 +11228,6 @@ node-int64@^0.4.0: resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-modules-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz" - integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= - node-notifier@9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.0.tgz#46c5bbecbb796d4a803f646cea5bc91403f2ff38" @@ -11266,7 +11323,7 @@ nwsapi@^2.2.0: resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -nwsapi@^2.2.2: +nwsapi@^2.2.2, nwsapi@^2.2.7: version "2.2.7" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== @@ -11650,7 +11707,7 @@ parse5@^7.0.0: dependencies: entities "^4.3.0" -parse5@^7.1.1: +parse5@^7.1.1, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== @@ -11807,13 +11864,6 @@ pino@7.9.1: sonic-boom "^2.2.1" thread-stream "^0.13.0" -pirates@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz" - integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== - dependencies: - node-modules-regexp "^1.0.0" - pirates@^4.0.4: version "4.0.4" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz" @@ -12335,6 +12385,15 @@ pretty-format@^29.0.0, pretty-format@^29.6.2: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" @@ -12429,6 +12488,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + pupa@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" @@ -13106,16 +13170,16 @@ rollup-plugin-typescript2@^0.31.2: resolve "^1.20.0" tslib "^2.3.1" -rollup-plugin-typescript2@^0.34.1: - version "0.34.1" - resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.34.1.tgz#c457f155a71d133c142689213fce78694e30d0be" - integrity sha512-P4cHLtGikESmqi1CA+tdMDUv8WbQV48mzPYt77TSTOPJpERyZ9TXdDgjSDix8Fkqce6soYz3+fa4lrC93IEkcw== +rollup-plugin-typescript2@^0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz#309564eb70d710412f5901344ca92045e180ed53" + integrity sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw== dependencies: "@rollup/pluginutils" "^4.1.2" find-cache-dir "^3.3.2" fs-extra "^10.0.0" - semver "^7.3.7" - tslib "^2.4.0" + semver "^7.5.4" + tslib "^2.6.2" rollup-plugin-web-worker-loader@^1.6.1: version "1.6.1" @@ -13129,7 +13193,7 @@ rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@^2.46.0, rollup@^2.79.1: +rollup@^2.47.0, rollup@^2.79.1: version "2.79.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== @@ -13164,6 +13228,11 @@ rollup@~2.78.0: optionalDependencies: fsevents "~2.3.2" +rrweb-cssom@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" + integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== + run-async@^2.4.0: version "2.4.1" resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" @@ -14241,7 +14310,7 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" -tough-cookie@^4.1.2: +tough-cookie@^4.1.2, tough-cookie@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== @@ -14273,6 +14342,13 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" +tr46@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" + integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== + dependencies: + punycode "^2.3.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" @@ -14295,20 +14371,6 @@ trim-repeated@^1.0.0: dependencies: escape-string-regexp "^1.0.2" -ts-jest@^27.0.5: - version "27.0.5" - resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.5.tgz" - integrity sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w== - dependencies: - bs-logger "0.x" - fast-json-stable-stringify "2.x" - jest-util "^27.0.0" - json5 "2.x" - lodash "4.x" - make-error "1.x" - semver "7.x" - yargs-parser "20.x" - ts-jest@^27.1.3: version "27.1.3" resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.3.tgz" @@ -14403,11 +14465,16 @@ tslib@^2.3.1: resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@^2.4.0, tslib@^2.5.3: +tslib@^2.5.3: version "2.5.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" @@ -14973,6 +15040,13 @@ w3c-xmlserializer@^4.0.0: dependencies: xml-name-validator "^4.0.0" +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + walker@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz" @@ -15077,6 +15151,13 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" @@ -15087,6 +15168,11 @@ whatwg-mimetype@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + whatwg-url@^11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" @@ -15095,6 +15181,14 @@ whatwg-url@^11.0.0: tr46 "^3.0.0" webidl-conversions "^7.0.0" +whatwg-url@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6" + integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw== + dependencies: + tr46 "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" @@ -15262,7 +15356,7 @@ ws@^7.2.3: resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz" integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== -ws@^7.4.3, ws@^7.4.5: +ws@^7.4.3: version "7.5.3" resolved "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz" integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== @@ -15272,6 +15366,11 @@ ws@^7.4.6: resolved "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz" integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== +ws@^8.14.2: + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" @@ -15287,6 +15386,11 @@ xml-name-validator@^4.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + xml2js@~0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" From 67a779e83eb93862dd201c92b71d01408569532c Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 27 Nov 2023 23:40:34 +0100 Subject: [PATCH 024/102] Add check types --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e210dda8ce..8240ee85e0 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "test:update": "yarn turbo run test:update", "format": "yarn prettier --write '**/*.{ts,md}'", "dev": "yarn turbo run dev", + "check-types": "yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references --check' 'yarn turbo run check-types'", "repl": "cd packages/rrweb && npm run repl", "live-stream": "cd packages/rrweb && yarn live-stream", "lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'", From 71d87d488d6b43c9882f9e357cf35374f054f678 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 14:44:39 +0100 Subject: [PATCH 025/102] Update snapshots to include assetCapture settings --- .../__snapshots__/integration.test.ts.snap | 276 +++++++++++++++--- .../test/__snapshots__/record.test.ts.snap | 132 +++++++-- .../cross-origin-iframes.test.ts.snap | 90 +++++- .../record/__snapshots__/webgl.test.ts.snap | 48 ++- 4 files changed, 455 insertions(+), 91 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index ae1aa7bf54..3f15913274 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 multiple \\"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 + } } }, { @@ -1049,7 +1061,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 + } } }, { @@ -1220,7 +1236,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 + } } }, { @@ -1398,7 +1418,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 + } } }, { @@ -1574,7 +1598,11 @@ exports[`record integration tests can record clicks 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1856,7 +1884,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 + } } }, { @@ -2639,7 +2671,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 + } } }, { @@ -3614,7 +3650,11 @@ exports[`record integration tests can record style changes compactly and preserv \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4104,7 +4144,11 @@ exports[`record integration tests can use maskInputOptions to configure which ty \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4975,7 +5019,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 + } } }, { @@ -5166,7 +5214,11 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -5931,7 +5983,11 @@ exports[`record integration tests should handle recursive console messages 1`] = \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -6111,7 +6167,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 + } } }, { @@ -6981,7 +7041,11 @@ exports[`record integration tests should mask password value attribute with mask \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -7428,7 +7492,11 @@ exports[`record integration tests should mask texts 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -7726,7 +7794,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 + } } }, { @@ -8024,7 +8096,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 + } } }, { @@ -8783,7 +8859,11 @@ exports[`record integration tests should not record blocked elements and its chi \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -8963,7 +9043,11 @@ exports[`record integration tests should not record blocked elements dynamically \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -9169,7 +9253,11 @@ exports[`record integration tests should not record input events on ignored elem \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -9580,7 +9668,11 @@ exports[`record integration tests should not record input values if dynamically \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -9812,7 +9904,11 @@ exports[`record integration tests should not record input values if maskAllInput \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -10682,7 +10778,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 + } } }, { @@ -10949,7 +11049,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 + } } }, { @@ -11223,7 +11327,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 + } } }, { @@ -11310,7 +11418,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 + } } }, { @@ -11529,7 +11641,11 @@ exports[`record integration tests should record console messages 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -12035,7 +12151,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 + } } }, { @@ -12432,7 +12552,11 @@ exports[`record integration tests should record images inside iframe with blob u \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": false, + \\"origins\\": false + } } }, { @@ -12825,7 +12949,11 @@ exports[`record integration tests should record images inside iframe with blob u \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": false, + \\"origins\\": false + } } }, { @@ -13293,7 +13421,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 + } } }, { @@ -13534,7 +13666,11 @@ exports[`record integration tests should record input userTriggered values if us \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -14434,7 +14570,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 + } } }, { @@ -14603,7 +14743,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 + } } }, { @@ -14786,7 +14930,11 @@ exports[`record integration tests should record mutations in iframes accross pag \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -15095,7 +15243,11 @@ exports[`record integration tests should record nested iframes and shadow doms 1 \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -15442,7 +15594,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 + } } }, { @@ -15892,7 +16048,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 + } } }, { @@ -16010,7 +16170,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 + } } }, { @@ -16128,7 +16292,11 @@ exports[`record integration tests should record shadow doms polyfilled by shadyd \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -16374,7 +16542,11 @@ exports[`record integration tests should record shadow doms polyfilled by synthe \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -16648,7 +16820,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 + } } }, { @@ -16863,7 +17039,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 + } } }, { @@ -17161,7 +17341,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 cb40f3328d..0bd7c0def4 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 1 \\"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 sna \\"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 sna \\"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 + } } }, { @@ -1487,7 +1519,11 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1787,7 +1823,11 @@ exports[`record captures nested stylesheet rules 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -1936,7 +1976,11 @@ exports[`record captures style property changes 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2065,7 +2109,11 @@ exports[`record captures stylesheet rules 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2203,7 +2251,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 + } } }, { @@ -2347,7 +2399,11 @@ exports[`record captures stylesheets with \`blob:\` url 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -2435,7 +2491,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 + } } }, { @@ -2670,7 +2730,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 + } } }, { @@ -2801,7 +2865,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 + } } }, { @@ -2926,7 +2994,11 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3262,7 +3334,11 @@ exports[`record loading stylesheets captures stylesheets that are still loading \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3442,7 +3518,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 + } } }, { @@ -3562,7 +3642,11 @@ exports[`record should record scroll position 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3682,7 +3766,11 @@ exports[`record without CSSGroupingRule support captures nested stylesheet rules \\"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 e160fb38d6..35ba745157 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 1`] \\"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 origi \\"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 cro \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -851,7 +863,11 @@ exports[`cross origin iframes blank.html should support packFn option in record( \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } }, \\"v\\": \\"v1\\" }, @@ -1020,7 +1036,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 + } } }, { @@ -2033,7 +2053,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 + } } }, { @@ -2689,7 +2713,11 @@ exports[`cross origin iframes move-node.html captures mutations on adopted style \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3103,7 +3131,11 @@ exports[`cross origin iframes move-node.html captures mutations on stylesheets 1 \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3522,7 +3554,11 @@ exports[`cross origin iframes move-node.html should record DOM attribute changes \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -3810,7 +3846,11 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`] \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4195,7 +4235,11 @@ exports[`cross origin iframes move-node.html should record DOM node removal 1`] \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4481,7 +4525,11 @@ exports[`cross origin iframes move-node.html should record DOM text changes 1`] \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -4767,7 +4815,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 + } } }, { @@ -5090,7 +5142,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 + } } }, { @@ -5372,7 +5428,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 + } } }, { diff --git a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap index c7aeb17e1c..d0b0ccbb23 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 + } } }, { @@ -166,7 +170,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 + } } }, { @@ -311,7 +319,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 + } } }, { @@ -404,7 +416,11 @@ exports[`record webgl will record changes to a canvas element before the canvas \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -532,7 +548,11 @@ exports[`record webgl will record changes to a canvas element before the canvas \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -660,7 +680,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 + } } }, { @@ -753,7 +777,11 @@ exports[`record webgl will record webgl variables 1`] = ` \\"data\\": { \\"href\\": \\"about:blank\\", \\"width\\": 1920, - \\"height\\": 1080 + \\"height\\": 1080, + \\"assetCapture\\": { + \\"objectURLs\\": true, + \\"origins\\": false + } } }, { @@ -866,7 +894,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 5b4a5b5ed907e955977f8f3232ab859e06e8a0f2 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:05:06 +0100 Subject: [PATCH 026/102] Mark `inlineImages` as deprecated in favor of `assetCapture` --- docs/recipes/assets.md | 2 +- docs/recipes/assets.zh_CN.md | 40 +++++++++++++++++++ guide.md | 52 ++++++++++++------------- guide.zh_CN.md | 3 +- 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 +++ 8 files changed, 95 insertions(+), 29 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 5881f3b86f..6469582d76 100644 --- a/guide.md +++ b/guide.md @@ -135,32 +135,32 @@ 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) | +| 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) | diff --git a/guide.zh_CN.md b/guide.zh_CN.md index dda679006c..c5f37faf7b 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -155,7 +155,8 @@ setInterval(save, 10 * 1000); | recordCanvas | false | 是否记录 canvas 内容, 可用选项:`false`, `true` | | recordCrossOriginIframes | false | 是否记录 cross origin iframes。 必须在每个子 iframe 中注入 rrweb 才能使其工作。 可用选项:`false`, `true` | | recordAfter | 'load' | 如果 document 还没有加载完成,recorder 将会在指定的事件触发后开始录制。可用选项: `DOMContentLoaded`, `load` | -| inlineImages | false | 是否将图片内容记内联录制 | +| ~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) | diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 81369e37ee..d125434df1 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -462,6 +462,9 @@ function serializeNode( maskTextFn: MaskTextFn | undefined; maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages: boolean; recordCanvas: boolean; keepIframeSrcFn: KeepIframeSrcFn; @@ -627,6 +630,9 @@ function serializeElementNode( maskInputOptions: MaskInputOptions; maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages: boolean; recordCanvas: boolean; keepIframeSrcFn: KeepIframeSrcFn; @@ -997,6 +1003,9 @@ export function serializeNodeWithId( slimDOMOptions: SlimDOMOptions; dataURLOptions?: DataURLOptions; keepIframeSrcFn?: KeepIframeSrcFn; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages?: boolean; recordCanvas?: boolean; preserveWhiteSpace?: boolean; @@ -1312,6 +1321,9 @@ function snapshot( maskInputFn?: MaskTextFn; 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 39693b1192..7b66c551e9 100644 --- a/packages/rrweb/scripts/stream.js +++ b/packages/rrweb/scripts/stream.js @@ -66,7 +66,10 @@ async function injectRecording(frame) { 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 9d72c74b5a..cedf260425 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -181,6 +181,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']; @@ -302,6 +305,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 ba1f8c05aa..66a3bc03de 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -69,6 +69,9 @@ export type recordOptions = { recordAfter?: 'DOMContentLoaded' | 'load'; userTriggeredOnInput?: boolean; collectFonts?: boolean; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages?: boolean; assetCapture?: assetCaptureParam; plugins?: RecordPlugin[]; @@ -106,6 +109,9 @@ export type observerParam = { sampling: SamplingStrategy; recordDOM: boolean; recordCanvas: boolean; + /** + * @deprecated please use `assetCapture` instead + */ inlineImages: boolean; userTriggeredOnInput: boolean; collectFonts: boolean; From 180f7fd266e9d73387fb58d675bedce536ad59d1 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:05:22 +0100 Subject: [PATCH 027/102] Fix rrdom build --- packages/rrdom-nodejs/tsconfig.json | 3 +++ packages/rrdom/jest.config.js | 10 ++++++++++ packages/rrdom/jest.setup.ts | 3 +++ 3 files changed, 16 insertions(+) create mode 100644 packages/rrdom/jest.setup.ts diff --git a/packages/rrdom-nodejs/tsconfig.json b/packages/rrdom-nodejs/tsconfig.json index 0c2f119853..b59df3f7ef 100644 --- a/packages/rrdom-nodejs/tsconfig.json +++ b/packages/rrdom-nodejs/tsconfig.json @@ -29,6 +29,9 @@ }, { "path": "../rrweb-snapshot" + }, + { + "path": "../types" } ] } diff --git a/packages/rrdom/jest.config.js b/packages/rrdom/jest.config.js index e5841e937d..3483edbdc2 100644 --- a/packages/rrdom/jest.config.js +++ b/packages/rrdom/jest.config.js @@ -2,4 +2,14 @@ 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, + * so we might want to remove this in its own PR + */ + snapshotFormat: { + escapeString: true, + printBasicPrototype: true, + }, }; 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 015e412fe6bee2c626232dcbc9d28477a864ae7a Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:09:57 +0100 Subject: [PATCH 028/102] 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 10a3a38eed..9fb0144bce 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -264,7 +264,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('http://localhost:3030/html/picture-blob-in-frame.html', { @@ -279,6 +279,10 @@ iframe.contentDocument.querySelector('center').clientHeight inlineStylesheet: false, onIframeLoad: function(iframe, sn) { window.snapshot = sn; + }, + assetCapture: { + origin: false, + objectURLs: false } })`); await waitForRAF(page); From 27663f36a70c3a88b2b5a2ee517e4782510e94a1 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:47:37 +0100 Subject: [PATCH 029/102] Rename `assetCapture` to `captureAssets` Also deprecate a couple inlineImages tests --- docs/recipes/assets.md | 10 +- docs/recipes/assets.zh_CN.md | 10 +- guide.md | 12 +- guide.zh_CN.md | 60 +- 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 | 1342 +++++++++++++++-- .../test/__snapshots__/record.test.ts.snap | 44 +- packages/rrweb/test/integration.test.ts | 75 +- .../cross-origin-iframes.test.ts.snap | 30 +- .../record/__snapshots__/webgl.test.ts.snap | 16 +- packages/rrweb/test/record/asset.test.ts | 28 +- packages/rrweb/test/utils.ts | 2 +- packages/types/src/index.ts | 4 +- 19 files changed, 1394 insertions(+), 275 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 6469582d76..0a40cf2a4e 100644 --- a/guide.md +++ b/guide.md @@ -160,12 +160,12 @@ 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. | -| 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. | +| ~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) | +| 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/guide.zh_CN.md b/guide.zh_CN.md index c5f37faf7b..09f57e442d 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -131,36 +131,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 | 是否将图片内容记内联录制 | -| 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 内部的某些内容抛出错误,则会调用该回调。 | +| 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 | 是否将图片内容记内联录制 | +| 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) | +| errorHandler | - | 一个可以定制化处理错误的回调函数,它的参数是错误对象。如果 rrweb recorder 内部的某些内容抛出错误,则会调用该回调。 | #### 隐私 diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index d125434df1..04155f3518 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -463,7 +463,7 @@ function serializeNode( maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages: boolean; recordCanvas: boolean; @@ -631,7 +631,7 @@ function serializeElementNode( maskInputFn: MaskInputFn | undefined; dataURLOptions?: DataURLOptions; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages: boolean; recordCanvas: boolean; @@ -1004,7 +1004,7 @@ export function serializeNodeWithId( dataURLOptions?: DataURLOptions; keepIframeSrcFn?: KeepIframeSrcFn; /** - * @deprecated please use `assetCapture` instead + * @deprecated please use `captureAssets` instead */ inlineImages?: boolean; recordCanvas?: boolean; @@ -1322,7 +1322,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 9fb0144bce..989e48cf54 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -280,7 +280,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 7b66c551e9..108f8f303a 100644 --- a/packages/rrweb/scripts/stream.js +++ b/packages/rrweb/scripts/stream.js @@ -66,7 +66,7 @@ async function injectRecording(frame) { 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 1410c45396..e361101bb3 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -83,7 +83,7 @@ function record( userTriggeredOnInput = false, collectFonts = false, inlineImages = false, - assetCapture = { + captureAssets = { objectURLs: true, origins: false, }, @@ -322,7 +322,7 @@ function record( assetManager = new AssetManager({ mutationCb: wrappedAssetEmit, win: window, - assetCapture, + captureAssets, }); const shadowDomManager = new ShadowDomManager({ @@ -363,7 +363,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 cedf260425..bbce696ec5 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -182,7 +182,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 66a3bc03de..3cf0cc2fe8 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -37,7 +37,7 @@ import type { styleDeclarationCallback, styleSheetRuleCallback, viewportResizeCallback, - assetCaptureParam, + captureAssetsParam, } from '@rrweb/types'; import type ProcessedNodeManager from './record/processed-node-manager'; import type AssetManager from './record/observers/asset-manager'; @@ -70,10 +70,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; @@ -110,7 +110,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 3f15913274..27d55dff60 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -1,5 +1,1119 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +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/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 41 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 41, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 41, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +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/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 47 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 47, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 47, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +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/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"id\\": 24 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + exports[`record integration tests can correctly serialize a shader and multiple webgl contexts 1`] = ` "[ { @@ -16,7 +1130,7 @@ exports[`record integration tests can correctly serialize a shader and multiple \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -434,7 +1548,7 @@ exports[`record integration tests can freeze mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -618,7 +1732,7 @@ exports[`record integration tests can mask character data mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1062,7 +2176,7 @@ exports[`record integration tests can record attribute mutation 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1237,7 +2351,7 @@ exports[`record integration tests can record character data muatations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1419,7 +2533,7 @@ exports[`record integration tests can record childList mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1599,7 +2713,7 @@ exports[`record integration tests can record clicks 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1885,7 +2999,7 @@ exports[`record integration tests can record form interactions 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2672,7 +3786,7 @@ exports[`record integration tests can record node mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3651,7 +4765,7 @@ exports[`record integration tests can record style changes compactly and preserv \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4145,7 +5259,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -5020,7 +6134,7 @@ exports[`record integration tests handles null attribute values 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -5215,7 +6329,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -5984,7 +7098,7 @@ exports[`record integration tests should handle recursive console messages 1`] = \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -6168,7 +7282,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 } @@ -7042,7 +8156,7 @@ exports[`record integration tests should mask password value attribute with mask \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -7493,7 +8607,7 @@ exports[`record integration tests should mask texts 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -7795,7 +8909,7 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -8097,7 +9211,7 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -8860,7 +9974,7 @@ exports[`record integration tests should not record blocked elements and its chi \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -9044,7 +10158,7 @@ exports[`record integration tests should not record blocked elements dynamically \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -9254,7 +10368,7 @@ exports[`record integration tests should not record input events on ignored elem \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -9669,7 +10783,7 @@ exports[`record integration tests should not record input values if dynamically \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -9905,7 +11019,7 @@ exports[`record integration tests should not record input values if maskAllInput \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -10779,7 +11893,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 } @@ -11050,7 +12164,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 } @@ -11328,7 +12442,7 @@ exports[`record integration tests should record after DOMContentLoaded event 1`] \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -11419,7 +12533,7 @@ exports[`record integration tests should record canvas mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -11642,7 +12756,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -12152,7 +13266,7 @@ exports[`record integration tests should record dynamic CSS changes 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -12536,7 +13650,6 @@ exports[`record integration tests should record dynamic CSS changes 1`] = ` } ]" `; - exports[`record integration tests should record images inside iframe with blob url 1`] = ` "[ { @@ -12553,8 +13666,8 @@ exports[`record integration tests should record images inside iframe with blob u \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { - \\"objectURLs\\": false, + \\"captureAssets\\": { + \\"objectURLs\\": true, \\"origins\\": false } } @@ -12886,8 +13999,7 @@ exports[`record integration tests should record images inside iframe with blob u \\"type\\": 2, \\"tagName\\": \\"img\\", \\"attributes\\": { - \\"src\\": \\"blob:http://localhost:xxxx/...\\", - \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + \\"src\\": \\"blob:http://localhost:xxxx/...\\" }, \\"childNodes\\": [], \\"rootId\\": 21, @@ -12898,37 +14010,19 @@ exports[`record integration tests should record images inside iframe with blob u } }, { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 41, - \\"attributes\\": { - \\"crossorigin\\": \\"anonymous\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, + \\"type\\": 7, \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 41, - \\"attributes\\": { - \\"crossorigin\\": null + \\"url\\": \\"blob:http://localhost:3030/2191dd12-9b7d-440a-88a1-60451e2cbae5\\", + \\"payload\\": { + \\"rr_type\\": \\"Blob\\", + \\"type\\": \\"text/plain\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"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\\" } - } - ], - \\"removes\\": [], - \\"adds\\": [] + ] + } } } ]" @@ -12950,8 +14044,8 @@ exports[`record integration tests should record images inside iframe with blob u \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { - \\"objectURLs\\": false, + \\"captureAssets\\": { + \\"objectURLs\\": true, \\"origins\\": false } } @@ -13358,8 +14452,7 @@ exports[`record integration tests should record images inside iframe with blob u \\"type\\": 2, \\"tagName\\": \\"img\\", \\"attributes\\": { - \\"src\\": \\"blob:http://localhost:xxxx/...\\", - \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + \\"src\\": \\"blob:http://localhost:xxxx/...\\" }, \\"childNodes\\": [], \\"rootId\\": 27, @@ -13370,37 +14463,19 @@ exports[`record integration tests should record images inside iframe with blob u } }, { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 47, - \\"attributes\\": { - \\"crossorigin\\": \\"anonymous\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, + \\"type\\": 7, \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 47, - \\"attributes\\": { - \\"crossorigin\\": null + \\"url\\": \\"blob:http://localhost:3030/1786bb8a-e2e5-4c5c-b2a5-ba9875e40c65\\", + \\"payload\\": { + \\"rr_type\\": \\"Blob\\", + \\"type\\": \\"text/plain\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"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\\" } - } - ], - \\"removes\\": [], - \\"adds\\": [] + ] + } } } ]" @@ -13422,8 +14497,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 } } @@ -13604,8 +14679,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 @@ -13615,37 +14689,19 @@ exports[`record integration tests should record images with blob url 1`] = ` } }, { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 24, - \\"attributes\\": { - \\"crossorigin\\": \\"anonymous\\" - } - } - ], - \\"removes\\": [], - \\"adds\\": [] - } - }, - { - \\"type\\": 3, + \\"type\\": 7, \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 24, - \\"attributes\\": { - \\"crossorigin\\": null + \\"url\\": \\"blob:http://localhost:3030/ed823e22-cc3c-436a-a460-d6dce1bd1ee9\\", + \\"payload\\": { + \\"rr_type\\": \\"Blob\\", + \\"type\\": \\"text/plain\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"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\\" } - } - ], - \\"removes\\": [], - \\"adds\\": [] + ] + } } } ]" @@ -13667,7 +14723,7 @@ exports[`record integration tests should record input userTriggered values if us \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -14571,7 +15627,7 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -14744,7 +15800,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 } @@ -14931,7 +15987,7 @@ exports[`record integration tests should record mutations in iframes accross pag \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -15244,7 +16300,7 @@ exports[`record integration tests should record nested iframes and shadow doms 1 \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -15595,7 +16651,7 @@ exports[`record integration tests should record shadow DOM 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -16049,7 +17105,7 @@ exports[`record integration tests should record shadow DOM 2 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -16171,7 +17227,7 @@ exports[`record integration tests should record shadow DOM 3 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -16293,7 +17349,7 @@ exports[`record integration tests should record shadow doms polyfilled by shadyd \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -16543,7 +17599,7 @@ exports[`record integration tests should record shadow doms polyfilled by synthe \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -16821,7 +17877,7 @@ exports[`record integration tests should record webgl canvas mutations 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -17040,7 +18096,7 @@ exports[`record integration tests should unmask texts using maskTextFn 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -17342,7 +18398,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 0bd7c0def4..01a3796d2a 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 1 \\"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 sna \\"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 sna \\"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 } @@ -1520,7 +1520,7 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1824,7 +1824,7 @@ exports[`record captures nested stylesheet rules 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1977,7 +1977,7 @@ exports[`record captures style property changes 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2110,7 +2110,7 @@ exports[`record captures stylesheet rules 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2252,7 +2252,7 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2400,7 +2400,7 @@ exports[`record captures stylesheets with \`blob:\` url 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2492,7 +2492,7 @@ exports[`record iframes captures stylesheet mutations in iframes 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2731,7 +2731,7 @@ exports[`record is safe to checkout during async callbacks 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2866,7 +2866,7 @@ exports[`record is safe to checkout during async callbacks 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -2995,7 +2995,7 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3335,7 +3335,7 @@ exports[`record loading stylesheets captures stylesheets that are still loading \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3519,7 +3519,7 @@ exports[`record no need for attribute mutations on adds 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3643,7 +3643,7 @@ exports[`record should record scroll position 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3767,7 +3767,7 @@ exports[`record without CSSGroupingRule support captures nested stylesheet rules \\"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 24953ac938..7b81823414 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -831,14 +831,14 @@ describe('record integration tests', function (this: ISuite) { 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`); @@ -851,14 +851,14 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); - it('should record images inside iframe with blob url', async () => { + 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, - assetCapture: { objectURLs: false, origins: false }, + captureAssets: { objectURLs: false, origins: false }, }), ); await page.waitForResponse(`${serverURL}/html/assets/robot.png`); @@ -871,14 +871,77 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); - it('should record images inside iframe with blob url after iframe was reloaded', async () => { + 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, - assetCapture: { objectURLs: false, origins: false }, + 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`); + 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('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', { + captureAssets: { objectURLs: true, 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('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', { + 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 35ba745157..1dfc3d7e72 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 1`] \\"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 origi \\"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 cro \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -864,7 +864,7 @@ exports[`cross origin iframes blank.html should support packFn option in record( \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -1037,7 +1037,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 } @@ -2054,7 +2054,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 } @@ -2714,7 +2714,7 @@ exports[`cross origin iframes move-node.html captures mutations on adopted style \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3132,7 +3132,7 @@ exports[`cross origin iframes move-node.html captures mutations on stylesheets 1 \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3555,7 +3555,7 @@ exports[`cross origin iframes move-node.html should record DOM attribute changes \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -3847,7 +3847,7 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`] \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4236,7 +4236,7 @@ exports[`cross origin iframes move-node.html should record DOM node removal 1`] \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4526,7 +4526,7 @@ exports[`cross origin iframes move-node.html should record DOM text changes 1`] \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -4816,7 +4816,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 } @@ -5143,7 +5143,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 } @@ -5429,7 +5429,7 @@ exports[`same origin iframes should emit contents of iframe once 1`] = ` \\"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 d0b0ccbb23..7dfc3d0155 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 } @@ -171,7 +171,7 @@ exports[`record webgl should batch events by RAF 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -320,7 +320,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 } @@ -417,7 +417,7 @@ exports[`record webgl will record changes to a canvas element before the canvas \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -549,7 +549,7 @@ exports[`record webgl will record changes to a canvas element before the canvas \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -681,7 +681,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 } @@ -778,7 +778,7 @@ exports[`record webgl will record webgl variables 1`] = ` \\"href\\": \\"about:blank\\", \\"width\\": 1920, \\"height\\": 1080, - \\"assetCapture\\": { + \\"captureAssets\\": { \\"objectURLs\\": true, \\"origins\\": false } @@ -895,7 +895,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/record/asset.test.ts b/packages/rrweb/test/record/asset.test.ts index 956ef2958e..9cb7d2ff5d 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 = { - assetCapture?: recordOptions['assetCapture']; + captureAssets?: recordOptions['captureAssets']; }; 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 = { - assetCapture: options.assetCapture, + captureAssets: options.captureAssets, emit(event) { (window as unknown as IWindow).snapshots.push(event); (window as unknown as IWindow).emit(event); @@ -104,10 +104,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), ); } @@ -140,7 +140,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { objectURLs: true, origins: false, }, @@ -282,7 +282,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { objectURLs: true, origins: false, }, @@ -327,7 +327,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { objectURLs: false, origins: false, }, @@ -407,7 +407,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: false, objectURLs: false, }, @@ -439,7 +439,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: [], objectURLs: false, }, @@ -471,7 +471,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: true, objectURLs: false, }, @@ -508,7 +508,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: true, objectURLs: false, }, @@ -578,7 +578,7 @@ describe('asset caching', function (this: ISuite) { `, { - assetCapture: { + captureAssets: { origins: ['{SERVER_URL}'], objectURLs: false, }, @@ -640,7 +640,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, }, diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 27b721dda4..f87c9a3fbb 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -702,7 +702,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 92200402b2..e5fa4bac99 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 b873923f37bd8655df70f253f79ac5431f31af1d Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 28 Nov 2023 21:48:41 +0100 Subject: [PATCH 030/102] 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 f1c263b527bb56b9492eed2b7e7766a997f30933 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 30 Nov 2023 16:42:27 +0100 Subject: [PATCH 031/102] 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 | 6 +- packages/rrweb/test/replay/asset-unit.test.ts | 62 +++++++++++++++++-- packages/types/src/index.ts | 2 + 5 files changed, 90 insertions(+), 14 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 5815ea60c4..d212e42b5e 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -287,9 +287,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 f8bf1c5809..9f9f52c8e5 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1814,11 +1814,7 @@ export class Replayer { } else { targetEl.setAttribute(attributeName, value); } - if ( - this.assetManager.isAttributeCacheable(targetEl, attributeName) - ) { - void this.assetManager.manageAttribute(targetEl, attributeName); - } + void this.assetManager.manageAttribute(targetEl, attributeName); } catch (error) { this.warn( 'An error occurred may due to the checkout feature.', diff --git a/packages/rrweb/test/replay/asset-unit.test.ts b/packages/rrweb/test/replay/asset-unit.test.ts index 2e821f01e5..3c00b5fca2 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'; describe('AssetManager', () => { let assetManager: AssetManager; @@ -28,7 +33,10 @@ describe('AssetManager', () => { }); beforeEach(() => { - assetManager = new AssetManager(); + assetManager = new AssetManager({ + origins: true, + objectURLs: true, + }); }); afterEach(() => { @@ -141,7 +149,7 @@ describe('AssetManager', () => { await expect(promise).resolves.toEqual({ status: 'reset' }); }); - const validCombinations = [ + const validAttributeCombinations = [ ['img', ['src', 'srcset']], ['video', ['src']], ['audio', ['src']], @@ -153,7 +161,7 @@ describe('AssetManager', () => { ['object', ['src']], ] as const; - const invalidCombinations = [ + const invalidAttributeCombinations = [ ['img', ['href']], ['script', ['href']], ['link', ['src']], @@ -167,7 +175,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`, () => { @@ -178,7 +186,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`, () => { @@ -189,6 +197,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 e5fa4bac99..204a7b679a 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 ffa46d5031b206d4b83d90dbbb8a6be41ca06e71 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 30 Nov 2023 16:43:42 +0100 Subject: [PATCH 032/102] 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 9cb7d2ff5d..fb76101973 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -478,6 +478,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 db9476532dbcf398c6ec946061e7e389a1f98584 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 30 Nov 2023 16:56:11 +0100 Subject: [PATCH 033/102] 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 27d55dff60..042a6e45df 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -14012,7 +14012,7 @@ exports[`record integration tests should record images inside iframe with blob u { \\"type\\": 7, \\"data\\": { - \\"url\\": \\"blob:http://localhost:3030/2191dd12-9b7d-440a-88a1-60451e2cbae5\\", + \\"url\\": \\"blob:http://localhost:3030/...\\", \\"payload\\": { \\"rr_type\\": \\"Blob\\", \\"type\\": \\"text/plain\\", @@ -14465,7 +14465,7 @@ exports[`record integration tests should record images inside iframe with blob u { \\"type\\": 7, \\"data\\": { - \\"url\\": \\"blob:http://localhost:3030/1786bb8a-e2e5-4c5c-b2a5-ba9875e40c65\\", + \\"url\\": \\"blob:http://localhost:3030/...\\", \\"payload\\": { \\"rr_type\\": \\"Blob\\", \\"type\\": \\"text/plain\\", @@ -14691,7 +14691,7 @@ exports[`record integration tests should record images with blob url 1`] = ` { \\"type\\": 7, \\"data\\": { - \\"url\\": \\"blob:http://localhost:3030/ed823e22-cc3c-436a-a460-d6dce1bd1ee9\\", + \\"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 f87c9a3fbb..4afc2df1f2 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -132,6 +132,9 @@ 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 634c1aecbf950eaf9261c3ce67a73d16a969e6c3 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 30 Nov 2023 16:57:01 +0100 Subject: [PATCH 034/102] 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 04155f3518..1c5ae940ac 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -782,9 +782,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 d756e6104847ba5c14b4fe32268126e497a1de82 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 1 Dec 2023 10:55:29 +0100 Subject: [PATCH 035/102] 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 1c5ae940ac..6732bc89cb 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -28,6 +28,7 @@ import { toLowerCase, extractFileExtension, getUrlsFromSrcset, + isAttributeCacheable, } from './utils'; let _id = 1; @@ -673,12 +674,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 @@ -774,15 +784,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 99fab105f6..672c6ea723 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -368,3 +368,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 9f9f52c8e5..a3f1222e72 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -391,7 +391,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, @@ -673,11 +675,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 d289e69638..7f74c4cc06 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -590,25 +590,3 @@ export function inDom(n: Node): boolean { if (!doc) return false; return doc.contains(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 fb76101973..1105d07326 100644 --- a/packages/rrweb/test/record/asset.test.ts +++ b/packages/rrweb/test/record/asset.test.ts @@ -596,8 +596,15 @@ describe('asset caching', function (this: ISuite) { - - + + +