Skip to content

Commit 2e281ad

Browse files
authored
test(replay): Add basic replay integration tests (#6735)
1 parent f432d09 commit 2e281ad

File tree

7 files changed

+175
-7
lines changed

7 files changed

+175
-7
lines changed
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 onclick="console.log('Test log')">Click me</button>
8+
</body>
9+
</html>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { expect } from '@playwright/test';
2+
import { SDK_VERSION } from '@sentry/browser';
3+
import type { Event } from '@sentry/types';
4+
5+
import { sentryTest } from '../../../utils/fixtures';
6+
import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
7+
8+
sentryTest('captureReplay', async ({ getLocalTestPath, page }) => {
9+
// Currently bundle tests are not supported for replay
10+
if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) {
11+
sentryTest.skip();
12+
}
13+
14+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
15+
return route.fulfill({
16+
status: 200,
17+
contentType: 'application/json',
18+
body: JSON.stringify({ id: 'test-id' }),
19+
});
20+
});
21+
22+
const url = await getLocalTestPath({ testDir: __dirname });
23+
await page.goto(url);
24+
25+
await page.click('button');
26+
await page.waitForTimeout(200);
27+
28+
const replayEvent = await getFirstSentryEnvelopeRequest<Event>(page, url);
29+
30+
expect(replayEvent).toBeDefined();
31+
expect(replayEvent).toEqual({
32+
type: 'replay_event',
33+
timestamp: expect.any(Number),
34+
error_ids: [],
35+
trace_ids: [],
36+
urls: [expect.stringContaining('/dist/index.html')],
37+
replay_id: expect.stringMatching(/\w{32}/),
38+
segment_id: 2,
39+
replay_type: 'session',
40+
event_id: expect.stringMatching(/\w{32}/),
41+
environment: 'production',
42+
sdk: {
43+
integrations: [
44+
'InboundFilters',
45+
'FunctionToString',
46+
'TryCatch',
47+
'Breadcrumbs',
48+
'GlobalHandlers',
49+
'LinkedErrors',
50+
'Dedupe',
51+
'HttpContext',
52+
'Replay',
53+
],
54+
version: SDK_VERSION,
55+
name: 'sentry.javascript.browser',
56+
},
57+
sdkProcessingMetadata: {},
58+
request: {
59+
url: expect.stringContaining('/dist/index.html'),
60+
headers: {
61+
'User-Agent': expect.stringContaining(''),
62+
},
63+
},
64+
platform: 'javascript',
65+
tags: { sessionSampleRate: 1, errorSampleRate: 0 },
66+
});
67+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = new Sentry.Replay({
5+
flushMinDelay: 200,
6+
initialFlushDelay: 200,
7+
});
8+
9+
Sentry.init({
10+
dsn: 'https://[email protected]/1337',
11+
sampleRate: 0,
12+
replaysSessionSampleRate: 1.0,
13+
replaysOnErrorSampleRate: 0.0,
14+
15+
integrations: [window.Replay],
16+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = new Sentry.Replay({
5+
flushMinDelay: 200,
6+
initialFlushDelay: 200,
7+
});
8+
9+
Sentry.init({
10+
dsn: 'https://[email protected]/1337',
11+
sampleRate: 0,
12+
replaysSessionSampleRate: 0.0,
13+
replaysOnErrorSampleRate: 0.0,
14+
15+
integrations: [window.Replay],
16+
});
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 onclick="console.log('Test log')">Click me</button>
8+
</body>
9+
</html>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import { getReplaySnapshot } from '../../../utils/helpers';
5+
6+
sentryTest('sampling', async ({ getLocalTestPath, page }) => {
7+
// Currently bundle tests are not supported for replay
8+
if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) {
9+
sentryTest.skip();
10+
}
11+
12+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
13+
// This should never be called!
14+
expect(true).toBe(false);
15+
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+
await page.goto(url);
25+
26+
await page.click('button');
27+
await page.waitForTimeout(200);
28+
29+
const replay = await getReplaySnapshot(page);
30+
31+
expect(replay.session?.sampled).toBe(false);
32+
33+
// Cannot wait on getFirstSentryEnvelopeRequest, as that never resolves
34+
});

packages/integration-tests/utils/helpers.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Page, Request } from '@playwright/test';
2+
import type { ReplayContainer } from '@sentry/replay/build/npm/types/types';
23
import type { Event, EventEnvelopeHeaders } from '@sentry/types';
34

45
const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
@@ -8,7 +9,13 @@ const envelopeRequestParser = (request: Request | null): Event => {
89
const envelope = request?.postData() || '';
910

1011
// Third row of the envelop is the event payload.
11-
return envelope.split('\n').map(line => JSON.parse(line))[2];
12+
return envelope.split('\n').map(line => {
13+
try {
14+
return JSON.parse(line);
15+
} catch (error) {
16+
return line;
17+
}
18+
})[2];
1219
};
1320

1421
export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => {
@@ -46,24 +53,34 @@ async function getSentryEvents(page: Page, url?: string): Promise<Array<Event>>
4653
return eventsHandle.jsonValue();
4754
}
4855

56+
/**
57+
* This returns the replay container (assuming it exists).
58+
* Note that due to how this works with playwright, this is a POJO copy of replay.
59+
* This means that we cannot access any methods on it, and also not mutate it in any way.
60+
*/
61+
export async function getReplaySnapshot(page: Page): Promise<ReplayContainer> {
62+
const replayIntegration = await page.evaluate<{ _replay: ReplayContainer }>('window.Replay');
63+
return replayIntegration._replay;
64+
}
65+
4966
/**
5067
* Waits until a number of requests matching urlRgx at the given URL arrive.
5168
* If the timout option is configured, this function will abort waiting, even if it hasn't reveived the configured
5269
* amount of requests, and returns all the events recieved up to that point in time.
5370
*/
54-
async function getMultipleRequests(
71+
async function getMultipleRequests<T>(
5572
page: Page,
5673
count: number,
5774
urlRgx: RegExp,
58-
requestParser: (req: Request) => Event,
75+
requestParser: (req: Request) => T,
5976
options?: {
6077
url?: string;
6178
timeout?: number;
6279
},
63-
): Promise<Event[]> {
64-
const requests: Promise<Event[]> = new Promise((resolve, reject) => {
80+
): Promise<T[]> {
81+
const requests: Promise<T[]> = new Promise((resolve, reject) => {
6582
let reqCount = count;
66-
const requestData: Event[] = [];
83+
const requestData: T[] = [];
6784
let timeoutId: NodeJS.Timeout | undefined = undefined;
6885

6986
function requestHandler(request: Request): void {
@@ -115,7 +132,7 @@ async function getMultipleSentryEnvelopeRequests<T>(
115132
): Promise<T[]> {
116133
// TODO: This is not currently checking the type of envelope, just casting for now.
117134
// We can update this to include optional type-guarding when we have types for Envelope.
118-
return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise<T[]>;
135+
return getMultipleRequests<T>(page, count, envelopeUrlRegex, requestParser, options) as Promise<T[]>;
119136
}
120137

121138
/**

0 commit comments

Comments
 (0)