Skip to content

Commit 551aedc

Browse files
mydeaLms24
andauthored
test(replay): Add replay E2E test (#7486)
--------- Co-authored-by: Lukas Stracke <[email protected]>
1 parent 383e929 commit 551aedc

File tree

4 files changed

+322
-0
lines changed

4 files changed

+322
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
interface Window {
22
recordedTransactions?: string[];
33
capturedExceptionId?: string;
4+
sentryReplayId?: string;
45
}

packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
import Index from './pages/Index';
1515
import User from './pages/User';
1616

17+
const replay = new Sentry.Replay();
18+
1719
Sentry.init({
1820
dsn: process.env.REACT_APP_E2E_TEST_DSN,
1921
integrations: [
@@ -26,11 +28,22 @@ Sentry.init({
2628
matchRoutes,
2729
),
2830
}),
31+
replay,
2932
],
3033
// We recommend adjusting this value in production, or using tracesSampler
3134
// for finer control
3235
tracesSampleRate: 1.0,
3336
release: 'e2e-test',
37+
38+
// Always capture replays, so we can test this properly
39+
replaysSessionSampleRate: 1.0,
40+
replaysOnErrorSampleRate: 0.0,
41+
});
42+
43+
Object.defineProperty(window, 'sentryReplayId', {
44+
get() {
45+
return replay['_replay'].session.id;
46+
},
3447
});
3548

