Skip to content

Commit

Permalink
Fix asset caching bug
Browse files Browse the repository at this point in the history
  • Loading branch information
Juice10 committed Dec 1, 2023
1 parent 3351246 commit 1fa6fcc
Show file tree
Hide file tree
Showing 11 changed files with 123 additions and 71 deletions.
23 changes: 12 additions & 11 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
getInputType,
toLowerCase,
getUrlsFromSrcset,
isAttributeCacheable,
} from './utils';

let _id = 1;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
22 changes: 22 additions & 0 deletions packages/rrweb-snapshot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
4 changes: 3 additions & 1 deletion packages/rrweb/src/record/observers/asset-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, File | Blob | MediaSource>();
Expand Down
20 changes: 10 additions & 10 deletions packages/rrweb/src/replay/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -149,14 +149,13 @@ export default class AssetManager implements RebuildAssetManagerInterface {
const originalValue = node.getAttribute(attribute);
if (!originalValue) return false;

const promises = [];
const promises: Promise<unknown>[] = [];

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) => {
Expand All @@ -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);
});
Expand Down
8 changes: 6 additions & 2 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = () => {
Expand Down
22 changes: 0 additions & 22 deletions packages/rrweb/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
8 changes: 6 additions & 2 deletions packages/rrweb/test/events/assets-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const events: eventWithTime[] = [
href: '',
width: 1600,
height: 900,
captureAssets: {
origins: ['ftp://example.com'],
objectURLs: false,
},
},
timestamp: 1636379531385,
},
Expand Down Expand Up @@ -109,7 +113,7 @@ const events: eventWithTime[] = [
{
id: 16,
attributes: {
src: 'httpx://example.com/image.png',
src: 'ftp://example.com/image.png',
},
},
],
Expand All @@ -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',
Expand Down
16 changes: 16 additions & 0 deletions packages/rrweb/test/html/assets/subtitles.vtt
Original file line number Diff line number Diff line change
@@ -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
65 changes: 44 additions & 21 deletions packages/rrweb/test/record/asset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,8 +596,15 @@ describe('asset caching', function (this: ISuite) {
<!DOCTYPE html>
<html>
<body>
<img src="{SERVER_URL}/html/assets/robot.png" />
<img src="{SERVER_B_URL}/html/assets/robot.png" />
<img src="{SERVER_URL}/html/assets/robot.png?img" />
<video><track default kind="captions" srclang="en" src="{SERVER_URL}/html/assets/subtitles.vtt" /><source src="{SERVER_URL}/html/assets/1-minute-of-silence.mp3?source" /></video>
<video src="{SERVER_URL}/html/assets/1-minute-of-silence.mp3?video" type="audio/mp3" />
<audio src="{SERVER_URL}/html/assets/1-minute-of-silence.mp3?audio" type="audio/mp3" />
<embed type="video/webm" src="{SERVER_URL}/html/assets/1-minute-of-silence.mp3?embed" width="250" height="200" />
<img srcset="{SERVER_URL}/html/assets/robot.png?1x, {SERVER_URL}/html/assets/robot.png?2x 2x" />
<img src="{SERVER_B_URL}/html/assets/robot.png?img" />
<input type="image" id="image" alt="Login" src="{SERVER_URL}/html/assets/robot.png?input-type-image" />
<iframe src="{SERVER_URL}/html/assets/robot.png?iframe" />
</body>
</html>
`,
Expand All @@ -609,25 +616,41 @@ describe('asset caching', function (this: ISuite) {
},
);

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),
},
}),
);
[
`{SERVER_URL}/html/assets/robot.png?img`,
`{SERVER_URL}/html/assets/1-minute-of-silence.mp3?audio`,
`{SERVER_URL}/html/assets/1-minute-of-silence.mp3?video`,
`{SERVER_URL}/html/assets/1-minute-of-silence.mp3?source`,
`{SERVER_URL}/html/assets/1-minute-of-silence.mp3?embed`,
'{SERVER_URL}/html/assets/subtitles.vtt',
'{SERVER_URL}/html/assets/robot.png?1x',
'{SERVER_URL}/html/assets/robot.png?2x',
'{SERVER_URL}/html/assets/robot.png?input-type-image',
'{SERVER_URL}/html/assets/robot.png?iframe',
].forEach((u) => {
it(`should capture ${u} with origin defined in config`, async () => {
const url = u.replace(/\{SERVER_URL\}/g, ctx.serverURL);
console.log(url, ctx.serverURL);
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,
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);
Expand All @@ -641,7 +664,7 @@ describe('asset caching', function (this: ISuite) {
expect.objectContaining({
type: EventType.Asset,
data: {
url: `${ctx.serverBURL}/html/assets/robot.png`,
url: `${ctx.serverBURL}/html/assets/robot.png?img`,
payload: expect.any(Object),
},
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/rrweb/test/replay/asset-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ describe('replayer', function () {
replayer.pause(0);
`);

await waitForRAF(page);

const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
Expand Down
4 changes: 2 additions & 2 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,11 +751,11 @@ export type RebuildAssetManagerStatus =
| RebuildAssetManagerFinalStatus;

export declare abstract class RebuildAssetManagerInterface {
constructor(config: captureAssetsParam);
constructor(config: captureAssetsParam | undefined);
abstract add(event: assetEvent): Promise<void>;
abstract get(url: string): RebuildAssetManagerStatus;
abstract whenReady(url: string): Promise<RebuildAssetManagerFinalStatus>;
abstract reset(): void;
abstract reset(config: captureAssetsParam | undefined): void;
abstract isAttributeCacheable(n: Element, attribute: string): boolean;
abstract isURLOfCacheableOrigin(url: string): boolean;
abstract manageAttribute(n: Element, attribute: string): void;
Expand Down

0 comments on commit 1fa6fcc

Please sign in to comment.