Skip to content

Commit f4564cc

Browse files
authored
test(replay): Test compressed recording payloads (#7082)
Add Playwright integration test utilities to test Replay's zlib compression. We now have a dedicated parser that attempts to decompress a payload, if it encounters a zlib-compressed replay recording payload. Additionally, add a simple test to check that an initial full compressed snapshot has the correct content.
1 parent 2a09fd6 commit f4564cc

File tree

8 files changed

+144
-3
lines changed

8 files changed

+144
-3
lines changed

packages/integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@playwright/test": "^1.29.2",
3333
"babel-loader": "^8.2.2",
3434
"html-webpack-plugin": "^5.5.0",
35+
"pako": "^2.1.0",
3536
"playwright": "^1.29.2",
3637
"typescript": "^4.5.2",
3738
"webpack": "^5.52.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+
import { Replay } from '@sentry/replay';
3+
4+
window.Sentry = Sentry;
5+
window.Replay = new Replay({
6+
flushMinDelay: 500,
7+
flushMaxDelay: 500,
8+
useCompression: true,
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
document.getElementById('go-background').addEventListener('click', () => {
2+
Object.defineProperty(document, 'hidden', { value: true, writable: true });
3+
const ev = document.createEvent('Event');
4+
ev.initEvent('visibilitychange');
5+
document.dispatchEvent(ev);
6+
});
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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import { getExpectedReplayEvent } from '../../../utils/replayEventTemplates';
5+
import { getFullRecordingSnapshots, getReplayEvent, waitForReplayRequest } from '../../../utils/replayHelpers';
6+
7+
sentryTest('replay recording should be compressed by default', async ({ getLocalTestPath, page }) => {
8+
// Replay bundles are es6 only
9+
if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_es5')) {
10+
sentryTest.skip();
11+
}
12+
13+
const reqPromise0 = waitForReplayRequest(page, 0);
14+
15+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
16+
return route.fulfill({
17+
status: 200,
18+
contentType: 'application/json',
19+
body: JSON.stringify({ id: 'test-id' }),
20+
});
21+
});
22+
23+
const url = await getLocalTestPath({ testDir: __dirname });
24+
25+
await page.goto(url);
26+
const replayEvent0 = getReplayEvent(await reqPromise0);
27+
expect(replayEvent0).toEqual(getExpectedReplayEvent());
28+
29+
const snapshots = getFullRecordingSnapshots(await reqPromise0);
30+
expect(snapshots.length).toEqual(1);
31+
32+
const stringifiedSnapshot = JSON.stringify(snapshots[0]);
33+
expect(stringifiedSnapshot).toContain('"tagName":"body"');
34+
expect(stringifiedSnapshot).toContain('"tagName":"html"');
35+
expect(stringifiedSnapshot).toContain('"tagName":"button"');
36+
expect(stringifiedSnapshot).toContain('"textContent":"*** ***"');
37+
expect(stringifiedSnapshot).toContain('"id":"go-background"');
38+
});

packages/integration-tests/utils/replayHelpers.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { RecordingEvent, ReplayContainer } from '@sentry/replay/build/npm/types/types';
22
import type { Breadcrumb, Event, ReplayEvent } from '@sentry/types';
3+
import pako from 'pako';
34
import type { Page, Request } from 'playwright';
45

56
import { envelopeRequestParser } from './helpers';
@@ -13,6 +14,17 @@ type PerformanceSpan = {
1314
data: Record<string, number>;
1415
};
1516

17+
export type RecordingSnapshot = {
18+
node: SnapshotNode;
19+
initialOffset: number;
20+
};
21+
22+
type SnapshotNode = {
23+
type: number;
24+
id: number;
25+
childNodes: SnapshotNode[];
26+
};
27+
1628
/**
1729
* Waits for a replay request to be sent by the page and returns it.
1830
*
@@ -85,7 +97,7 @@ export function getCustomRecordingEvents(replayRequest: Request): {
8597
breadcrumbs: Breadcrumb[];
8698
performanceSpans: PerformanceSpan[];
8799
} {
88-
const recordingEvents = envelopeRequestParser(replayRequest, 5) as RecordingEvent[];
100+
const recordingEvents = getDecompressedRecordingEvents(replayRequest);
89101

90102
const breadcrumbs = getReplayBreadcrumbs(recordingEvents);
91103
const performanceSpans = getReplayPerformanceSpans(recordingEvents);
@@ -108,3 +120,60 @@ function getReplayPerformanceSpans(recordingEvents: RecordingEvent[]): Performan
108120
.filter(data => data.tag === 'performanceSpan')
109121
.map(data => data.payload) as PerformanceSpan[];
110122
}
123+
124+
export function getFullRecordingSnapshots(replayRequest: Request): RecordingSnapshot[] {
125+
const events = getDecompressedRecordingEvents(replayRequest) as RecordingEvent[];
126+
return events.filter(event => event.type === 2).map(event => event.data as RecordingSnapshot);
127+
}
128+
129+
function getDecompressedRecordingEvents(replayRequest: Request): RecordingEvent[] {
130+
return replayEnvelopeRequestParser(replayRequest, 5) as RecordingEvent[];
131+
}
132+
133+
/**
134+
* Copy of the envelopeParser from ./helpers.ts, but with the ability
135+
* to decompress zlib-compressed envelope payloads which we need to inspect for replay recordings.
136+
* This parser can handle uncompressed as well as compressed replay recordings.
137+
*/
138+
const replayEnvelopeRequestParser = (request: Request | null, envelopeIndex = 2): Event => {
139+
const envelope = replayEnvelopeParser(request);
140+
return envelope[envelopeIndex] as Event;
141+
};
142+
143+
const replayEnvelopeParser = (request: Request | null): unknown[] => {
144+
// https://develop.sentry.dev/sdk/envelopes/
145+
const envelopeBytes = request?.postDataBuffer() || '';
146+
147+
// first, we convert the bugger to string to split and go through the uncompressed lines
148+
const envelopeString = envelopeBytes.toString();
149+
150+
const lines = envelopeString.split('\n').map(line => {
151+
try {
152+
return JSON.parse(line);
153+
} catch (error) {
154+
// If we fail to parse a line, we _might_ have found a compressed payload,
155+
// so let's check if this is actually the case.
156+
// This is quite hacky but we can't go through `line` because the prior operations
157+
// seem to have altered its binary content. Hence, we take the raw envelope and
158+
// look up the place where the zlib compression header(0x78 0x9c) starts
159+
for (let i = 0; i < envelopeBytes.length; i++) {
160+
if (envelopeBytes[i] === 0x78 && envelopeBytes[i + 1] === 0x9c) {
161+
try {
162+
// We found a zlib-compressed payload - let's decompress it
163+
const payload = envelopeBytes.slice(i);
164+
// now we return the decompressed payload as JSON
165+
const decompressedPayload = pako.inflate(payload as unknown as Uint8Array, { to: 'string' });
166+
return JSON.parse(decompressedPayload);
167+
} catch {
168+
// Let's log that something went wrong
169+
return line;
170+
}
171+
}
172+
}
173+
174+
return line;
175+
}
176+
});
177+
178+
return lines;
179+
};

packages/replay/src/worker/worker.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18616,7 +18616,7 @@ pad@^3.2.0:
1861618616
dependencies:
1861718617
wcwidth "^1.0.1"
1861818618

18619-
pako@^2.0.4:
18619+
pako@^2.0.4, pako@^2.1.0:
1862018620
version "2.1.0"
1862118621
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
1862218622
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==

0 commit comments

Comments
 (0)