Skip to content

Commit

Permalink
feat(replays): Add snapshot function to replay canvas integration (#1…
Browse files Browse the repository at this point in the history
…0066)

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 getsentry/team-replay#308

---------

Co-authored-by: Francesco Novy <francesco.novy@sentry.io>
Co-authored-by: Billy Vong <billyvg@users.noreply.github.com>
  • Loading branch information
3 people committed Jan 17, 2024
1 parent 0477453 commit 583d720
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 8 deletions.
2 changes: 1 addition & 1 deletion dev-packages/browser-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@playwright/test": "^1.31.1",
"@sentry-internal/rrweb": "2.8.0",
"@sentry-internal/rrweb": "2.9.0",
"@sentry/browser": "7.93.0",
"@sentry/tracing": "7.93.0",
"axios": "1.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window.Replay = new Sentry.Replay({
flushMinDelay: 50,
flushMaxDelay: 50,
minReplayDuration: 0,
});

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 0,
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 0.0,
debug: true,

integrations: [window.Replay, new Sentry.ReplayCanvas({ enableManualSnapshot: true })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<canvas id="canvas" width="150" height="150"></canvas>
<button id="draw">Draw</button>
</body>


<script>
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
console.log('has canvas')
const ctx = canvas.getContext("2d");

ctx.fillRect(25, 25, 100, 100);
ctx.clearRect(45, 45, 60, 60);
ctx.strokeRect(50, 50, 50, 50);
}
}
document.getElementById('draw').addEventListener('click', draw);
</script>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';

sentryTest('can manually snapshot canvas', async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipReplayTest() || browserName === 'webkit' || (process.env.PW_BUNDLE || '').startsWith('bundle')) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);
const reqPromise1 = waitForReplayRequest(page, 1);
const reqPromise2 = waitForReplayRequest(page, 2);
const reqPromise3 = waitForReplayRequest(page, 3);

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'test-id' }),
});
});

const url = await getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;
await Promise.all([page.click('#draw'), reqPromise1]);

const { incrementalSnapshots } = getReplayRecordingContent(await reqPromise2);
expect(incrementalSnapshots).toEqual([]);

await page.evaluate(() => {
(window as any).Sentry.getClient().getIntegrationById('ReplayCanvas').snapshot();
});

const { incrementalSnapshots: incrementalSnapshotsManual } = getReplayRecordingContent(await reqPromise3);
expect(incrementalSnapshotsManual).toEqual(
expect.arrayContaining([
{
data: {
commands: [
{
args: [0, 0, 150, 150],
property: 'clearRect',
},
{
args: [
{
args: [
{
data: [
{
base64: expect.any(String),
rr_type: 'ArrayBuffer',
},
],
rr_type: 'Blob',
type: 'image/webp',
},
],
rr_type: 'ImageBitmap',
},
0,
0,
],
property: 'drawImage',
},
],
id: 9,
source: 9,
type: 0,
},
timestamp: 0,
type: 3,
},
]),
);
});
2 changes: 1 addition & 1 deletion packages/replay-canvas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"homepage": "https://docs.sentry.io/platforms/javascript/session-replay/",
"devDependencies": {
"@babel/core": "^7.17.5",
"@sentry-internal/rrweb": "2.8.0"
"@sentry-internal/rrweb": "2.9.0"
},
"dependencies": {
"@sentry/core": "7.93.0",
Expand Down
19 changes: 17 additions & 2 deletions packages/replay-canvas/src/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import type { CanvasManagerInterface, CanvasManagerOptions } from '@sentry/repla
import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types';

interface ReplayCanvasOptions {
enableManualSnapshot?: boolean;
quality: 'low' | 'medium' | 'high';
}

type GetCanvasManager = (options: CanvasManagerOptions) => CanvasManagerInterface;
export interface ReplayCanvasIntegrationOptions {
enableManualSnapshot?: boolean;
recordCanvas: true;
getCanvasManager: GetCanvasManager;
sampling: {
Expand Down Expand Up @@ -58,21 +60,34 @@ const INTEGRATION_NAME = 'ReplayCanvas';
const replayCanvasIntegration = ((options: Partial<ReplayCanvasOptions> = {}) => {
const _canvasOptions = {
quality: options.quality || 'medium',
enableManualSnapshot: options.enableManualSnapshot,
};

let canvasManagerResolve: (value: CanvasManager) => void;
const _canvasManager: Promise<CanvasManager> = new Promise(resolve => (canvasManagerResolve = resolve));

return {
name: INTEGRATION_NAME,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setupOnce() {},
getOptions(): ReplayCanvasIntegrationOptions {
const { quality } = _canvasOptions;
const { quality, enableManualSnapshot } = _canvasOptions;

return {
enableManualSnapshot,
recordCanvas: true,
getCanvasManager: (options: CanvasManagerOptions) => new CanvasManager(options),
getCanvasManager: (options: CanvasManagerOptions) => {
const manager = new CanvasManager({ ...options, enableManualSnapshot });
canvasManagerResolve(manager);
return manager;
},
...(CANVAS_QUALITY[quality || 'medium'] || CANVAS_QUALITY.medium),
};
},
async snapshot(canvasElement?: HTMLCanvasElement) {
const canvasManager = await _canvasManager;
canvasManager.snapshot(canvasElement);
},
};
}) satisfies IntegrationFn;

Expand Down
5 changes: 3 additions & 2 deletions packages/replay-canvas/test/canvas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ it('initializes with default options', () => {
});
});

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

expect(rc.getOptions()).toEqual({
enableManualSnapshot: true,
recordCanvas: true,
getCanvasManager: expect.any(Function),
sampling: {
Expand Down
4 changes: 2 additions & 2 deletions packages/replay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"devDependencies": {
"@babel/core": "^7.17.5",
"@sentry-internal/replay-worker": "7.93.0",
"@sentry-internal/rrweb": "2.8.0",
"@sentry-internal/rrweb-snapshot": "2.8.0",
"@sentry-internal/rrweb": "2.9.0",
"@sentry-internal/rrweb-snapshot": "2.9.0",
"fflate": "^0.8.1",
"jsdom-worker": "^0.2.1"
},
Expand Down
1 change: 1 addition & 0 deletions packages/replay/src/types/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ export interface SlowClickConfig {
}

export interface ReplayCanvasIntegrationOptions {
enableManualSnapshot?: boolean;
recordCanvas: true;
getCanvasManager: (options: CanvasManagerOptions) => CanvasManagerInterface;
sampling: {
Expand Down
1 change: 1 addition & 0 deletions packages/replay/src/types/rrweb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface CanvasManagerInterface {

export interface CanvasManagerOptions {
recordCanvas: boolean;
enableManualSnapshot?: boolean;
blockClass: string | RegExp;
blockSelector: string | null;
unblockSelector: string | null;
Expand Down

0 comments on commit 583d720

Please sign in to comment.