Skip to content

Commit 0d20348

Browse files
committed
feat(canvas): Add "maxCanvasSize" option for canvas (#174)
This adds a configuration option to set a max size for canvas snapshots. This will scale down canvas snapshot images if they are larger than the configure size limit. The size configuration is width * height.
1 parent 0da7d14 commit 0d20348

File tree

11 files changed

+393
-6
lines changed

11 files changed

+393
-6
lines changed

guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ The parameter of `rrweb.record` accepts the following options.
156156
| maskAttributeFn | - | callback before transforming attribute. can be used to mask specific attributes |
157157
| maskInputFn | - | customize mask input content recording logic |
158158
| maskTextFn | - | customize mask text content recording logic |
159+
| maxCanvasSize | null | Configure maximum size ([width, height]) for canvas snapshots |
159160
| slimDOMOptions | {} | remove unnecessary parts of the DOM <br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
160161
| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data |
161162
| inlineStylesheet | true | whether to inline the stylesheet in the events |
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
testMatch: ['**/**.test.ts'],
5+
};

packages/rrweb-worker/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"typings": "tsc -p tsconfig.types.json",
1414
"prepare": "npm run typings && npm run bundle",
1515
"check-types": "tsc -noEmit",
16+
"test": "yarn jest",
1617
"lint": "yarn eslint src"
1718
},
1819
"repository": {
@@ -28,6 +29,12 @@
2829
"@sentry-internal/rrweb-snapshot": "2.12.0",
2930
"@sentry-internal/rrweb-types": "2.12.0"
3031
},
32+
"devDependencies": {
33+
"@types/jest": "^29.5.0",
34+
"jest": "^29.6.0",
35+
"ts-jest": "^29.1.1",
36+
"ts-node": "^10.9.1"
37+
},
3138
"engines": {
3239
"node": ">=12"
3340
}

packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type {
55
ImageBitmapDataURLWorkerResponse,
66
} from '@sentry-internal/rrweb-types';
77

8+
import { getScaledDimensions } from './getScaledDimensions';
9+
810
const lastBlobMap: Map<number, string> = new Map();
911
const transparentBlobMap: Map<string, string> = new Map();
1012

