diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 536bb75b0f..d0f93c9ea0 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -16,6 +16,7 @@ import { elementNode, asset, DataURLOptions, + captureAssetsParam, } from '@rrweb/types'; import { Mirror, @@ -29,6 +30,7 @@ import { toLowerCase, extractFileExtension, isAttributeCapturable, + shouldIgnoreAsset, } from './utils'; let _id = 1; @@ -462,6 +464,7 @@ function serializeNode( * @deprecated please use `captureAssets` instead */ inlineImages?: boolean; + captureAssets?: captureAssetsParam; recordCanvas: boolean; keepIframeSrcFn: KeepIframeSrcFn; /** @@ -489,6 +492,10 @@ function serializeNode( maskInputFn, dataURLOptions = {}, inlineImages, + captureAssets = { + objectURLs: true, + origins: false, + }, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, @@ -528,6 +535,7 @@ function serializeNode( maskInputFn, dataURLOptions, inlineImages, + captureAssets, recordCanvas, keepIframeSrcFn, newlyAddedElement, @@ -630,6 +638,7 @@ function serializeElementNode( * @deprecated please use `captureAssets` instead */ inlineImages?: boolean; + captureAssets?: captureAssetsParam; recordCanvas: boolean; keepIframeSrcFn: KeepIframeSrcFn; /** @@ -655,6 +664,10 @@ function serializeElementNode( maskInputFn, dataURLOptions = {}, inlineImages, + captureAssets = { + objectURLs: true, + origins: false, + }, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, @@ -681,7 +694,8 @@ function serializeElementNode( value && typeof value === 'string' && onAssetDetected && - isAttributeCapturable(n, attr.name) + isAttributeCapturable(n, attr.name) && + !shouldIgnoreAsset(attr.value, captureAssets) ) { assets.push({ element: n, @@ -1015,6 +1029,7 @@ export function serializeNodeWithId( * @deprecated please use `captureAssets` instead */ inlineImages?: boolean; + captureAssets?: captureAssetsParam; recordCanvas?: boolean; preserveWhiteSpace?: boolean; onSerialize?: (n: Node) => unknown; @@ -1052,6 +1067,10 @@ export function serializeNodeWithId( slimDOMOptions, dataURLOptions = {}, inlineImages = false, + captureAssets = { + objectURLs: true, + origins: false, + }, recordCanvas = false, onSerialize, onIframeLoad, @@ -1091,6 +1110,7 @@ export function serializeNodeWithId( maskInputFn, dataURLOptions, inlineImages, + captureAssets, recordCanvas, keepIframeSrcFn, newlyAddedElement, @@ -1167,6 +1187,7 @@ export function serializeNodeWithId( slimDOMOptions, dataURLOptions, inlineImages, + captureAssets, recordCanvas, preserveWhiteSpace, onSerialize, @@ -1246,6 +1267,7 @@ export function serializeNodeWithId( slimDOMOptions, dataURLOptions, inlineImages, + captureAssets, recordCanvas, preserveWhiteSpace, onSerialize, @@ -1298,6 +1320,7 @@ export function serializeNodeWithId( slimDOMOptions, dataURLOptions, inlineImages, + captureAssets, recordCanvas, preserveWhiteSpace, onSerialize, @@ -1341,6 +1364,7 @@ function snapshot( * @deprecated please use `captureAssets` instead */ inlineImages?: boolean; + captureAssets?: captureAssetsParam; recordCanvas?: boolean; preserveWhiteSpace?: boolean; onSerialize?: (n: Node) => unknown; @@ -1372,6 +1396,10 @@ function snapshot( maskTextSelector = null, inlineStylesheet = true, inlineImages = false, + captureAssets = { + objectURLs: true, + origins: false, + }, recordCanvas = false, maskAllInputs = false, maskTextFn, @@ -1445,6 +1473,7 @@ function snapshot( slimDOMOptions, dataURLOptions, inlineImages, + captureAssets, recordCanvas, preserveWhiteSpace, onSerialize, diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 8bac271fea..ae990c978e 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -9,6 +9,7 @@ import { documentTypeNode, textNode, elementNode, + captureAssetsParam, } from '@rrweb/types'; export function isElement(n: Node): n is Element { @@ -406,3 +407,30 @@ export function isAttributeCapturable(n: Element, attribute: string): boolean { } return acceptedAttributesSet.has(attribute); } + +export function shouldIgnoreAsset( + url: string, + config: captureAssetsParam, +): 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 !config.objectURLs; + + // Check if url matches any ignorable origins + for (const origin of originsToIgnore) { + if (url.startsWith(origin)) return true; + } + + // Check the origins + const captureOrigins = config.origins; + if (typeof captureOrigins === 'boolean') { + return !captureOrigins; + } else if (Array.isArray(captureOrigins)) { + const urlOrigin = new URL(url).origin; + return !captureOrigins.includes(urlOrigin); + } + + return false; +} diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index bf77a0dd2b..f6f4a23c32 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -277,6 +277,10 @@ describe('onAssetDetected callback', () => { slimDOMOptions: {}, newlyAddedElement: false, inlineImages: false, + captureAssets: { + objectURLs: true, + origins: ['https://example.com'], + }, onAssetDetected, }); }; diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index e4c9a30441..2047a289aa 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -342,6 +342,7 @@ function record( inlineStylesheet, maskInputOptions, dataURLOptions, + captureAssets, maskTextFn, maskInputFn, recordCanvas, @@ -390,6 +391,7 @@ function record( maskTextFn, slimDOM: slimDOMOptions, dataURLOptions, + captureAssets, recordCanvas, onSerialize: (n) => { if (isSerializedIframe(n, mirror)) { @@ -550,6 +552,7 @@ function record( keepIframeSrcFn, blockSelector, slimDOMOptions, + captureAssets, dataURLOptions, mirror, iframeManager, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index ea64e90739..6cfd0baa6b 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -180,6 +180,7 @@ export default class MutationBuffer { private maskInputFn: observerParam['maskInputFn']; private keepIframeSrcFn: observerParam['keepIframeSrcFn']; private recordCanvas: observerParam['recordCanvas']; + private captureAssets: observerParam['captureAssets']; private slimDOMOptions: observerParam['slimDOMOptions']; private dataURLOptions: observerParam['dataURLOptions']; private doc: observerParam['doc']; @@ -205,6 +206,7 @@ export default class MutationBuffer { 'maskTextFn', 'maskInputFn', 'keepIframeSrcFn', + 'captureAssets', 'recordCanvas', 'slimDOMOptions', 'dataURLOptions', @@ -315,6 +317,7 @@ export default class MutationBuffer { maskInputFn: this.maskInputFn, slimDOMOptions: this.slimDOMOptions, dataURLOptions: this.dataURLOptions, + captureAssets: this.captureAssets, recordCanvas: this.recordCanvas, onSerialize: (currentN) => { if (isSerializedIframe(currentN, this.mirror)) { diff --git a/packages/rrweb/src/record/observers/asset-manager.ts b/packages/rrweb/src/record/observers/asset-manager.ts index 455f7a3413..a6edb82d4f 100644 --- a/packages/rrweb/src/record/observers/asset-manager.ts +++ b/packages/rrweb/src/record/observers/asset-manager.ts @@ -11,7 +11,11 @@ import { encode } from 'base64-arraybuffer'; import { patch } from '../../utils'; import type { recordOptions, assetStatus } from '../../types'; -import { isAttributeCapturable, getSourcesFromSrcset } from 'rrweb-snapshot'; +import { + isAttributeCapturable, + getSourcesFromSrcset, + shouldIgnoreAsset, +} from 'rrweb-snapshot'; export default class AssetManager { private urlObjectMap = new Map(); @@ -85,27 +89,7 @@ export default class AssetManager { } 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.objectURLs; - - // Check if url matches any ignorable origins - for (const origin of originsToIgnore) { - if (url.startsWith(origin)) return true; - } - - // Check the origins - const captureOrigins = this.config.origins; - if (typeof captureOrigins === 'boolean') { - return !captureOrigins; - } else if (Array.isArray(captureOrigins)) { - const urlOrigin = new URL(url).origin; - return !captureOrigins.includes(urlOrigin); - } - - return false; + return shouldIgnoreAsset(url, this.config); } public async getURLObject( @@ -139,7 +123,10 @@ export default class AssetManager { } public captureUrl(url: string): assetStatus { - if (this.shouldIgnore(url)) return { status: 'refused' }; + if (this.shouldIgnore(url)) { + console.warn(`snapshot.ts should know to ignore ${url}`); + return { status: 'refused' }; + } if (this.capturedURLs.has(url)) { return { status: 'captured' }; diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 1387485096..336f69f9fa 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -108,6 +108,7 @@ export type observerParam = { fontCb: fontCallback; sampling: SamplingStrategy; recordDOM: boolean; + captureAssets: captureAssetsParam; recordCanvas: boolean; userTriggeredOnInput: boolean; collectFonts: boolean; @@ -145,6 +146,7 @@ export type MutationBufferParam = Pick< | 'maskTextFn' | 'maskInputFn' | 'keepIframeSrcFn' + | 'captureAssets' | 'recordCanvas' | 'slimDOMOptions' | 'dataURLOptions' 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 74036c6add..e160fb38d6 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 @@ -280,7 +280,7 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`] \\"type\\": 2, \\"tagName\\": \\"source\\", \\"attributes\\": { - \\"rr_captured_src\\": \\"http://localhost:3030/html/assets/1-minute-of-silence.mp3\\", + \\"src\\": \\"http://localhost:3030/html/assets/1-minute-of-silence.mp3\\", \\"type\\": \\"audio/mpeg\\" }, \\"childNodes\\": [],