Skip to content

Commit 4e9f785

Browse files
committed
test(replay): Add basic replay integration tests
1 parent f432d09 commit 4e9f785

File tree

7 files changed

+165
-7
lines changed

7 files changed

+165
-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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
10+
return route.fulfill({
11+
status: 200,
12+
contentType: 'application/json',
13+
body: JSON.stringify({ id: 'test-id' }),
14+
});
15+
});
16+
17+
const url = await getLocalTestPath({ testDir: __dirname });
18+
await page.goto(url);
19+
20+
await page.click('button');
21+
await page.waitForTimeout(200);
22+
23+
const replayEvent = await getFirstSentryEnvelopeRequest<Event>(page, url);
24+
25+
expect(replayEvent).toBeDefined();
26+
expect(replayEvent).toEqual({
27+
type: 'replay_event',
28+
timestamp: expect.any(Number),
29+
error_ids: [],
30+
trace_ids: [],
31+
urls: [expect.stringContaining('/dist/index.html')],
32+
replay_id: expect.stringMatching(/\w{32}/),
33+
segment_id: 2,
34+
replay_type: 'session',
35+
event_id: expect.stringMatching(/\w{32}/),
36+
environment: 'production',
37+
sdk: {
38+
integrations: [
39+
'InboundFilters',
40+
'FunctionToString',
41+
'TryCatch',
42+
'Breadcrumbs',
43+
'GlobalHandlers',
44+
'LinkedErrors',
45+
'Dedupe',
46+
'HttpContext',
47+
'Replay',
48+
],
49+
version: SDK_VERSION,
50+
name: 'sentry.javascript.browser',
51+
},
52+
sdkProcessingMetadata: {},
53+
request: {
54+
url: expect.stringContaining('/dist/index.html'),
55+
headers: {
56+
'User-Agent': expect.stringContaining(''),
57+
},
58+
},
59+
platform: 'javascript',
60+
tags: { sessionSampleRate: 1, errorSampleRate: 0 },
61+
});
62+
});
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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
8+
// This should never be called!
9+
expect(true).toBe(false);
10+
11+
return route.fulfill({
12+
status: 200,
13+
contentType: 'application/json',
14+
body: JSON.stringify({ id: 'test-id' }),
15+
});
16+
});
17+
18+
const url = await getLocalTestPath({ testDir: __dirname });
19+
await page.goto(url);
20+
21+
await page.click('button');
22+
await page.waitForTimeout(200);
23+
24+
const replay = await getReplaySnapshot(page);
25+
26+
expect(replay.session?.sampled).toBe(false);
27+
28+
// Cannot wait on getFirstSentryEnvelopeRequest, as that never resolves
29+
});

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)