From 1fa6fcccf2e808869c31251f29aa493a36717cde Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 1 Dec 2023 10:55:29 +0100 Subject: [PATCH] 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 280ec70074..ebb68e491e 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -25,6 +25,7 @@ import { getInputType, toLowerCase, getUrlsFromSrcset, + isAttributeCacheable, } from './utils'; let _id = 1; @@ -674,12 +675,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 @@ -776,15 +786,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 2b8b38d1ee..8000854ab7 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -348,3 +348,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 3fee096751..b85045ffe2 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -380,7 +380,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, @@ -653,11 +655,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) { - - + + +