Skip to content

Commit

Permalink
feat(replay): Add ReplayCanvas integration (#10112)
Browse files Browse the repository at this point in the history
Adding this integration in addition to `Replay` will set up canvas
recording.

```javascript
Sentry.init({
  dsn: '',
  integrations: [
    Sentry.Replay(),
    Sentry.ReplayCanvas(),
  ]
});
```

---------

Co-authored-by: Francesco Novy <francesco.novy@sentry.io>
  • Loading branch information
billyvg and mydea authored Jan 17, 2024
1 parent 70d1cbb commit 11a8afe
Show file tree
Hide file tree
Showing 52 changed files with 659 additions and 108 deletions.
4 changes: 4 additions & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ jobs:
- *shared
- 'packages/browser/**'
- 'packages/replay/**'
- 'packages/replay-canvas/**'
- 'packages/feedback/**'
browser_integration:
- *shared
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/browser-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@playwright/test": "^1.31.1",
"@sentry-internal/rrweb": "2.7.3",
"@sentry-internal/rrweb": "2.8.0",
"@sentry/browser": "7.93.0",
"@sentry/tracing": "7.93.0",
"axios": "1.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { getCanvasManager } from '@sentry-internal/rrweb';
import * as Sentry from '@sentry/browser';

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

Sentry.init({
Expand All @@ -20,5 +14,5 @@ Sentry.init({
replaysOnErrorSampleRate: 0.0,
debug: true,

integrations: [window.Replay],
integrations: [window.Replay, new Sentry.ReplayCanvas()],
});
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window.Replay = new Sentry.Replay({
flushMinDelay: 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],
});
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window.Replay = new Sentry.Replay({
flushMinDelay: 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()],
});
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window.Replay = new Sentry.Replay({
flushMinDelay: 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],
});
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ sentryTest(
networkCaptureBodies: true,
networkRequestHasHeaders: true,
networkResponseHasHeaders: true,
shouldRecordCanvas: false,
},
},
]);
Expand Down
11 changes: 9 additions & 2 deletions dev-packages/browser-integration-tests/utils/replayHelpers.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -174,21 +175,27 @@ 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 = {
_isPaused: replay.isPaused(),
_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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const ReplayRecordingData = [
networkRequestHasHeaders: true,
networkResponseHasHeaders: true,
sessionSampleRate: 1,
shouldRecordCanvas: false,
useCompression: false,
useCompressionOption: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const ReplayRecordingData = [
networkRequestHasHeaders: true,
networkResponseHasHeaders: true,
sessionSampleRate: 1,
shouldRecordCanvas: false,
useCompression: false,
useCompressionOption: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const ReplayRecordingData = [
networkRequestHasHeaders: true,
networkResponseHasHeaders: true,
sessionSampleRate: 1,
shouldRecordCanvas: false,
useCompression: false,
useCompressionOption: true,
},
Expand Down
1 change: 1 addition & 0 deletions dev-packages/rollup-utils/plugins/bundlePlugins.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"packages/react",
"packages/remix",
"packages/replay",
"packages/replay-canvas",
"packages/replay-worker",
"packages/serverless",
"packages/svelte",
Expand Down
1 change: 1 addition & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export type {
ReplaySpanFrameEvent,
} from '@sentry/replay';

export { ReplayCanvas } from '@sentry-internal/replay-canvas';

export { Feedback } from '@sentry-internal/feedback';

export {
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/test/integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/remix/test/integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/replay-canvas/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
build/
25 changes: 25 additions & 0 deletions packages/replay-canvas/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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',
},
},
],
};
4 changes: 4 additions & 0 deletions packages/replay-canvas/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
/*.tgz
.eslintcache
build
Loading

0 comments on commit 11a8afe

Please sign in to comment.