Skip to content

Commit 61feab0

Browse files
committed
feat(replay): Allow to treeshake & configure compression worker URL
1 parent a5e8424 commit 61feab0

File tree

19 files changed

+4476
-23
lines changed

19 files changed

+4476
-23
lines changed

.size-limit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = [
2121
__RRWEB_EXCLUDE_CANVAS__: true,
2222
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
2323
__RRWEB_EXCLUDE_IFRAME__: true,
24+
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
2425
}),
2526
);
2627
return config;

packages/browser-integration-tests/suites/replay/compression/subject.js

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/browser-integration-tests/suites/replay/compression/test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { getExpectedReplayEvent } from '../../../utils/replayEventTemplates';
55
import {
66
getFullRecordingSnapshots,
77
getReplayEvent,
8+
replayEnvelopeIsCompressed,
89
shouldSkipReplayTest,
910
waitForReplayRequest,
1011
} from '../../../utils/replayHelpers';
1112

12-
sentryTest('replay recording should be compressed by default', async ({ getLocalTestPath, page }) => {
13+
sentryTest('replay recording should be compressed by default', async ({ getLocalTestPath, page, forceFlushReplay }) => {
1314
if (shouldSkipReplayTest()) {
1415
sentryTest.skip();
1516
}
@@ -27,10 +28,16 @@ sentryTest('replay recording should be compressed by default', async ({ getLocal
2728
const url = await getLocalTestPath({ testDir: __dirname });
2829

2930
await page.goto(url);
30-
const replayEvent0 = getReplayEvent(await reqPromise0);
31+
await forceFlushReplay();
32+
33+
const req0 = await reqPromise0;
34+
35+
const replayEvent0 = getReplayEvent(req0);
3136
expect(replayEvent0).toEqual(getExpectedReplayEvent());
3237

33-
const snapshots = getFullRecordingSnapshots(await reqPromise0);
38+
expect(replayEnvelopeIsCompressed(req0)).toEqual(true);
39+
40+
const snapshots = getFullRecordingSnapshots(req0);
3441
expect(snapshots.length).toEqual(1);
3542

3643
const stringifiedSnapshot = JSON.stringify(snapshots[0]);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = new Sentry.Replay({
5+
flushMinDelay: 200,
6+
flushMaxDelay: 200,
7+
minReplayDuration: 0,
8+
useCompression: false,
9+
});
10+
11+
Sentry.init({
12+
dsn: 'https://[email protected]/1337',
13+
sampleRate: 0,
14+
replaysSessionSampleRate: 1.0,
15+
replaysOnErrorSampleRate: 0.0,
16+
17+
integrations: [window.Replay],
18+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="go-background">New Tab</button>
8+
</body>
9+
</html>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import { getExpectedReplayEvent } from '../../../utils/replayEventTemplates';
5+
import {
6+
getFullRecordingSnapshots,
7+
getReplayEvent,
8+
replayEnvelopeIsCompressed,
9+
shouldSkipReplayTest,
10+
waitForReplayRequest,
11+
} from '../../../utils/replayHelpers';
12+
13+
sentryTest(
14+
'replay recording should allow to disable compression',
15+
async ({ getLocalTestPath, page, forceFlushReplay }) => {
16+
if (shouldSkipReplayTest()) {
17+
sentryTest.skip();
18+
}
19+
20+
const reqPromise0 = waitForReplayRequest(page, 0);
21+
22+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
23+
return route.fulfill({
24+
status: 200,
25+
contentType: 'application/json',
26+
body: JSON.stringify({ id: 'test-id' }),
27+
});
28+
});
29+
30+
const url = await getLocalTestPath({ testDir: __dirname });
31+
32+
await page.goto(url);
33+
await forceFlushReplay();
34+
35+
const req0 = await reqPromise0;
36+
37+
const replayEvent0 = getReplayEvent(req0);
38+
expect(replayEvent0).toEqual(getExpectedReplayEvent());
39+
40+
expect(replayEnvelopeIsCompressed(req0)).toEqual(false);
41+
42+
const snapshots = getFullRecordingSnapshots(req0);
43+
expect(snapshots.length).toEqual(1);
44+
45+
const stringifiedSnapshot = JSON.stringify(snapshots[0]);
46+
expect(stringifiedSnapshot).toContain('"tagName":"body"');
47+
expect(stringifiedSnapshot).toContain('"tagName":"html"');
48+
expect(stringifiedSnapshot).toContain('"tagName":"button"');
49+
expect(stringifiedSnapshot).toContain('"textContent":"*** ***"');
50+
expect(stringifiedSnapshot).toContain('"id":"go-background"');
51+
},
52+
);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = new Sentry.Replay({
5+
flushMinDelay: 200,
6+
flushMaxDelay: 200,
7+
minReplayDuration: 0,
8+
useCompression: true,
9+
workerUrl: `${window.location.origin}/my-test-worker.js`,
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
sampleRate: 0,
15+
replaysSessionSampleRate: 1.0,
16+
replaysOnErrorSampleRate: 0.0,
17+
18+
integrations: [window.Replay],
19+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="go-background">New Tab</button>
8+
</body>
9+
</html>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect } from '@playwright/test';
2+
import fs from 'fs';
3+
import path from 'path';
4+
5+
import { sentryTest, TEST_HOST } from '../../../utils/fixtures';
6+
import { getExpectedReplayEvent } from '../../../utils/replayEventTemplates';
7+
import {
8+
getFullRecordingSnapshots,
9+
getReplayEvent,
10+
replayEnvelopeIsCompressed,
11+
shouldSkipReplayTest,
12+
waitForReplayRequest,
13+
} from '../../../utils/replayHelpers';
14+
15+
sentryTest(
16+
'replay recording should be compressed if using custom workerUrl',
17+
async ({ getLocalTestUrl, page, forceFlushReplay }) => {
18+
if (shouldSkipReplayTest()) {
19+
sentryTest.skip();
20+
}
21+
22+
const reqPromise0 = waitForReplayRequest(page, 0);
23+
24+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
25+
return route.fulfill({
26+
status: 200,
27+
contentType: 'application/json',
28+
body: JSON.stringify({ id: 'test-id' }),
29+
});
30+
});
31+
32+
const url = await getLocalTestUrl({ testDir: __dirname });
33+
34+
let customCompressCalled = 0;
35+
36+
// Ensure to register this _after_ getLocalTestUrl is called, as that also registers a default route for TEST_HOST
37+
await page.route(`${TEST_HOST}/my-test-worker.js`, route => {
38+
const filePath = path.resolve(__dirname, '../../../../replay-worker/examples/worker.min.js');
39+
40+
customCompressCalled++;
41+
42+
return fs.existsSync(filePath) ? route.fulfill({ path: filePath }) : route.continue();
43+
});
44+
45+
await page.goto(url);
46+
await forceFlushReplay();
47+
48+
const req0 = await reqPromise0;
49+
50+
const replayEvent0 = getReplayEvent(req0);
51+
expect(replayEvent0).toEqual(getExpectedReplayEvent());
52+
53+
expect(replayEnvelopeIsCompressed(req0)).toEqual(true);
54+
expect(customCompressCalled).toBe(1);
55+
56+
const snapshots = getFullRecordingSnapshots(req0);
57+
expect(snapshots.length).toEqual(1);
58+
59+
const stringifiedSnapshot = JSON.stringify(snapshots[0]);
60+
expect(stringifiedSnapshot).toContain('"tagName":"body"');
61+
expect(stringifiedSnapshot).toContain('"tagName":"html"');
62+
expect(stringifiedSnapshot).toContain('"tagName":"button"');
63+
expect(stringifiedSnapshot).toContain('"textContent":"*** ***"');
64+
expect(stringifiedSnapshot).toContain('"id":"go-background"');
65+
},
66+
);

packages/browser-integration-tests/utils/replayEventTemplates.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const DEFAULT_REPLAY_EVENT = {
88
timestamp: expect.any(Number),
99
error_ids: [],
1010
trace_ids: [],
11-
urls: [expect.stringContaining('/dist/index.html')],
11+
urls: [expect.stringContaining('/index.html')],
1212
replay_id: expect.stringMatching(/\w{32}/),
1313
replay_start_timestamp: expect.any(Number),
1414
segment_id: 0,
@@ -31,7 +31,7 @@ const DEFAULT_REPLAY_EVENT = {
3131
name: 'sentry.javascript.browser',
3232
},
3333
request: {
34-
url: expect.stringContaining('/dist/index.html'),
34+
url: expect.stringContaining('/index.html'),
3535
headers: {
3636
'User-Agent': expect.stringContaining(''),
3737
},

packages/browser-integration-tests/utils/replayHelpers.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,38 @@ const replayEnvelopeRequestParser = (request: Request | null, envelopeIndex = 2)
302302
return envelope[envelopeIndex] as Event;
303303
};
304304

305+
export function replayEnvelopeIsCompressed(resOrReq: Request | Response): boolean {
306+
const request = getRequest(resOrReq);
307+
308+
// https://develop.sentry.dev/sdk/envelopes/
309+
const envelopeBytes = request.postDataBuffer() || '';
310+
311+
// first, we convert the bugger to string to split and go through the uncompressed lines
312+
const envelopeString = envelopeBytes.toString();
313+
314+
const lines: boolean[] = envelopeString.split('\n').map(line => {
315+
try {
316+
JSON.parse(line);
317+
} catch (error) {
318+
// If we fail to parse a line, we _might_ have found a compressed payload,
319+
// so let's check if this is actually the case.
320+
// This is quite hacky but we can't go through `line` because the prior operations
321+
// seem to have altered its binary content. Hence, we take the raw envelope and
322+
// look up the place where the zlib compression header(0x78 0x9c) starts
323+
for (let i = 0; i < envelopeBytes.length; i++) {
324+
if (envelopeBytes[i] === 0x78 && envelopeBytes[i + 1] === 0x9c) {
325+
// We found a zlib-compressed payload - let's decompress it
326+
return true;
327+
}
328+
}
329+
}
330+
331+
return false;
332+
});
333+
334+
return lines.some(line => line);
335+
}
336+
305337
export const replayEnvelopeParser = (request: Request | null): unknown[] => {
306338
// https://develop.sentry.dev/sdk/envelopes/
307339
const envelopeBytes = request?.postDataBuffer() || '';

0 commit comments

Comments
 (0)