diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 8fb2ce61b0ea..e090e9e788bf 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -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", diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js new file mode 100644 index 000000000000..244f951588c6 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js @@ -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 })], +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/template.html b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/template.html new file mode 100644 index 000000000000..5f23d569fcc2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/template.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts new file mode 100644 index 000000000000..583ff672e590 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts @@ -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, + }, + ]), + ); +}); diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 81a70f18c20d..0a495945f2d1 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -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", diff --git a/packages/replay-canvas/src/canvas.ts b/packages/replay-canvas/src/canvas.ts index 90b65e5ccd35..ba3ec85ebcfb 100644 --- a/packages/replay-canvas/src/canvas.ts +++ b/packages/replay-canvas/src/canvas.ts @@ -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: { @@ -58,21 +60,34 @@ const INTEGRATION_NAME = 'ReplayCanvas'; const replayCanvasIntegration = ((options: Partial = {}) => { const _canvasOptions = { quality: options.quality || 'medium', + enableManualSnapshot: options.enableManualSnapshot, }; + let canvasManagerResolve: (value: CanvasManager) => void; + const _canvasManager: Promise = 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; diff --git a/packages/replay-canvas/test/canvas.test.ts b/packages/replay-canvas/test/canvas.test.ts index e5464ba544ec..346df7b4a89d 100644 --- a/packages/replay-canvas/test/canvas.test.ts +++ b/packages/replay-canvas/test/canvas.test.ts @@ -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: { diff --git a/packages/replay/package.json b/packages/replay/package.json index 936b861a7f17..0b44a611a270 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -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" }, diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index 93fd60a868f3..61325e4a9959 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -538,6 +538,7 @@ export interface SlowClickConfig { } export interface ReplayCanvasIntegrationOptions { + enableManualSnapshot?: boolean; recordCanvas: true; getCanvasManager: (options: CanvasManagerOptions) => CanvasManagerInterface; sampling: { diff --git a/packages/replay/src/types/rrweb.ts b/packages/replay/src/types/rrweb.ts index 7cbea7af07eb..a490a6e46c1b 100644 --- a/packages/replay/src/types/rrweb.ts +++ b/packages/replay/src/types/rrweb.ts @@ -56,6 +56,7 @@ export interface CanvasManagerInterface { export interface CanvasManagerOptions { recordCanvas: boolean; + enableManualSnapshot?: boolean; blockClass: string | RegExp; blockSelector: string | null; unblockSelector: string | null;