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 + +

+ +# 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"