diff --git a/.craft.yml b/.craft.yml
index 1a4b07c18874..7c09cb4ddd4c 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -32,6 +32,10 @@ targets:
- name: npm
id: '@sentry-internal/feedback'
includeNames: /^sentry-internal-feedback-\d.*\.tgz$/
+ ## 1.8 ReplayCanvas package (browser only)
+ - name: npm
+ id: '@sentry-internal/replay-canvas'
+ includeNames: /^sentry-internal-replay-canvas-\d.*\.tgz$/
## 2. Browser & Node SDKs
- name: npm
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 83fb3f0be7b1..e590f8e8b066 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -100,6 +100,7 @@ jobs:
- *shared
- 'packages/browser/**'
- 'packages/replay/**'
+ - 'packages/replay-canvas/**'
- 'packages/feedback/**'
browser_integration:
- *shared
@@ -371,6 +372,7 @@ jobs:
${{ github.workspace }}/packages/browser/build/bundles/**
${{ github.workspace }}/packages/integrations/build/bundles/**
${{ github.workspace }}/packages/replay/build/bundles/**
+ ${{ github.workspace }}/packages/replay-canvas/build/bundles/**
${{ github.workspace }}/packages/**/*.tgz
${{ github.workspace }}/packages/serverless/build/aws/dist-serverless/*.zip
diff --git a/.size-limit.js b/.size-limit.js
index 2ddf44088be9..606014bfec0e 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -14,6 +14,13 @@ module.exports = [
gzip: true,
limit: '75 KB',
},
+ {
+ name: '@sentry/browser (incl. Tracing, Replay with Canvas) - Webpack (gzipped)',
+ path: 'packages/browser/build/npm/esm/index.js',
+ import: '{ init, Replay, BrowserTracing, ReplayCanvas }',
+ gzip: true,
+ limit: '90 KB',
+ },
{
name: '@sentry/browser (incl. Tracing, Replay) - Webpack with treeshaking flags (gzipped)',
path: 'packages/browser/build/npm/esm/index.js',
diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json
index 2d7a2629af15..8fb2ce61b0ea 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.7.3",
+ "@sentry-internal/rrweb": "2.8.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/records/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js
index e530d741a8bc..296245b01c26 100644
--- a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js
+++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js
@@ -1,4 +1,3 @@
-import { getCanvasManager } from '@sentry-internal/rrweb';
import * as Sentry from '@sentry/browser';
window.Sentry = Sentry;
@@ -6,11 +5,6 @@ window.Replay = new Sentry.Replay({
flushMinDelay: 50,
flushMaxDelay: 50,
minReplayDuration: 0,
- _experiments: {
- canvas: {
- manager: getCanvasManager,
- },
- },
});
Sentry.init({
@@ -20,5 +14,5 @@ Sentry.init({
replaysOnErrorSampleRate: 0.0,
debug: true,
- integrations: [window.Replay],
+ integrations: [window.Replay, new Sentry.ReplayCanvas()],
});
diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts
index 372ca8978356..0f9580fccc52 100644
--- a/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts
+++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts
@@ -4,7 +4,7 @@ import { sentryTest } from '../../../../utils/fixtures';
import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
sentryTest('can record canvas', async ({ getLocalTestUrl, page, browserName }) => {
- if (shouldSkipReplayTest() || browserName === 'webkit') {
+ if (shouldSkipReplayTest() || browserName === 'webkit' || (process.env.PW_BUNDLE || '').startsWith('bundle')) {
sentryTest.skip();
}
@@ -24,6 +24,16 @@ sentryTest('can record canvas', async ({ getLocalTestUrl, page, browserName }) =
await page.goto(url);
await reqPromise0;
+ const content0 = getReplayRecordingContent(await reqPromise0);
+ expect(content0.optionsEvents).toEqual([
+ {
+ tag: 'options',
+ payload: expect.objectContaining({
+ shouldRecordCanvas: true,
+ }),
+ },
+ ]);
+
await Promise.all([page.click('#draw'), reqPromise1]);
const { incrementalSnapshots } = getReplayRecordingContent(await reqPromise2);
diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js
new file mode 100644
index 000000000000..fa3248066150
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window.Replay = new Sentry.Replay({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ minReplayDuration: 0,
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 0,
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+ debug: true,
+
+ integrations: [new Sentry.ReplayCanvas(), window.Replay],
+});
diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/test.ts
new file mode 100644
index 000000000000..104098eed2cf
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/test.ts
@@ -0,0 +1,31 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
+
+sentryTest('sets up canvas when adding ReplayCanvas integration first', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipReplayTest() || (process.env.PW_BUNDLE || '').startsWith('bundle')) {
+ sentryTest.skip();
+ }
+
+ const reqPromise0 = waitForReplayRequest(page, 0);
+
+ 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;
+
+ const replay = await getReplaySnapshot(page);
+ const canvasOptions = replay._canvas;
+ expect(canvasOptions?.sampling.canvas).toBe(2);
+ expect(canvasOptions?.dataURLOptions.quality).toBe(0.4);
+ expect(replay._hasCanvas).toBe(true);
+});
diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js
new file mode 100644
index 000000000000..1a9d5d179c4a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window.Replay = new Sentry.Replay({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ 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()],
+});
diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/test.ts
new file mode 100644
index 000000000000..d25066dc065b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/test.ts
@@ -0,0 +1,31 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
+
+sentryTest('sets up canvas when adding ReplayCanvas integration after Replay', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipReplayTest() || (process.env.PW_BUNDLE || '').startsWith('bundle')) {
+ sentryTest.skip();
+ }
+
+ const reqPromise0 = waitForReplayRequest(page, 0);
+
+ 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;
+
+ const replay = await getReplaySnapshot(page);
+ const canvasOptions = replay._canvas;
+ expect(canvasOptions?.sampling.canvas).toBe(2);
+ expect(canvasOptions?.dataURLOptions.quality).toBe(0.4);
+ expect(replay._hasCanvas).toBe(true);
+});
diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js
new file mode 100644
index 000000000000..92a463a4bc84
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window.Replay = new Sentry.Replay({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ 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],
+});
diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/test.ts
new file mode 100644
index 000000000000..af3ec4a4bc97
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/test.ts
@@ -0,0 +1,27 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getReplaySnapshot, shouldSkipReplayTest } from '../../../../utils/replayHelpers';
+
+sentryTest('does not setup up canvas without ReplayCanvas integration', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipReplayTest()) {
+ sentryTest.skip();
+ }
+
+ 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);
+
+ const replay = await getReplaySnapshot(page);
+ const canvasOptions = replay._canvas;
+ expect(canvasOptions).toBe(undefined);
+ expect(replay._hasCanvas).toBe(false);
+});
diff --git a/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts b/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts
index c4bcbcc2c792..78fbca08d4cd 100644
--- a/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts
+++ b/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts
@@ -227,6 +227,7 @@ sentryTest(
networkCaptureBodies: true,
networkRequestHasHeaders: true,
networkResponseHasHeaders: true,
+ shouldRecordCanvas: false,
},
},
]);
diff --git a/dev-packages/browser-integration-tests/utils/replayHelpers.ts b/dev-packages/browser-integration-tests/utils/replayHelpers.ts
index 2a951e4a215e..295ae82e0fa3 100644
--- a/dev-packages/browser-integration-tests/utils/replayHelpers.ts
+++ b/dev-packages/browser-integration-tests/utils/replayHelpers.ts
@@ -1,4 +1,5 @@
/* eslint-disable max-lines */
+import type { ReplayCanvasIntegrationOptions } from '@sentry-internal/replay-canvas';
import type { fullSnapshotEvent, incrementalSnapshotEvent } from '@sentry-internal/rrweb';
import { EventType } from '@sentry-internal/rrweb';
import type { ReplayEventWithTime } from '@sentry/browser';
@@ -174,12 +175,17 @@ export function getReplaySnapshot(page: Page): Promise<{
_isEnabled: boolean;
_context: InternalEventContext;
_options: ReplayPluginOptions;
+ _canvas: ReplayCanvasIntegrationOptions | undefined;
_hasCanvas: boolean;
session: Session | undefined;
recordingMode: ReplayRecordingMode;
}> {
return page.evaluate(() => {
- const replayIntegration = (window as unknown as Window & { Replay: { _replay: ReplayContainer } }).Replay;
+ const replayIntegration = (
+ window as unknown as Window & {
+ Replay: { _replay: ReplayContainer & { _canvas: ReplayCanvasIntegrationOptions | undefined } };
+ }
+ ).Replay;
const replay = replayIntegration._replay;
const replaySnapshot = {
@@ -187,8 +193,9 @@ export function getReplaySnapshot(page: Page): Promise<{
_isEnabled: replay.isEnabled(),
_context: replay.getContext(),
_options: replay.getOptions(),
+ _canvas: replay['_canvas'],
// We cannot pass the function through as this is serialized
- _hasCanvas: typeof replay.getOptions()._experiments.canvas?.manager === 'function',
+ _hasCanvas: typeof replay['_canvas']?.getCanvasManager === 'function',
session: replay.session,
recordingMode: replay.recordingMode,
};
diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/fixtures/ReplayRecordingData.ts
index 3cf31a74ca49..698cb83b5fb4 100644
--- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/fixtures/ReplayRecordingData.ts
+++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/fixtures/ReplayRecordingData.ts
@@ -18,6 +18,7 @@ export const ReplayRecordingData = [
networkRequestHasHeaders: true,
networkResponseHasHeaders: true,
sessionSampleRate: 1,
+ shouldRecordCanvas: false,
useCompression: false,
useCompressionOption: true,
},
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts
index 3cf31a74ca49..698cb83b5fb4 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts
@@ -18,6 +18,7 @@ export const ReplayRecordingData = [
networkRequestHasHeaders: true,
networkResponseHasHeaders: true,
sessionSampleRate: 1,
+ shouldRecordCanvas: false,
useCompression: false,
useCompressionOption: true,
},
diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts
index 3cf31a74ca49..698cb83b5fb4 100644
--- a/dev-packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts
+++ b/dev-packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts
@@ -18,6 +18,7 @@ export const ReplayRecordingData = [
networkRequestHasHeaders: true,
networkResponseHasHeaders: true,
sessionSampleRate: 1,
+ shouldRecordCanvas: false,
useCompression: false,
useCompressionOption: true,
},
diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
index 9aef97dc828f..b0a1c806ef98 100644
--- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
+++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
@@ -124,6 +124,7 @@ export function makeTerserPlugin() {
'_support',
// We want to keep some replay fields unmangled to enable integration tests to access them
'_replay',
+ '_canvas',
// We also can't mangle rrweb private fields when bundling rrweb in the replay CDN bundles
'_cssText',
// We want to keep the _integrations variable unmangled to send all installed integrations from replay
diff --git a/package.json b/package.json
index aa16add34b06..930794c00e91 100644
--- a/package.json
+++ b/package.json
@@ -66,6 +66,7 @@
"packages/react",
"packages/remix",
"packages/replay",
+ "packages/replay-canvas",
"packages/replay-worker",
"packages/serverless",
"packages/svelte",
diff --git a/packages/browser/package.json b/packages/browser/package.json
index 196bc7c8f65c..d367f603d701 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -30,6 +30,7 @@
},
"dependencies": {
"@sentry-internal/feedback": "7.93.0",
+ "@sentry-internal/replay-canvas": "7.93.0",
"@sentry-internal/tracing": "7.93.0",
"@sentry/core": "7.93.0",
"@sentry/replay": "7.93.0",
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 97abefea8242..aae8a2f70d89 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -33,6 +33,8 @@ export type {
ReplaySpanFrameEvent,
} from '@sentry/replay';
+export { ReplayCanvas } from '@sentry-internal/replay-canvas';
+
export { Feedback } from '@sentry-internal/feedback';
export {
diff --git a/packages/nextjs/test/integration/package.json b/packages/nextjs/test/integration/package.json
index 0bbe126a10b6..14ac5a38aa6b 100644
--- a/packages/nextjs/test/integration/package.json
+++ b/packages/nextjs/test/integration/package.json
@@ -31,6 +31,7 @@
"@sentry/node": "file:../../../node",
"@sentry/react": "file:../../../react",
"@sentry/replay": "file:../../../replay",
+ "@sentry-internal/replay-canvas": "file:../../../replay-canvas",
"@sentry/tracing": "file:../../../tracing",
"@sentry-internal/tracing": "file:../../../tracing-internal",
"@sentry-internal/feedback": "file:../../../feedback",
diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json
index 04030a492670..232c4fc9ea59 100644
--- a/packages/remix/test/integration/package.json
+++ b/packages/remix/test/integration/package.json
@@ -29,6 +29,7 @@
"@sentry/node": "file:../../../node",
"@sentry/react": "file:../../../react",
"@sentry/replay": "file:../../../replay",
+ "@sentry-internal/replay-canvas": "file:../../../replay-canvas",
"@sentry/tracing": "file:../../../tracing",
"@sentry-internal/tracing": "file:../../../tracing-internal",
"@sentry-internal/feedback": "file:../../../feedback",
diff --git a/packages/replay-canvas/.eslintignore b/packages/replay-canvas/.eslintignore
new file mode 100644
index 000000000000..b38db2f296ff
--- /dev/null
+++ b/packages/replay-canvas/.eslintignore
@@ -0,0 +1,2 @@
+node_modules/
+build/
diff --git a/packages/replay-canvas/.eslintrc.js b/packages/replay-canvas/.eslintrc.js
new file mode 100644
index 000000000000..eaa6b88347ca
--- /dev/null
+++ b/packages/replay-canvas/.eslintrc.js
@@ -0,0 +1,25 @@
+// Note: All paths are relative to the directory in which eslint is being run, rather than the directory where this file
+// lives
+
+// ESLint config docs: https://eslint.org/docs/user-guide/configuring/
+
+module.exports = {
+ extends: ['../../.eslintrc.js'],
+ overrides: [
+ {
+ files: ['src/**/*.ts'],
+ rules: {
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
+ },
+ },
+ {
+ files: ['jest.setup.ts', 'jest.config.ts'],
+ parserOptions: {
+ project: ['tsconfig.test.json'],
+ },
+ rules: {
+ 'no-console': 'off',
+ },
+ },
+ ],
+};
diff --git a/packages/replay-canvas/.gitignore b/packages/replay-canvas/.gitignore
new file mode 100644
index 000000000000..363d3467c6fa
--- /dev/null
+++ b/packages/replay-canvas/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/*.tgz
+.eslintcache
+build
diff --git a/packages/replay-canvas/CONTRIBUTING.md b/packages/replay-canvas/CONTRIBUTING.md
new file mode 100644
index 000000000000..829930e2b05e
--- /dev/null
+++ b/packages/replay-canvas/CONTRIBUTING.md
@@ -0,0 +1,4 @@
+## Updating the rrweb dependency
+
+When [updating the `rrweb` dependency](https://github.com/getsentry/sentry-javascript/blob/a493aa6a46555b944c8d896a2164bcd8b11caaf5/packages/replay/package.json?plain=1#LL55),
+please be aware that [`@sentry/replay`'s README.md](https://github.com/getsentry/sentry-javascript/blob/a493aa6a46555b944c8d896a2164bcd8b11caaf5/packages/replay/README.md?plain=1#LL204) also needs to be updated.
diff --git a/packages/replay-canvas/LICENSE b/packages/replay-canvas/LICENSE
new file mode 100644
index 000000000000..ea5e82344f87
--- /dev/null
+++ b/packages/replay-canvas/LICENSE
@@ -0,0 +1,14 @@
+Copyright (c) 2024 Sentry (https://sentry.io) and individual contributors. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/replay-canvas/README.md b/packages/replay-canvas/README.md
new file mode 100644
index 000000000000..eac14facc5cc
--- /dev/null
+++ b/packages/replay-canvas/README.md
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+# Sentry Session Replay with Canvas
+
+## Pre-requisites
+
+Replay with canvas requires Node 12+, and browsers newer than IE11.
+
+## Installation
+
+Replay and ReplayCanvas can be imported from `@sentry/browser`, or a respective SDK package like `@sentry/react` or `@sentry/vue`.
+You don't need to install anything in order to use Session Replay. The minimum version that includes Replay is 7.27.0.
+
+For details on using Replay when using Sentry via the CDN bundles, see [CDN bundle](#loading-replay-as-a-cdn-bundle).
+
+## Setup
+
+To set up the canvas integration, add the following to your Sentry integrations:
+
+```javascript
+new Sentry.ReplayCanvas(),
+```
+
+### Full Example
+
+```javascript
+import * as Sentry from '@sentry/browser';
+// or e.g. import * as Sentry from '@sentry/react';
+
+Sentry.init({
+ dsn: '__DSN__',
+
+ // This sets the sample rate to be 10%. You may want this to be 100% while
+ // in development and sample at a lower rate in production
+ replaysSessionSampleRate: 0.1,
+
+ // If the entire session is not sampled, use the below sample rate to sample
+ // sessions when an error occurs.
+ replaysOnErrorSampleRate: 1.0,
+
+ integrations: [
+ new Sentry.Replay(),
+ new Sentry.ReplayCanvas(),
+ ],
+ // ...
+});
+```
diff --git a/packages/replay-canvas/jest.config.js b/packages/replay-canvas/jest.config.js
new file mode 100644
index 000000000000..cd02790794a7
--- /dev/null
+++ b/packages/replay-canvas/jest.config.js
@@ -0,0 +1,6 @@
+const baseConfig = require('../../jest/jest.config.js');
+
+module.exports = {
+ ...baseConfig,
+ testEnvironment: 'jsdom',
+};
diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json
new file mode 100644
index 000000000000..81a70f18c20d
--- /dev/null
+++ b/packages/replay-canvas/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "@sentry-internal/replay-canvas",
+ "version": "7.93.0",
+ "description": "Replay canvas integration",
+ "main": "build/npm/cjs/index.js",
+ "module": "build/npm/esm/index.js",
+ "types": "build/npm/types/index.d.ts",
+ "typesVersions": {
+ "<4.9": {
+ "build/npm/types/index.d.ts": [
+ "build/npm/types-ts3.8/index.d.ts"
+ ]
+ }
+ },
+ "files": [
+ "cjs",
+ "esm",
+ "types",
+ "types-ts3.8"
+ ],
+ "sideEffects": false,
+ "scripts": {
+ "build": "run-p build:transpile build:types build:bundle",
+ "build:transpile": "rollup -c rollup.npm.config.mjs",
+ "build:bundle": "rollup -c rollup.bundle.config.mjs",
+ "build:dev": "run-p build:transpile build:types",
+ "build:types": "run-s build:types:core build:types:downlevel",
+ "build:types:core": "tsc -p tsconfig.types.json",
+ "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8",
+ "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch",
+ "build:dev:watch": "run-p build:transpile:watch build:types:watch",
+ "build:transpile:watch": "yarn build:transpile --watch",
+ "build:bundle:watch": "yarn build:bundle --watch",
+ "build:types:watch": "tsc -p tsconfig.types.json --watch",
+ "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm",
+ "circularDepCheck": "madge --circular src/index.ts",
+ "clean": "rimraf build sentry-replay-*.tgz",
+ "fix": "eslint . --format stylish --fix",
+ "lint": "eslint . --format stylish",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push --sig"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/getsentry/sentry-javascript.git"
+ },
+ "author": "Sentry",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/getsentry/sentry-javascript/issues"
+ },
+ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/",
+ "devDependencies": {
+ "@babel/core": "^7.17.5",
+ "@sentry-internal/rrweb": "2.8.0"
+ },
+ "dependencies": {
+ "@sentry/core": "7.93.0",
+ "@sentry/replay": "7.93.0",
+ "@sentry/types": "7.93.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/replay-canvas/rollup.bundle.config.mjs b/packages/replay-canvas/rollup.bundle.config.mjs
new file mode 100644
index 000000000000..1575b8114594
--- /dev/null
+++ b/packages/replay-canvas/rollup.bundle.config.mjs
@@ -0,0 +1,13 @@
+import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils';
+
+const baseBundleConfig = makeBaseBundleConfig({
+ bundleType: 'addon',
+ entrypoints: ['src/index.ts'],
+ jsVersion: 'es6',
+ licenseTitle: '@sentry-internal/replay-canvas',
+ outputFileBase: () => 'bundles/replay-canvas',
+});
+
+const builds = makeBundleConfigVariants(baseBundleConfig);
+
+export default builds;
diff --git a/packages/replay-canvas/rollup.npm.config.mjs b/packages/replay-canvas/rollup.npm.config.mjs
new file mode 100644
index 000000000000..8c50a33f0afb
--- /dev/null
+++ b/packages/replay-canvas/rollup.npm.config.mjs
@@ -0,0 +1,16 @@
+import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
+
+export default makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ hasBundles: true,
+ packageSpecificConfig: {
+ output: {
+ // set exports to 'named' or 'auto' so that rollup doesn't warn
+ exports: 'named',
+ // set preserveModules to false because for Replay we actually want
+ // to bundle everything into one file.
+ preserveModules: false,
+ },
+ },
+ }),
+);
diff --git a/packages/replay-canvas/scripts/craft-pre-release.sh b/packages/replay-canvas/scripts/craft-pre-release.sh
new file mode 100644
index 000000000000..bae7c3246cdb
--- /dev/null
+++ b/packages/replay-canvas/scripts/craft-pre-release.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -eux
+OLD_VERSION="${1}"
+NEW_VERSION="${2}"
+
+# Do not tag and commit changes made by "npm version"
+export npm_config_git_tag_version=false
+npm version "${NEW_VERSION}"
diff --git a/packages/replay-canvas/src/canvas.ts b/packages/replay-canvas/src/canvas.ts
new file mode 100644
index 000000000000..90b65e5ccd35
--- /dev/null
+++ b/packages/replay-canvas/src/canvas.ts
@@ -0,0 +1,85 @@
+import { CanvasManager } from '@sentry-internal/rrweb';
+import { convertIntegrationFnToClass } from '@sentry/core';
+import type { CanvasManagerInterface, CanvasManagerOptions } from '@sentry/replay';
+import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
+
+interface ReplayCanvasOptions {
+ quality: 'low' | 'medium' | 'high';
+}
+
+type GetCanvasManager = (options: CanvasManagerOptions) => CanvasManagerInterface;
+export interface ReplayCanvasIntegrationOptions {
+ recordCanvas: true;
+ getCanvasManager: GetCanvasManager;
+ sampling: {
+ canvas: number;
+ };
+ dataURLOptions: {
+ type: string;
+ quality: number;
+ };
+}
+
+const CANVAS_QUALITY = {
+ low: {
+ sampling: {
+ canvas: 1,
+ },
+ dataURLOptions: {
+ type: 'image/webp',
+ quality: 0.25,
+ },
+ },
+ medium: {
+ sampling: {
+ canvas: 2,
+ },
+ dataURLOptions: {
+ type: 'image/webp',
+ quality: 0.4,
+ },
+ },
+ high: {
+ sampling: {
+ canvas: 4,
+ },
+ dataURLOptions: {
+ type: 'image/webp',
+ quality: 0.5,
+ },
+ },
+};
+
+const INTEGRATION_NAME = 'ReplayCanvas';
+
+/**
+ * An integration to add canvas recording to replay.
+ */
+const replayCanvasIntegration = ((options: Partial = {}) => {
+ const _canvasOptions = {
+ quality: options.quality || 'medium',
+ };
+
+ return {
+ name: INTEGRATION_NAME,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ setupOnce() {},
+ getOptions(): ReplayCanvasIntegrationOptions {
+ const { quality } = _canvasOptions;
+
+ return {
+ recordCanvas: true,
+ getCanvasManager: (options: CanvasManagerOptions) => new CanvasManager(options),
+ ...(CANVAS_QUALITY[quality || 'medium'] || CANVAS_QUALITY.medium),
+ };
+ },
+ };
+}) satisfies IntegrationFn;
+
+// TODO(v8)
+// eslint-disable-next-line deprecation/deprecation
+export const ReplayCanvas = convertIntegrationFnToClass(INTEGRATION_NAME, replayCanvasIntegration) as IntegrationClass<
+ Integration & {
+ getOptions: () => ReplayCanvasIntegrationOptions;
+ }
+>;
diff --git a/packages/replay-canvas/src/index.ts b/packages/replay-canvas/src/index.ts
new file mode 100644
index 000000000000..97e5bab007a2
--- /dev/null
+++ b/packages/replay-canvas/src/index.ts
@@ -0,0 +1,2 @@
+export { ReplayCanvas } from './canvas';
+export type { ReplayCanvasIntegrationOptions } from './canvas';
diff --git a/packages/replay-canvas/test/canvas.test.ts b/packages/replay-canvas/test/canvas.test.ts
new file mode 100644
index 000000000000..e5464ba544ec
--- /dev/null
+++ b/packages/replay-canvas/test/canvas.test.ts
@@ -0,0 +1,33 @@
+import { ReplayCanvas } from '../src/canvas';
+
+it('initializes with default options', () => {
+ const rc = new ReplayCanvas();
+
+ expect(rc.getOptions()).toEqual({
+ recordCanvas: true,
+ getCanvasManager: expect.any(Function),
+ sampling: {
+ canvas: 2,
+ },
+ dataURLOptions: {
+ type: 'image/webp',
+ quality: 0.4,
+ },
+ });
+});
+
+it('initializes with quality option', () => {
+ const rc = new ReplayCanvas({ quality: 'low' });
+
+ expect(rc.getOptions()).toEqual({
+ recordCanvas: true,
+ getCanvasManager: expect.any(Function),
+ sampling: {
+ canvas: 1,
+ },
+ dataURLOptions: {
+ type: 'image/webp',
+ quality: 0.25,
+ },
+ });
+});
diff --git a/packages/replay-canvas/tsconfig.json b/packages/replay-canvas/tsconfig.json
new file mode 100644
index 000000000000..f8f54556da93
--- /dev/null
+++ b/packages/replay-canvas/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "esnext"
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/replay-canvas/tsconfig.test.json b/packages/replay-canvas/tsconfig.test.json
new file mode 100644
index 000000000000..ad87caa06c48
--- /dev/null
+++ b/packages/replay-canvas/tsconfig.test.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["test/**/*.ts", "jest.config.ts", "jest.setup.ts"],
+
+ "compilerOptions": {
+ "types": ["node", "jest"],
+ "esModuleInterop": true,
+ "allowJs": true,
+ "noImplicitAny": true,
+ "noImplicitThis": false,
+ "strictNullChecks": true,
+ "strictPropertyInitialization": false
+ }
+}
diff --git a/packages/replay-canvas/tsconfig.types.json b/packages/replay-canvas/tsconfig.types.json
new file mode 100644
index 000000000000..374fd9bc9364
--- /dev/null
+++ b/packages/replay-canvas/tsconfig.types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build/npm/types"
+ }
+}
diff --git a/packages/replay/package.json b/packages/replay/package.json
index 02a23ffc1df1..936b861a7f17 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.7.3",
- "@sentry-internal/rrweb-snapshot": "2.7.3",
+ "@sentry-internal/rrweb": "2.8.0",
+ "@sentry-internal/rrweb-snapshot": "2.8.0",
"fflate": "^0.8.1",
"jsdom-worker": "^0.2.1"
},
diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts
index ec3325a4253a..13ccea43df22 100644
--- a/packages/replay/src/constants.ts
+++ b/packages/replay/src/constants.ts
@@ -53,33 +53,3 @@ export const MAX_REPLAY_DURATION = 3_600_000; // 60 minutes in ms;
/** Default attributes to be ignored when `maskAllText` is enabled */
export const DEFAULT_IGNORED_ATTRIBUTES = ['title', 'placeholder'];
-
-export const CANVAS_QUALITY = {
- low: {
- sampling: {
- canvas: 1,
- },
- dataURLOptions: {
- type: 'image/webp',
- quality: 0.25,
- },
- },
- medium: {
- sampling: {
- canvas: 2,
- },
- dataURLOptions: {
- type: 'image/webp',
- quality: 0.4,
- },
- },
- high: {
- sampling: {
- canvas: 4,
- },
- dataURLOptions: {
- type: 'image/webp',
- quality: 0.5,
- },
- },
-};
diff --git a/packages/replay/src/index.ts b/packages/replay/src/index.ts
index 412354f1dc54..8e8a5d55579a 100644
--- a/packages/replay/src/index.ts
+++ b/packages/replay/src/index.ts
@@ -1,6 +1,7 @@
export { Replay } from './integration';
export type {
+ ReplayConfiguration,
ReplayEventType,
ReplayEventWithTime,
ReplayBreadcrumbFrame,
@@ -10,6 +11,8 @@ export type {
ReplayFrameEvent,
ReplaySpanFrame,
ReplaySpanFrameEvent,
+ CanvasManagerInterface,
+ CanvasManagerOptions,
} from './types';
// TODO (v8): Remove deprecated types
diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts
index 80bbeca5fdf3..c73da03c1b85 100644
--- a/packages/replay/src/integration.ts
+++ b/packages/replay/src/integration.ts
@@ -10,7 +10,13 @@ import {
MIN_REPLAY_DURATION_LIMIT,
} from './constants';
import { ReplayContainer } from './replay';
-import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions, SendBufferedReplayOptions } from './types';
+import type {
+ RecordingOptions,
+ ReplayCanvasIntegrationOptions,
+ ReplayConfiguration,
+ ReplayPluginOptions,
+ SendBufferedReplayOptions,
+} from './types';
import { getPrivacyOptions } from './util/getPrivacyOptions';
import { maskAttribute } from './util/maskAttribute';
@@ -318,6 +324,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
return this._replay.getSessionId();
}
+
/**
* Initializes replay.
*/
@@ -326,6 +333,12 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
return;
}
+ // We have to run this in _initialize, because this runs in setTimeout
+ // So when this runs all integrations have been added
+ // Before this, we cannot access integrations on the client,
+ // so we need to mutate the options here
+ this._maybeLoadFromReplayCanvasIntegration();
+
this._replay.initializeSampling();
}
@@ -339,6 +352,27 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
recordingOptions: this._recordingOptions,
});
}
+
+ /** Get canvas options from ReplayCanvas integration, if it is also added. */
+ private _maybeLoadFromReplayCanvasIntegration(): void {
+ // To save bundle size, we skip checking for stuff here
+ // and instead just try-catch everything - as generally this should all be defined
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
+ try {
+ const client = getClient()!;
+ const canvasIntegration = client.getIntegrationByName!('ReplayCanvas') as Integration & {
+ getOptions(): ReplayCanvasIntegrationOptions;
+ };
+ if (!canvasIntegration) {
+ return;
+ }
+
+ this._replay!['_canvas'] = canvasIntegration.getOptions();
+ } catch {
+ // ignore errors here
+ }
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
+ }
}
/** Parse Replay-related options from SDK options */
diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts
index 27513fe5643d..30ccda422544 100644
--- a/packages/replay/src/replay.ts
+++ b/packages/replay/src/replay.ts
@@ -12,7 +12,6 @@ import { logger } from '@sentry/utils';
import {
BUFFER_CHECKOUT_TIME,
- CANVAS_QUALITY,
SESSION_IDLE_EXPIRE_DURATION,
SESSION_IDLE_PAUSE_DURATION,
SLOW_CLICK_SCROLL_TIMEOUT,
@@ -39,6 +38,7 @@ import type {
RecordingEvent,
RecordingOptions,
ReplayBreadcrumbFrame,
+ ReplayCanvasIntegrationOptions,
ReplayContainer as ReplayContainerInterface,
ReplayPerformanceEntry,
ReplayPluginOptions,
@@ -145,6 +145,11 @@ export class ReplayContainer implements ReplayContainerInterface {
private _context: InternalEventContext;
+ /**
+ * Internal use for canvas recording options
+ */
+ private _canvas: ReplayCanvasIntegrationOptions | undefined;
+
public constructor({
options,
recordingOptions,
@@ -218,6 +223,13 @@ export class ReplayContainer implements ReplayContainerInterface {
return this._isPaused;
}
+ /**
+ * Determine if canvas recording is enabled
+ */
+ public isRecordingCanvas(): boolean {
+ return Boolean(this._canvas);
+ }
+
/** Get the replay integration options. */
public getOptions(): ReplayPluginOptions {
return this._options;
@@ -338,7 +350,8 @@ export class ReplayContainer implements ReplayContainerInterface {
*/
public startRecording(): void {
try {
- const canvas = this._options._experiments.canvas;
+ const canvasOptions = this._canvas;
+
this._stopRecording = record({
...this._recordingOptions,
// When running in error sampling mode, we need to overwrite `checkoutEveryNms`
@@ -347,12 +360,14 @@ export class ReplayContainer implements ReplayContainerInterface {
...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }),
emit: getHandleRecordingEmit(this),
onMutation: this._onMutationHandler,
- ...(canvas &&
- canvas.manager && {
- recordCanvas: true,
- getCanvasManager: canvas.manager,
- ...(CANVAS_QUALITY[canvas.quality || 'medium'] || CANVAS_QUALITY.medium),
- }),
+ ...(canvasOptions
+ ? {
+ recordCanvas: canvasOptions.recordCanvas,
+ getCanvasManager: canvasOptions.getCanvasManager,
+ sampling: canvasOptions.sampling,
+ dataURLOptions: canvasOptions.dataURLOptions,
+ }
+ : {}),
});
} catch (err) {
this._handleException(err);
diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts
index 1b2b19d4d4cd..93fd60a868f3 100644
--- a/packages/replay/src/types/replay.ts
+++ b/packages/replay/src/types/replay.ts
@@ -14,7 +14,7 @@ import type { SKIPPED, THROTTLED } from '../util/throttle';
import type { AllPerformanceEntry, AllPerformanceEntryData, ReplayPerformanceEntry } from './performance';
import type { ReplayFrameEvent } from './replayFrame';
import type { ReplayNetworkRequestOrResponse } from './request';
-import type { CanvasManagerInterface, GetCanvasManagerOptions, ReplayEventWithTime, RrwebRecordOptions } from './rrweb';
+import type { CanvasManagerInterface, CanvasManagerOptions, ReplayEventWithTime, RrwebRecordOptions } from './rrweb';
export type RecordingEvent = ReplayFrameEvent | ReplayEventWithTime;
export type RecordingOptions = RrwebRecordOptions;
@@ -232,10 +232,6 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
_experiments: Partial<{
captureExceptions: boolean;
traceInternals: boolean;
- canvas: {
- quality?: 'low' | 'medium' | 'high';
- manager: (options: GetCanvasManagerOptions) => CanvasManagerInterface;
- };
}>;
}
@@ -489,6 +485,7 @@ export interface ReplayContainer {
) => typeof THROTTLED | typeof SKIPPED | Promise;
isEnabled(): boolean;
isPaused(): boolean;
+ isRecordingCanvas(): boolean;
getContext(): InternalEventContext;
initializeSampling(): void;
start(): void;
@@ -539,3 +536,15 @@ export interface SlowClickConfig {
scrollTimeout: number;
ignoreSelector: string;
}
+
+export interface ReplayCanvasIntegrationOptions {
+ recordCanvas: true;
+ getCanvasManager: (options: CanvasManagerOptions) => CanvasManagerInterface;
+ sampling: {
+ canvas: number;
+ };
+ dataURLOptions: {
+ type: string;
+ quality: number;
+ };
+}
diff --git a/packages/replay/src/types/replayFrame.ts b/packages/replay/src/types/replayFrame.ts
index 8c41173e1a8e..3a595e47a4cf 100644
--- a/packages/replay/src/types/replayFrame.ts
+++ b/packages/replay/src/types/replayFrame.ts
@@ -122,6 +122,7 @@ interface ReplayOptionFrame {
networkRequestHasHeaders: boolean;
networkResponseHasHeaders: boolean;
sessionSampleRate: number;
+ shouldRecordCanvas: boolean;
useCompression: boolean;
useCompressionOption: boolean;
}
diff --git a/packages/replay/src/types/rrweb.ts b/packages/replay/src/types/rrweb.ts
index bbef4f94903f..7cbea7af07eb 100644
--- a/packages/replay/src/types/rrweb.ts
+++ b/packages/replay/src/types/rrweb.ts
@@ -51,9 +51,10 @@ export interface CanvasManagerInterface {
unfreeze(): void;
lock(): void;
unlock(): void;
+ snapshot(): void;
}
-export interface GetCanvasManagerOptions {
+export interface CanvasManagerOptions {
recordCanvas: boolean;
blockClass: string | RegExp;
blockSelector: string | null;
@@ -63,4 +64,7 @@ export interface GetCanvasManagerOptions {
type: string;
quality: number;
}>;
+ mutationCb: (p: any) => void;
+ win: typeof globalThis & Window;
+ mirror: any;
}
diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts
index 1d0fd10d0aa3..eaec29be261a 100644
--- a/packages/replay/src/util/handleRecordingEmit.ts
+++ b/packages/replay/src/util/handleRecordingEmit.ts
@@ -122,6 +122,7 @@ export function createOptionsEvent(replay: ReplayContainer): ReplayOptionFrameEv
data: {
tag: 'options',
payload: {
+ shouldRecordCanvas: replay.isRecordingCanvas(),
sessionSampleRate: options.sessionSampleRate,
errorSampleRate: options.errorSampleRate,
useCompressionOption: options.useCompression,
diff --git a/packages/replay/test/integration/rrweb.test.ts b/packages/replay/test/integration/rrweb.test.ts
index 2e648358ffe3..82dd18f2d6ec 100644
--- a/packages/replay/test/integration/rrweb.test.ts
+++ b/packages/replay/test/integration/rrweb.test.ts
@@ -40,33 +40,4 @@ describe('Integration | rrweb', () => {
}
`);
});
-
- it('calls rrweb.record with default canvas options', async () => {
- const { mockRecord } = await resetSdkMock({
- replayOptions: {
- _experiments: {
- canvas: {
- // @ts-expect-error This should return
- // CanvasManagerInterface, but we don't care about it
- // for this test
- manager: () => null,
- },
- },
- },
- });
-
- expect(mockRecord).toHaveBeenLastCalledWith(
- expect.objectContaining({
- recordCanvas: true,
- getCanvasManager: expect.any(Function),
- dataURLOptions: {
- quality: 0.4,
- type: 'image/webp',
- },
- sampling: {
- canvas: 2,
- },
- }),
- );
- });
});
diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts
index fb1d90fbb900..567224854baa 100644
--- a/scripts/node-unit-tests.ts
+++ b/scripts/node-unit-tests.ts
@@ -20,6 +20,7 @@ const DEFAULT_SKIP_TESTS_PACKAGES = [
'@sentry/angular',
'@sentry/svelte',
'@sentry/replay',
+ '@sentry-internal/replay-canvas',
'@sentry-internal/feedback',
'@sentry/wasm',
'@sentry/bun',
diff --git a/yarn.lock b/yarn.lock
index d888e192dd15..1c41f5a976da 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5231,33 +5231,33 @@
semver "7.3.2"
semver-intersect "1.4.0"
-"@sentry-internal/rrdom@2.7.3":
- version "2.7.3"
- resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.7.3.tgz#2efe68a9cf23de9a8970acf4303748cdd7866b20"
- integrity sha512-XD14G4Lv3ppvJlR7VkkCgHTKu1ylh7yvXdSsN5/FyGTH+IAXQIKL5nINIgWZTN3noNBWV9R0vcHDufXG/WktWA==
+"@sentry-internal/rrdom@2.8.0":
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.8.0.tgz#092c6201b71474f650fdda0320a9a82319c0f460"
+ integrity sha512-2jPoYMwRDwMjpYPnQ0/BTjpVvemABrQpH2zWuBYsACCLcAeCenNNQ7Qrqciv+hBnpUwBpTD1sYDqvSx2bP0C+w==
dependencies:
- "@sentry-internal/rrweb-snapshot" "2.7.3"
+ "@sentry-internal/rrweb-snapshot" "2.8.0"
-"@sentry-internal/rrweb-snapshot@2.7.3":
- version "2.7.3"
- resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.7.3.tgz#9a7173825a31c07ccf27a5956f154400e11fdd97"
- integrity sha512-mSZuBPmWia3x9wCuaJiZMD9ZVDnFv7TSG1Nz9X4ZqWb3DdaxB2MogGUU/2aTVqmRj6F91nl+GHb5NpmNYojUsw==
+"@sentry-internal/rrweb-snapshot@2.8.0":
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.8.0.tgz#4db7c10125d53fb20b4ff2a2a61a63b3f3ccaac1"
+ integrity sha512-JTpwdT6sVS4X3zr+qBs6/SsCR2JMLGHMDz4pN48HHDuBmW0QqiXDrM4SD9Tl7vUahHBCO0hG+E/UGFUbCV2b3w==
-"@sentry-internal/rrweb-types@2.7.3":
- version "2.7.3"
- resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.7.3.tgz#e38737bc5c31aa9dfb8ce8faf46af374c2aa7cbb"
- integrity sha512-EqALxhZtvH0rimYfj7J48DRC+fj+AGsZ/VDdOPKh3MQXptTyHncoWBj4ZtB1AaH7foYUr+2wkyxl3HqMVwe+6g==
+"@sentry-internal/rrweb-types@2.8.0":
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.8.0.tgz#ca1da1847630c4457a7b838d79f58b2b89b2d740"
+ integrity sha512-K66baWV0srzWl+ioUPf1FRGEp7gNOl8PqVaabmQfXc7RmA5t2215+63f1f3dMnEwMSJF0WmojA7orFcSy8sx0g==
dependencies:
- "@sentry-internal/rrweb-snapshot" "2.7.3"
+ "@sentry-internal/rrweb-snapshot" "2.8.0"
-"@sentry-internal/rrweb@2.7.3":
- version "2.7.3"
- resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.7.3.tgz#900ce7b1bd8ec43b5557d73698cc1b4dc9f47f72"
- integrity sha512-K2pksQ1FgDumv54r1o0+RAKB3Qx3Zgx6OrQAkU/SI6/7HcBIxe7b4zepg80NyvnfohUs/relw2EoD3K3kqd8tg==
+"@sentry-internal/rrweb@2.8.0":
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.8.0.tgz#d1492bdabff2a87001dd7d4514038d40c142267f"
+ integrity sha512-vKqGk7epbi8a4wn5yWMJCe+m2tbYDHT7v258o/moj0YdlBXMxHjIBJF/m1AlqreQlCP7AESyFnYFX2OTdKyygQ==
dependencies:
- "@sentry-internal/rrdom" "2.7.3"
- "@sentry-internal/rrweb-snapshot" "2.7.3"
- "@sentry-internal/rrweb-types" "2.7.3"
+ "@sentry-internal/rrdom" "2.8.0"
+ "@sentry-internal/rrweb-snapshot" "2.8.0"
+ "@sentry-internal/rrweb-types" "2.8.0"
"@types/css-font-loading-module" "0.0.7"
"@xstate/fsm" "^1.4.0"
base64-arraybuffer "^1.0.1"