Skip to content

Commit 48566f1

Browse files
committed
ensure rrweb records canvas elements that are not ready on start (#102)
- Adds a verbose logger for the canvas manager - Fixes issue with WebGL canvas elements not being recorded if they were not ready (context not ready) when recording started. - Fixes rrweb dev build (`yarn dev`)
1 parent 1d0b214 commit 48566f1

File tree

3 files changed

+63
-16
lines changed

3 files changed

+63
-16
lines changed

packages/rrweb/src/record/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ function record<T = eventWithTime>(
101101
enableStrictPrivacy = false,
102102
ignoreCSSAttributes = new Set([]),
103103
errorHandler,
104+
logger,
104105
} = options;
105106

106107
registerErrorHandler(errorHandler);
@@ -330,6 +331,7 @@ function record<T = eventWithTime>(
330331
resizeQuality: sampling?.canvas?.resizeQuality,
331332
resizeFactor: sampling?.canvas?.resizeFactor,
332333
maxSnapshotDimension: sampling?.canvas?.maxSnapshotDimension,
334+
logger: logger,
333335
});
334336

335337
const shadowDomManager = new ShadowDomManager({

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

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export class CanvasManager {
3232
private pendingCanvasMutations: pendingCanvasMutationsMap = new Map();
3333
private rafStamps: RafStamps = { latestId: 0, invokeId: null };
3434
private mirror: Mirror;
35+
private logger?: {
36+
debug: (...args: Parameters<typeof console.debug>) => void;
37+
warn: (...args: Parameters<typeof console.warn>) => void;
38+
};
3539

3640
private mutationCb: canvasMutationCallback;
3741
private resetObservers?: listenerHandler;
@@ -71,6 +75,10 @@ export class CanvasManager {
7175
resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high';
7276
resizeFactor?: number;
7377
maxSnapshotDimension?: number;
78+
logger?: {
79+
debug: (...args: Parameters<typeof console.debug>) => void;
80+
warn: (...args: Parameters<typeof console.warn>) => void;
81+
};
7482
}) {
7583
const {
7684
sampling = 'all',
@@ -82,6 +90,7 @@ export class CanvasManager {
8290
} = options;
8391
this.mutationCb = options.mutationCb;
8492
this.mirror = options.mirror;
93+
this.logger = options.logger;
8594

8695
if (recordCanvas && sampling === 'all')
8796
this.initCanvasMutationObserver(win, blockClass, blockSelector);
@@ -100,6 +109,18 @@ export class CanvasManager {
100109
);
101110
}
102111

112+
private debug(
113+
canvas?: HTMLCanvasElement,
114+
...args: Parameters<typeof console.log>
115+
) {
116+
if (!this.logger) return;
117+
let prefix = '[highlight-canvas]';
118+
if (canvas) {
119+
prefix += ` [ctx:${(canvas as ICanvas).__context}]`;
120+
}
121+
this.logger.debug(prefix, canvas, ...args);
122+
}
123+
103124
private processMutation: canvasManagerMutationCallback = (
104125
target,
105126
mutation,
@@ -184,6 +205,7 @@ export class CanvasManager {
184205
const matchedCanvas: HTMLCanvasElement[] = [];
185206
win.document.querySelectorAll('canvas').forEach((canvas) => {
186207
if (!isBlocked(canvas, blockClass, blockSelector, true)) {
208+
this.debug(canvas, 'discovered canvas');
187209
matchedCanvas.push(canvas);
188210
}
189211
});
@@ -200,18 +222,27 @@ export class CanvasManager {
200222
}
201223
lastSnapshotTime = timestamp;
202224

203-
getCanvas()
204-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
205-
.forEach(async (canvas: HTMLCanvasElement) => {
206-
const id = this.mirror.getId(canvas);
207-
if (snapshotInProgressMap.get(id)) return;
225+
getCanvas().forEach(async (canvas: HTMLCanvasElement) => {
226+
this.debug(canvas, 'starting snapshotting');
227+
const id = this.mirror.getId(canvas);
228+
if (snapshotInProgressMap.get(id)) {
229+
this.debug(canvas, 'snapshotting already in progress for', id);
230+
return;
231+
}
208232

209-
// The browser throws if the canvas is 0 in size
210-
// Uncaught (in promise) DOMException: Failed to execute 'createImageBitmap' on 'Window': The source image width is 0.
211-
// Assuming the same happens with height
212-
if (canvas.width === 0 || canvas.height === 0) return;
233+
// The browser throws if the canvas is 0 in size
234+
// Uncaught (in promise) DOMException: Failed to execute 'createImageBitmap' on 'Window': The source image width is 0.
235+
// Assuming the same happens with height
236+
if (canvas.width === 0 || canvas.height === 0) {
237+
this.debug(canvas, 'not yet ready', {
238+
width: canvas.width,
239+
height: canvas.height,
240+
});
241+
return;
242+
}
213243

214-
snapshotInProgressMap.set(id, true);
244+
snapshotInProgressMap.set(id, true);
245+
try {
215246
if (['webgl', 'webgl2'].includes((canvas as ICanvas).__context)) {
216247
// if the canvas hasn't been modified recently,
217248
// its contents won't be in memory and `createImageBitmap`
@@ -234,11 +265,6 @@ export class CanvasManager {
234265
context.clear(context.COLOR_BUFFER_BIT);
235266
}
236267
}
237-
// canvas is not yet ready... this retry on the next sampling iteration.
238-
// we don't want to crash the worker if the canvas is not yet rendered.
239-
if (canvas.width === 0 || canvas.height === 0) {
240-
return;
241-
}
242268
let scale = resizeFactor || 1;
243269
if (maxSnapshotDimension) {
244270
const maxDim = Math.max(canvas.width, canvas.height);
@@ -247,11 +273,18 @@ export class CanvasManager {
247273
const width = canvas.width * scale;
248274
const height = canvas.height * scale;
249275

276+
window.performance.mark(`canvas-${canvas.id}-snapshot`);
250277
const bitmap = await createImageBitmap(canvas, {
251278
resizeQuality: resizeQuality || 'low',
252279
resizeWidth: width,
253280
resizeHeight: height,
254281
});
282+
this.debug(
283+
canvas,
284+
'took a snapshot in',
285+
window.performance.measure(`canvas-snapshot`),
286+
);
287+
window.performance.mark(`canvas-postMessage`);
255288
worker.postMessage(
256289
{
257290
id,
@@ -264,7 +297,15 @@ export class CanvasManager {
264297
},
265298
[bitmap],
266299
);
267-
});
300+
this.debug(
301+
canvas,
302+
'send message in',
303+
window.performance.measure(`canvas-postMessage`),
304+
);
305+
} finally {
306+
snapshotInProgressMap.set(id, false);
307+
}
308+
});
268309
rafId = requestAnimationFrame(takeCanvasSnapshots);
269310
};
270311

packages/rrweb/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export type recordOptions<T> = {
7979
* Text will be randomized. Instead of seeing "Hello World" in a recording, you will see "1fds1 j59a0".
8080
*/
8181
enableStrictPrivacy?: boolean;
82+
logger?: {
83+
debug: (...args: Parameters<typeof console.debug>) => void;
84+
warn: (...args: Parameters<typeof console.warn>) => void;
85+
};
8286
};
8387

8488
export type observerParam = {

0 commit comments

Comments
 (0)