Skip to content

Commit f233472

Browse files
committed
Set AssetEvent timestamps such that they match the FullSnapshot or IncrementalSnapshot (mutation) that generated them.
The logic is that this is the time they were created, even if capture was delayed for at record time. If sorting by timestamp is performed before replay, then so long as the sort algorithm is stable, this will bring the asset events closer to the snapshot that they are associated with, where they are needed for replay
1 parent 21b231e commit f233472

File tree

4 files changed

+79
-31
lines changed

4 files changed

+79
-31
lines changed

packages/rrweb/src/record/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,10 +343,11 @@ function record<T = eventWithTime>(
343343
},
344344
});
345345

346-
const wrappedAssetEmit = (p: assetParam) =>
346+
const wrappedAssetEmit = (p: assetParam, snapshotTimestamp: number) =>
347347
wrappedEmit({
348348
type: EventType.Asset,
349349
data: p,
350+
timestamp: snapshotTimestamp,
350351
});
351352

352353
const wrappedAdoptedStyleSheetEmit = (a: adoptedStyleSheetParam) =>
@@ -491,7 +492,10 @@ function record<T = eventWithTime>(
491492
stylesheetManager.attachLinkElement(linkEl, childSn);
492493
},
493494
onAssetDetected: (asset: asset) => {
494-
const assetStatus = assetManager.capture(asset);
495+
const assetStatus = assetManager.capture(
496+
asset,
497+
true, // indicate it's a FullSnapshot
498+
);
495499
if (Array.isArray(assetStatus)) {
496500
// removeme when we just capture one asset from srcset
497501
capturedAssetStatuses.push(...assetStatus);
@@ -512,9 +516,12 @@ function record<T = eventWithTime>(
512516
if (capturedAssetStatuses.length) {
513517
data.capturedAssetStatuses = capturedAssetStatuses;
514518
}
519+
let now = nowTimestamp();
520+
assetManager.lastFullSnapshotTimestamp = now;
515521
wrappedEmit(
516522
{
517523
type: EventType.FullSnapshot,
524+
timestamp: now,
518525
data,
519526
},
520527
isCheckout,

packages/rrweb/src/record/mutation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export default class MutationBuffer {
241241
ancestorBad = false;
242242
}
243243
}
244-
244+
245245
if (this.addedSet.has(parentNode.lastChild as Node)) {
246246
// jump instead of crawling nextSibling to nextSibling
247247
n = parentNode.lastChild as Node;
@@ -361,7 +361,7 @@ export default class MutationBuffer {
361361
},
362362
cssCaptured,
363363
onAssetDetected: (asset: asset) => {
364-
this.assetManager.capture(asset);
364+
this.assetManager.capture(asset, now);
365365
},
366366
});
367367
if (sn) {

packages/rrweb/src/record/observers/asset-manager.ts

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { encode } from 'base64-arraybuffer';
1414
import { patch } from '@rrweb/utils';
1515

1616
import type { recordOptions, ProcessingStyleElement } from '../../types';
17+
import { nowTimestamp } from '../../utils';
1718
import {
1819
getSourcesFromSrcset,
1920
shouldCaptureAsset,
@@ -41,6 +42,8 @@ export default class AssetManager {
4142
undefined
4243
>;
4344

45+
public lastFullSnapshotTimestamp: number;
46+
4447
public reset() {
4548
this.urlObjectMap.clear();
4649
this.urlTextMap.clear();
@@ -133,6 +136,7 @@ export default class AssetManager {
133136
sheetBaseHref: string,
134137
el: HTMLLinkElement | HTMLStyleElement,
135138
styleId?: number,
139+
snapshotTimestamp?: number | true,
136140
): assetStatus {
137141
let cssRules: CSSRuleList;
138142
let url = sheetBaseHref; // linkEl.href for a link element
@@ -143,14 +147,23 @@ export default class AssetManager {
143147
if (!linkAppliedQuery.matches) {
144148
try {
145149
try {
146-
linkAppliedQuery.addEventListener(
147-
'change',
148-
() => this.captureStylesheet(sheetBaseHref, el, styleId),
150+
linkAppliedQuery.addEventListener('change', () =>
151+
this.captureStylesheet(
152+
sheetBaseHref,
153+
el,
154+
styleId,
155+
nowTimestamp(), // the time of the change event
156+
),
149157
);
150158
} catch (e1) {
151159
// deprecated Safari method
152-
linkAppliedQuery.addListener(
153-
() => this.captureStylesheet(sheetBaseHref, el, styleId),
160+
linkAppliedQuery.addListener(() =>
161+
this.captureStylesheet(
162+
sheetBaseHref,
163+
el,
164+
styleId,
165+
nowTimestamp(), // the time of the change event
166+
),
154167
);
155168
}
156169
return {
@@ -203,10 +216,15 @@ export default class AssetManager {
203216
rr_type: 'CssText',
204217
cssTexts: [absolutifyURLs(cssText, sheetBaseHref)],
205218
};
206-
this.mutationCb({
207-
url,
208-
payload,
209-
});
219+
this.mutationCb(
220+
{
221+
url,
222+
payload,
223+
},
224+
snapshotTimestamp === true
225+
? this.lastFullSnapshotTimestamp
226+
: snapshotTimestamp,
227+
);
210228
}
211229
})
212230
.catch(this.fetchCatcher(url));
@@ -226,15 +244,25 @@ export default class AssetManager {
226244
if (el.childNodes.length > 1) {
227245
payload.cssTexts = splitCssText(cssText, el as HTMLStyleElement);
228246
}
229-
this.mutationCb({
230-
url,
231-
payload,
232-
});
247+
this.mutationCb(
248+
{
249+
url,
250+
payload,
251+
},
252+
snapshotTimestamp === true
253+
? this.lastFullSnapshotTimestamp
254+
: snapshotTimestamp,
255+
);
233256
} else {
234-
this.mutationCb({
235-
url: sheetBaseHref,
236-
payload,
237-
});
257+
this.mutationCb(
258+
{
259+
url: sheetBaseHref,
260+
payload,
261+
},
262+
snapshotTimestamp === true
263+
? this.lastFullSnapshotTimestamp
264+
: snapshotTimestamp,
265+
);
238266
}
239267
if (isProcessingStyleElement(el)) {
240268
delete el.__rrProcessingStylesheet;
@@ -286,25 +314,32 @@ export default class AssetManager {
286314
}
287315
}
288316

289-
public capture(asset: asset): assetStatus | assetStatus[] {
317+
public capture(
318+
asset: asset,
319+
snapshotTimestamp?: number | true,
320+
): assetStatus | assetStatus[] {
290321
if ('sheet' in asset.element) {
291322
return this.captureStylesheet(
292323
asset.value,
293324
asset.element as HTMLStyleElement | HTMLLinkElement,
294325
asset.styleId,
326+
snapshotTimestamp,
295327
);
296328
} else if (asset.attr === 'srcset') {
297329
const statuses: assetStatus[] = [];
298330
getSourcesFromSrcset(asset.value).forEach((url) => {
299-
statuses.push(this.captureUrl(url));
331+
statuses.push(this.captureUrl(url, snapshotTimestamp));
300332
});
301333
return statuses;
302334
} else {
303-
return this.captureUrl(asset.value);
335+
return this.captureUrl(asset.value, snapshotTimestamp);
304336
}
305337
}
306338

307-
private captureUrl(url: string): assetStatus {
339+
private captureUrl(
340+
url: string,
341+
snapshotTimestamp?: number | true,
342+
): assetStatus {
308343
if (this.capturedURLs.has(url)) {
309344
return {
310345
url,
@@ -345,10 +380,15 @@ export default class AssetManager {
345380
this.capturedURLs.add(url);
346381
this.capturingURLs.delete(url);
347382

348-
this.mutationCb({
349-
url,
350-
payload,
351-
});
383+
this.mutationCb(
384+
{
385+
url,
386+
payload,
387+
},
388+
snapshotTimestamp === true
389+
? this.lastFullSnapshotTimestamp
390+
: snapshotTimestamp,
391+
);
352392
}
353393
}
354394
})

packages/types/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ export type event = eventWithoutTime;
255255

256256
export type eventWithTime = eventWithoutTime & {
257257
timestamp: number;
258-
delay?: number;
258+
delay?: number; // added during replay
259259
};
260260

261261
export type canvasEventWithTime = eventWithTime & {
@@ -732,6 +732,7 @@ export type assetParam =
732732
| {
733733
url: string;
734734
payload: SerializedCanvasArg | SerializedCssTextArg;
735+
timestamp?: number;
735736
}
736737
| {
737738
url: string;
@@ -741,7 +742,7 @@ export type assetParam =
741742
};
742743
};
743744

744-
export type assetCallback = (d: assetParam) => void;
745+
export type assetCallback = (d: assetParam, timestamp?: number | true) => void;
745746

746747
/**
747748
* @deprecated

0 commit comments

Comments
 (0)