@@ -49,23 +51,40 @@ const worker: ImageBitmapDataURLResponseWorker = self;
4951
// eslint-disable-next-line @typescript-eslint/no-misused-promises
5052
worker.onmessage = async function (e) {
5153
if ('OffscreenCanvas' in globalThis) {
52-
const { id, bitmap, width, height, dataURLOptions } = e.data;
54+
const { id, bitmap, width, height, maxCanvasSize, dataURLOptions } = e.data;
5355

5456
const transparentBase64 = getTransparentBlobFor(
5557
width,
5658
height,
5759
dataURLOptions,
5860
);
5961

60-
const offscreen = new OffscreenCanvas(width, height);
61-
const ctx = offscreen.getContext('2d')!;
62+
const [targetWidth, targetHeight] = getScaledDimensions(
63+
width,
64+
height,
65+
maxCanvasSize,
66+
);
67+
const offscreen = new OffscreenCanvas(targetWidth, targetHeight);
68+
const ctx = offscreen.getContext('bitmaprenderer')!;
69+
const resizedBitmap =
70+
targetWidth === width && targetHeight === height
71+
? bitmap
72+
: // resize bitmap to fit within maxsize
73+
// ~95% browser support https://caniuse.com/mdn-api_createimagebitmap_options_resizewidth_parameter
74+
await createImageBitmap(bitmap, {
75+
resizeWidth: targetWidth,
76+
resizeHeight: targetHeight,
77+
resizeQuality: 'low',
78+
});
6279

63-
ctx.drawImage(bitmap, 0, 0);
80+
ctx.transferFromImageBitmap(resizedBitmap);
6481
bitmap.close();
82+
6583
const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while
6684
const type = blob.type;
6785
const arrayBuffer = await blob.arrayBuffer();
6886
const base64 = encode(arrayBuffer); // cpu intensive
87+
resizedBitmap.close();
6988

7089
// on first try we should check if canvas is transparent,
7190
// no need to save it's contents in that case
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import { getScaledDimensions } from './getScaledDimensions';
2+
3+
describe('getScaledDimensions', () => {
4+
describe.each([
5+
[
6+
600,
7+
400,
8+
[
9+
[
10+
[1200, 800],
11+
[600, 400],
12+
],
13+
[
14+
[1200, 1200],
15+
[400, 400],
16+
],
17+
[
18+
[1200, 1600],
19+
[300, 400],
20+
],
21+
22+
[
23+
[650, 450],
24+
[577, 400],
25+
],
26+
[
27+
[600, 450],
28+
[533, 400],
29+
],
30+
[
31+
[550, 450],
32+
[488, 400],
33+
],
34+
35+
[
36+
[650, 400],
37+
[600, 369],
38+
],
39+
[
40+
[600, 400],
41+
[600, 400],
42+
],
43+
[
44+
[550, 400],
45+
[550, 400],
46+
],
47+
48+
[
49+
[650, 350],
50+
[600, 323],
51+
],
52+
[
53+
[600, 350],
54+
[600, 350],
55+
],
56+
[
57+
[550, 350],
58+
[550, 350],
59+
],
60+
61+
[
62+
[450, 650],
63+
[276, 400],
64+
],
65+
[
66+
[450, 600],
67+
[300, 400],
68+
],
69+
[
70+
[450, 550],
71+
[327, 400],
72+
],
73+
74+
[
75+
[400, 650],
76+
[246, 400],
77+
],
78+
[
79+
[400, 600],
80+
[266, 400],
81+
],
82+
[
83+
[400, 550],
84+
[290, 400],
85+
],
86+
87+
[
88+
[350, 650],
89+
[215, 400],
90+
],
91+
[
92+
[350, 600],
93+
[233, 400],
94+
],
95+
[
96+
[350, 550],
97+
[254, 400],
98+
],
99+
],
100+
],
101+
102+
[
103+
400,
104+
600,
105+
[
106+
[
107+
[1200, 800],
108+
[400, 266],
109+
],
110+
[
111+
[1200, 1200],
112+
[400, 400],
113+
],
114+
[
115+
[1200, 1600],
116+
[400, 533],
117+
],
118+
119+
[
120+
[650, 450],
121+
[400, 276],
122+
],
123+
[
124+
[600, 450],
125+
[400, 300],
126+
],
127+
[
128+
[550, 450],
129+
[400, 327],
130+
],
131+
132+
[
133+
[650, 400],
134+
[400, 246],
135+
],
136+
[
137+
[600, 400],
138+
[400, 266],
139+
],
140+
[
141+
[550, 400],
142+
[400, 290],
143+
],
144+
145+
[
146+
[650, 350],
147+
[400, 215],
148+
],
149+
[
150+
[600, 350],
151+
[400, 233],
152+
],
153+
[
154+
[550, 350],
155+
[400, 254],
156+
],
157+
158+
[
159+
[450, 650],
160+
[400, 577],
161+
],
162+
[
163+
[450, 600],
164+
[400, 533],
165+
],
166+
[
167+
[450, 550],
168+
[400, 488],
169+
],
170+
171+
[
172+
[400, 650],
173+
[369, 600],
174+
],
175+
[
176+
[400, 600],
177+
[400, 600],
178+
],
179+
[
180+
[400, 550],
181+
[400, 550],
182+
],
183+
184+
[
185+
[350, 650],
186+
[323, 600],
187+
],
188+
[
189+
[350, 600],
190+
[350, 600],
191+
],
192+
[
193+
[350, 550],
194+
[350, 550],
195+
],
196+
],
197+
],
198+
199+
[
200+
600,
201+
600,
202+
[
203+
[
204+
[1200, 800],
205+
[600, 400],
206+
],
207+
[
208+
[1200, 1200],
209+
[600, 600],
210+
],
211+
[
212+
[1200, 1600],
213+
[450, 600],
214+
],
215+
216+
[
217+
[650, 450],
218+
[600, 415],
219+
],
220+
[
221+
[600, 450],
222+
[600, 450],
223+
],
224+
[
225+
[550, 450],
226+
[550, 450],
227+
],
228+
229+
[
230+
[650, 400],
231+
[600, 369],
232+
],
233+
[
234+
[600, 400],
235+
[600, 400],
236+
],
237+
[
238+
[550, 400],
239+
[550, 400],
240+
],
241+
242+
[
243+
[650, 350],
244+
[600, 323],
245+
],
246+
[
247+
[600, 350],
248+
[600, 350],
249+
],
250+
[
251+
[550, 350],
252+
[550, 350],
253+
],
254+
255+
[
256+
[450, 650],
257+
[415, 600],
258+
],
259+
[
260+
[450, 600],
261+
[450, 600],
262+
],
263+
[
264+
[450, 550],
265+
[450, 550],
266+
],
267+
268+
[
269+
[400, 650],
270+
[369, 600],
271+
],
272+
[
273+
[400, 600],
274+
[400, 600],
275+
],
276+
[
277+
[400, 550],
278+
[400, 550],
279+
],
280+
281+
[
282+
[350, 650],
283+
[323, 600],
284+
],
285+
[
286+
[350, 600],
287+
[350, 600],
288+
],
289+
[
290+
[350, 550],
291+
[350, 550],
292+
],
293+
],
294+
],
295+
])('maxWidth: %s, maxHeight: %s', (width, height, cases) => {
296+
it.each(cases)('%s scales down to %s', (value, expected) => {
297+
expect(getScaledDimensions(value[0], value[1], [width, height])).toEqual(
298+
expected,
299+
);
300+
});
301+
});
302+
});

0 commit comments

Comments
 (0)