8000 feat(replays): Add snapshot function to replay canvas integration (#1… · GingerAdonis/sentry-javascript@583d720 · GitHub
[go: up one dir, main page]

Skip to content

Commit 583d720

Browse files
c298leemydeabillyvg
authored
feat(replays): Add snapshot function to replay canvas integration (getsentry#10066)
Adds a snapshot function that allows the user to manually snapshot canvas for gl/3d contexts. Manual snapshot will take a snapshot of the canvas element passed in or all canvas in the window if nothing is passed in. To manually snapshot: 1. Add ReplayCanvas to your Sentry integrations: `new Sentry.ReplayCanvas({ enableManualSnapshot:true })` 2. Add snapshot function, eg. `Sentry.getClient().getIntegrationByName('ReplayCanvas').snapshot();` into your render/repaint Closes https://github.com/getsentry/team-replay/issues/308 --------- Co-authored-by: Francesco Novy <francesco.novy@sentry.io> Co-authored-by: Billy Vong <billyvg@users.noreply.github.com>
1 parent 0477453 commit 583d720

File tree

10 files changed

+149
-8
lines changed

10 files changed

+149
-8
lines changed

dev-packages/browser-integration-tests/package.json

Lines chang 8000 ed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"dependencies": {
4646
"@babel/preset-typescript": "^7.16.7",
4747
"@playwright/test": "^1.31.1",
48-
"@sentry-internal/rrweb": "2.8.0",
48+
"@sentry-internal/rrweb": "2.9.0",
4949
"@sentry/browser": "7.93.0",
5050
"@sentry/tracing": "7.93.0",
5151
"axios": "1.6.0",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = new Sentry.Replay({
5+
flushMinDelay: 50,
6+
flushMaxDelay: 50,
7+
minReplayDuration: 0,
8+
});
9+
10+
Sentry.init({
11+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
12+
sampleRate: 0,
13+
replaysSessionSampleRate: 1.0,
14+
replaysOnErrorSampleRate: 0.0,
15+
debug: true,
16+
17+
integrations: [window.Replay, new Sentry.ReplayCanvas({ enableManualSnapshot: true })],
18+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<canvas id="canvas" width="150" height="150"></canvas>
8+
<button id="draw">Draw</button>
9+
</body>
10+
11+
12+
<script>
13+
function draw() {
14+
const canvas = document.getElementById("canvas");
15+
if (canvas.getContext) {
16+
console.log('has canvas')
17+
const ctx = canvas.getContext("2d");
18+
19+
ctx.fillRect(25, 25, 100, 100);
20+
ctx.clearRect(45, 45, 60, 60);
21+
ctx.strokeRect(50, 50, 50, 50);
22+
}
23+
}
24+
document.getElementById('draw').addEventListener('click', draw);
25+
</script>
26+
</html>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
5+
6+
sentryTest('can manually snapshot canvas', async ({ getLocalTestUrl, page, browserName }) => {
7+
if (shouldSkipReplayTest() || browserName === 'webkit' || (process.env.PW_BUNDLE || '').startsWith('bundle')) {
8+
sentryTest.skip();
9+
}
10+
11+
const reqPromise0 = waitForReplayRequest(page, 0);
12+
const reqPromise1 = waitForReplayRequest(page, 1);
13+
const reqPromise2 = waitForReplayRequest(page, 2);
14+
const reqPromise3 = waitForReplayRequest(page, 3);
15+
16+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
17+
return route.fulfill({
18+
status: 200,
19+
contentType: 'application/json',
20+
body: JSON.stringify({ id: 'test-id' }),
21+
});
22+
});
23+
24+
const url = await getLocalTestUrl({ testDir: __dirname });
25+
26+
await page.goto(url);
27+
await reqPromise0;
28+
await Promise.all([page.click('#draw'), reqPromise1]);
29+
30+
const { incrementalSnapshots } = getReplayRecordingContent(await reqPromise2);
31+
expect(incrementalSnapshots).toEqual([]);
32+
33+
await page.evaluate(() => {
34+
(window as any).Sentry.getClient().getIntegrationById('ReplayCanvas').snapshot();
35+
});
36+
37+
const { incrementalSnapshots: incrementalSnapshotsManual } = getReplayRecordingContent(await reqPromise3);
38+
expect(incrementalSnapshotsManual).toEqual(
39+
expect.arrayContaining([
40+
{
41+
data: {
42+
commands: [
43+
{
44+
args: [0, 0, 150, 150],
45+
property: 'clearRect',
46+
},
47+
{
48+
args: [
49+
{
50+
args: [
51+
{
52+
data: [
53+
{
54+
base64: expect.any(String),
55+
rr_type: 'ArrayBuffer',
56+
},
57+
],
58+
rr_type: 'Blob',
59+
type: 'image/webp',
60+
},
61+
],
62+
rr_type: 'ImageBitmap',
63+
},
64+
0,
65+
0,
66+
],
67+
property: 'drawImage',
68+
},
69+
],
70+
id: 9,
71+
source: 9,
72+
type: 0,
73+
},
74+
timestamp: 0,
75+
type: 3,
76+
},
77+
]),
78+
);
79+
});

packages/replay-canvas/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"homepage": "https://docs.sentry.io/platforms/javascript/session-replay/",
5454
"devDependencies": {
5555
"@babel/core": "^7.17.5",
56-
"@sentry-internal/rrweb": "2.8.0"
56+
"@sentry-internal/rrweb": "2.9.0"
5757
},
5858
"dependencies": {
5959
"@sentry/core": "7.93.0",

packages/replay-canvas/src/canvas.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import type { CanvasManagerInterface, CanvasManagerOptions } from '@sentry/repla
44
import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
55

66
interface ReplayCanvasOptions {
7+
enableManualSnapshot?: boolean;
78
quality: 'low' | 'medium' | 'high';
89
}
910

1011
type GetCanvasManager = (options: CanvasManagerOptions) => CanvasManagerInterface;
1112
export interface ReplayCanvasIntegrationOptions {
13+
enableManualSnapshot?: boolean;
1214
recordCanvas: true;
1315
getCanvasManager: GetCanvasManager;
1416
sampling: {
@@ -58,21 +60,34 @@ const INTEGRATION_NAME = 'ReplayCanvas';
5860
const replayCanvasIntegration = ((options: Partial<ReplayCanvasOptions> = {}) => {
5961
const _canvasOptions = {
6062
quality: options.quality || 'medium',
63+
enableManualSnapshot: options.enableManualSnapshot,
6164
};
6265

66+
let canvasManagerResolve: (value: CanvasManager) => void;
67+
const _canvasManager: Promise<CanvasManager> = new Promise(resolve => (canvasManagerResolve = resolve));
68+
6369
return {
6470
name: INTEGRATION_NAME,
6571
// eslint-disable-next-line @typescript-eslint/no-empty-function
6672
setupOnce() {},
6773
getOptions(): ReplayCanvasIntegrationOptions {
68-
const { quality } = _canvasOptions;
74+
const { quality, enableManualSnapshot } = _canvasOptions;
6975

7076
return {
77+
enableManualSnapshot,
7178
recordCanvas: true,
72-
getCanvasManager: (options: CanvasManagerOptions) => new CanvasManager(options),
79+
getCanvasManager: (options: CanvasManagerOptions) => {
80+
const manager = new CanvasManager({ ...options, enableManualSnapshot });
81+
canvasManagerResolve(manager);
82+
return manager;
83+
},
7384
...(CANVAS_QUALITY[quality || 'medium'] || CANVAS_QUALITY.medium),
7485
};
7586
},
87+
async snapshot(canvasElement?: HTMLCanvasElement) {
88+
const canvasManager = await _canvasManager;
89+
canvasManager.snapshot(canvasElement);
90+
},
7691
};
7792
}) satisfies IntegrationFn;
7893

packages/replay-canvas/test/canvas.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ it('initializes with default options', () => {
1616
});
1717
});
1818

19-
it('initializes with quality option', () => {
20-
const rc = new ReplayCanvas({ quality: 'low' });
19+
it('initializes with quality option and manual snapshot', () => {
20+
const rc = new ReplayCanvas({ enableManualSnapshot: true, quality: 'low' });
2121

2222
expect(rc.getOptions()).toEqual({
23+
enableManualSnapshot: true,
2324
recordCanvas: true,
2425
getCanvasManager: expect.any(Function),
2526
sampling: {

packages/replay/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@
5454
"devDependencies": {
5555
"@babel/core": "^7.17.5",
5656
"@sentry-internal/replay-worker": "7.93.0",
57-
"@sentry-internal/rrweb": "2.8.0",
58-
"@sentry-internal/rrweb-snapshot": "2.8.0",
57+
"@sentry-internal/rrweb": "2.9.0",
58+
"@sentry-internal/rrweb-snapshot": "2.9.0",
5959
"fflate": "^0.8.1",
6060
"jsdom-worker": "^0.2.1"
6161
},

packages/replay/src/types/replay.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ export interface SlowClickConfig {
538538
}
539539

540540
export interface ReplayCanvasIntegrationOptions {
541+
enableManualSnapshot?: boolean;
541542
recordCanvas: true;
542543
getCanvasManager: (options: CanvasManagerOptions) => CanvasManagerInterface;
543544
sampling: {

packages/replay/src/types/rrweb.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface CanvasManagerInterface {
5656

5757
export interface CanvasManagerOptions {
5858
recordCanvas: boolean;
59+
enableManualSnapshot?: boolean;
5960
blockClass: string | RegExp;
6061
blockSelector: string | null;
6162
unblockSelector: string | null;

0 commit comments

Comments
 (0)
0