3649
Sentry.addGlobalEventProcessor(event => {

packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test, expect } from '@playwright/test';
22
import axios, { AxiosError } from 'axios';
3+
import { ReplayRecordingData } from './fixtures/ReplayRecordingData';
34

45
const EVENT_POLLING_TIMEOUT = 30_000;
56

@@ -169,3 +170,85 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => {
169170

170171
expect(hadPageNavigationTransaction).toBe(true);
171172
});
173+
174+
test('Sends a Replay recording to Sentry', async ({ browser }) => {
175+
const context = await browser.newContext();
176+
const page = await context.newPage();
177+
178+
await page.goto('/');
179+
180+
const replayId = await page.waitForFunction(() => {
181+
return window.sentryReplayId;
182+
});
183+
184+
// Wait for replay to be sent
185+
186+
if (replayId === undefined) {
187+
throw new Error("Application didn't set a replayId");
188+
}
189+
190+
console.log(`Polling for replay with ID: ${replayId}`);
191+
192+
await expect
193+
.poll(
194+
async () => {
195+
try {
196+
const response = await axios.get(
197+
`https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/replays/${replayId}/`,
198+
{ headers: { Authorization: `Bearer ${authToken}` } },
199+
);
200+
201+
return response.status;
202+
} catch (e) {
203+
if (e instanceof AxiosError && e.response) {
204+
if (e.response.status !== 404) {
205+
throw e;
206+
} else {
207+
return e.response.status;
208+
}
209+
} else {
210+
throw e;
211+
}
212+
}
213+
},
214+
{
215+
timeout: EVENT_POLLING_TIMEOUT,
216+
},
217+
)
218+
.toBe(200);
219+
220+
// now fetch the first recording segment
221+
await expect
222+
.poll(
223+
async () => {
224+
try {
225+
const response = await axios.get(
226+
`https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/replays/${replayId}/recording-segments/?cursor=100%3A0%3A1`,
227+
{ headers: { Authorization: `Bearer ${authToken}` } },
228+
);
229+
230+
return {
231+
status: response.status,
232+
data: response.data,
233+
};
234+
} catch (e) {
235+
if (e instanceof AxiosError && e.response) {
236+
if (e.response.status !== 404) {
237+
throw e;
238+
} else {
239+
return e.response.status;
240+
}
241+
} else {
242+
throw e;
243+
}
244+
}
245+
},
246+
{
247+
timeout: EVENT_POLLING_TIMEOUT,
248+
},
249+
)
250+
.toEqual({
251+
status: 200,
252+
data: ReplayRecordingData,
253+
});
254+
});
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { expect } from '@playwright/test';
2+
3+
export const ReplayRecordingData = [
4+
[
5+
{ type: 4, data: { href: 'http://localhost:3000/', width: 1280, height: 720 }, timestamp: expect.any(Number) },
6+
{
7+
type: 2,
8+
data: {
9+
node: {
10+
type: 0,
11+
childNodes: [
12+
{ type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
13+
{
14+
type: 2,
15+
tagName: 'html',
16+
attributes: { lang: 'en' },
17+
childNodes: [
18+
{
19+
type: 2,
20+
tagName: 'head',
21+
attributes: {},
22+
childNodes: [
23+
{ type: 2, tagName: 'meta', attributes: { charset: 'utf-8' }, childNodes: [], id: 5 },
24+
{
25+
type: 2,
26+
tagName: 'meta',
27+
attributes: { name: 'viewport', content: 'width=device-width,initial-scale=1' },
28+
childNodes: [],
29+
id: 6,
30+
},
31+
{
32+
type: 2,
33+
tagName: 'meta',
34+
attributes: { name: 'theme-color', content: '#000000' },
35+
childNodes: [],
36+
id: 7,
37+
},
38+
{
39+
type: 2,
40+
tagName: 'title',
41+
attributes: {},
42+
childNodes: [{ type: 3, textContent: '***** ***', id: 9 }],
43+
id: 8,
44+
},
45+
],
46+
id: 4,
47+
},
48+
{
49+
type: 2,
50+
tagName: 'body',
51+
attributes: {},
52+
childNodes: [
53+
{
54+
type: 2,
55+
tagName: 'noscript',
56+
attributes: {},
57+
childNodes: [{ type: 3, textContent: '*** **** ** ****** ********** ** *** **** ****', id: 12 }],
58+
id: 11,
59+
},
60+
{ type: 2, tagName: 'div', attributes: { id: 'root' }, childNodes: [], id: 13 },
61+
],
62+
id: 10,
63+
},
64+
],
65+
id: 3,
66+
},
67+
],
68+
id: 1,
69+
},
70+
initialOffset: { left: 0, top: 0 },
71+
},
72+
timestamp: expect.any(Number),
73+
},
74+
{
75+
type: 5,
76+
timestamp: expect.any(Number),
77+
data: {
78+
tag: 'performanceSpan',
79+
payload: {
80+
op: 'memory',
81+
description: 'memory',
82+
startTimestamp: expect.any(Number),
83+
endTimestamp: expect.any(Number),
84+
data: {
85+
memory: {
86+
jsHeapSizeLimit: expect.any(Number),
87+
totalJSHeapSize: expect.any(Number),
88+
usedJSHeapSize: expect.any(Number),
89+
},
90+
},
91+
},
92+
},
93+
},
94+
{
95+
type: 3,
96+
data: {
97+
source: 0,
98+
texts: [],
99+
attributes: [],
100+
removes: [],
101+
adds: [
102+
{
103+
parentId: 13,
104+
nextId: null,
105+
node: {
106+
type: 2,
107+
tagName: 'a',
108+
attributes: { id: 'navigation', href: 'http://localhost:3000/user/5' },
109+
childNodes: [],
110+
id: 14,
111+
},
112+
},
113+
{ parentId: 14, nextId: null, node: { type: 3, textContent: '********', id: 15 } },
114+
{
115+
parentId: 13,
116+
nextId: 14,
117+
node: {
118+
type: 2,
119+
tagName: 'input',
120+
attributes: { type: 'button', id: 'exception-button', value: '******* *********' },
121+
childNodes: [],
122+
id: 16,
123+
},
124+
},
125+
],
126+
},
127+
timestamp: expect.any(Number),
128+
},
129+
{
130+
type: 3,
131+
data: { source: 5, text: 'Capture Exception', isChecked: false, id: 16 },
132+
timestamp: expect.any(Number),
133+
},
134+
],
135+
[
136+
{
137+
type: 5,
138+
timestamp: expect.any(Number),
139+
data: {
140+
tag: 'performanceSpan',
141+
payload: {
142+
op: 'navigation.navigate',
143+
description: 'http://localhost:3000/',
144+
startTimestamp: expect.any(Number),
145+
endTimestamp: expect.any(Number),
146+
data: { size: expect.any(Number), duration: expect.any(Number) },
147+
},
148+
},
149+
},
150+
{
151+
type: 5,
152+
timestamp: expect.any(Number),
153+
data: {
154+
tag: 'performanceSpan',
155+
payload: {
156+
op: 'resource.script',
157+
description: expect.stringMatching(/http:\/\/localhost:3000\/static\/js\/main.(\w+).js/),
158+
startTimestamp: expect.any(Number),
159+
endTimestamp: expect.any(Number),
160+
data: { size: expect.any(Number), encodedBodySize: expect.any(Number) },
161+
},
162+
},
163+
},
164+
{
165+
type: 5,
166+
timestamp: expect.any(Number),
167+
data: {
168+
tag: 'performanceSpan',
169+
payload: {
170+
op: 'largest-contentful-paint',
171+
description: 'largest-contentful-paint',
172+
startTimestamp: expect.any(Number),
173+
endTimestamp: expect.any(Number),
174+
data: { value: expect.any(Number), size: expect.any(Number), nodeId: 16 },
175+
},
176+
},
177+
},
178+
{
179+
type: 5,
180+
timestamp: expect.any(Number),
181+
data: {
182+
tag: 'performanceSpan',
183+
payload: {
184+
op: 'paint',
185+
description: 'first-paint',
186+
startTimestamp: expect.any(Number),
187+
endTimestamp: expect.any(Number),
188+
},
189+
},
190+
},
191+
{
192+
type: 5,
193+
timestamp: expect.any(Number),
194+
data: {
195+
tag: 'performanceSpan',
196+
payload: {
197+
op: 'paint',
198+
description: 'first-contentful-paint',
199+
startTimestamp: expect.any(Number),
200+
endTimestamp: expect.any(Number),
201+
},
202+
},
203+
},
204+
{
205+
type: 5,
206+
timestamp: expect.any(Number),
207+
data: {
208+
tag: 'performanceSpan',
209+
payload: {
210+
op: 'memory',
211+
description: 'memory',
212+
startTimestamp: expect.any(Number),
213+
endTimestamp: expect.any(Number),
214+
data: {
215+
memory: {
216+
jsHeapSizeLimit: expect.any(Number),
217+
totalJSHeapSize: expect.any(Number),
218+
usedJSHeapSize: expect.any(Number),
219+
},
220+
},
221+
},
222+
},
223+
},
224+
],
225+
];

0 commit comments

Comments
 (0)