Skip to content

Commit 0fdeb46

Browse files
committed
feat(replay): Add experiment to capture request/response bodies
1 parent c4fbb1f commit 0fdeb46

File tree

25 files changed

+1197
-309
lines changed

25 files changed

+1197
-309
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
_experiments: {
8+
captureNetworkBodies: true,
9+
},
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
sampleRate: 1,
15+
// We ensure to sample for errors, so by default nothing is sent
16+
replaysSessionSampleRate: 0.0,
17+
replaysOnErrorSampleRate: 1.0,
18+
19+
integrations: [window.Replay],
20+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
5+
import {
6+
getCustomRecordingEvents,
7+
shouldSkipReplayTest,
8+
waitForReplayRequest,
9+
} from '../../../../../utils/replayHelpers';
10+
11+
sentryTest('captures requestBody & responseBody when experiment is configured', async ({ getLocalTestPath, page }) => {
12+
if (shouldSkipReplayTest()) {
13+
sentryTest.skip();
14+
}
15+
16+
await page.route('**/foo', route => {
17+
return route.fulfill({
18+
status: 200,
19+
body: JSON.stringify({ res: 'this' }),
20+
headers: {
21+
'Content-Type': 'application/json',
22+
},
23+
});
24+
});
25+
26+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
27+
return route.fulfill({
28+
status: 200,
29+
contentType: 'application/json',
30+
body: JSON.stringify({ id: 'test-id' }),
31+
});
32+
});
33+
34+
const requestPromise = waitForErrorRequest(page);
35+
const replayRequestPromise1 = waitForReplayRequest(page, 0);
36+
37+
const url = await getLocalTestPath({ testDir: __dirname });
38+
await page.goto(url);
39+
40+
await page.evaluate(() => {
41+
/* eslint-disable */
42+
fetch('http://localhost:7654/foo', {
43+
method: 'POST',
44+
headers: {
45+
Accept: 'application/json',
46+
'Content-Type': 'application/json',
47+
Cache: 'no-cache',
48+
},
49+
body: '{"foo":"bar"}',
50+
}).then(() => {
51+
// @ts-ignore Sentry is a global
52+
Sentry.captureException('test error');
53+
});
54+
/* eslint-enable */
55+
});
56+
57+
const request = await requestPromise;
58+
const eventData = envelopeRequestParser(request);
59+
60+
expect(eventData.exception?.values).toHaveLength(1);
61+
62+
expect(eventData?.breadcrumbs?.length).toBe(1);
63+
expect(eventData!.breadcrumbs![0]).toEqual({
64+
timestamp: expect.any(Number),
65+
category: 'fetch',
66+
type: 'http',
67+
data: {
68+
method: 'POST',
69+
request_body_size: 13,
70+
response_body_size: 14,
71+
status_code: 200,
72+
url: 'http://localhost:7654/foo',
73+
},
74+
});
75+
76+
const replayReq1 = await replayRequestPromise1;
77+
const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
78+
expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
79+
{
80+
data: {
81+
method: 'POST',
82+
requestBodySize: 13,
83+
responseBodySize: 14,
84+
requestBody: '{"foo":"bar"}',
85+
responseBody: '{"res":"this"}',
86+
statusCode: 200,
87+
},
88+
description: 'http://localhost:7654/foo',
89+
endTimestamp: expect.any(Number),
90+
op: 'resource.fetch',
91+
startTimestamp: expect.any(Number),
92+
},
93+
]);
94+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
_experiments: {
8+
captureNetworkBodies: true,
9+
},
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
sampleRate: 1,
15+
// We ensure to sample for errors, so by default nothing is sent
16+
replaysSessionSampleRate: 0.0,
17+
replaysOnErrorSampleRate: 1.0,
18+
19+
integrations: [window.Replay],
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
5+
import {
6+
getCustomRecordingEvents,
7+
shouldSkipReplayTest,
8+
waitForReplayRequest,
9+
} from '../../../../../utils/replayHelpers';
10+
11+
sentryTest(
12+
'captures non-text fetch requestBody & responseBody when experiment is configured',
13+
async ({ getLocalTestPath, page }) => {
14+
if (shouldSkipReplayTest()) {
15+
sentryTest.skip();
16+
}
17+
18+
await page.route('**/foo', route => {
19+
return route.fulfill({
20+
status: 200,
21+
body: Buffer.from('<html>Hello world</html>'),
22+
headers: {
23+
'Content-Type': 'application/json',
24+
},
25+
});
26+
});
27+
28+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
29+
return route.fulfill({
30+
status: 200,
31+
contentType: 'application/json',
32+
body: JSON.stringify({ id: 'test-id' }),
33+
});
34+
});
35+
36+
const requestPromise = waitForErrorRequest(page);
37+
const replayRequestPromise1 = waitForReplayRequest(page, 0);
38+
39+
const url = await getLocalTestPath({ testDir: __dirname });
40+
await page.goto(url);
41+
42+
await page.evaluate(() => {
43+
const body = new URLSearchParams();
44+
body.append('name', 'Anne');
45+
body.append('age', '32');
46+
47+
/* eslint-disable */
48+
fetch('http://localhost:7654/foo', {
49+
method: 'POST',
50+
headers: {
51+
Accept: 'application/json',
52+
Cache: 'no-cache',
53+
},
54+
body: body,
55+
}).then(() => {
56+
// @ts-ignore Sentry is a global
57+
Sentry.captureException('test error');
58+
});
59+
/* eslint-enable */
60+
});
61+
62+
const request = await requestPromise;
63+
const eventData = envelopeRequestParser(request);
64+
65+
expect(eventData.exception?.values).toHaveLength(1);
66+
67+
expect(eventData?.breadcrumbs?.length).toBe(1);
68+
expect(eventData!.breadcrumbs![0]).toEqual({
69+
timestamp: expect.any(Number),
70+
category: 'fetch',
71+
type: 'http',
72+
data: {
73+
method: 'POST',
74+
request_body_size: 16,
75+
response_body_size: 24,
76+
status_code: 200,
77+
url: 'http://localhost:7654/foo',
78+
},
79+
});
80+
81+
const replayReq1 = await replayRequestPromise1;
82+
const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
83+
expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
84+
{
85+
data: {
86+
method: 'POST',
87+
requestBodySize: 16,
88+
responseBodySize: 24,
89+
requestBody: 'name=Anne&age=32',
90+
responseBody: '<html>Hello world</html>',
91+
statusCode: 200,
92+
},
93+
description: 'http://localhost:7654/foo',
94+
endTimestamp: expect.any(Number),
95+
op: 'resource.fetch',
96+
startTimestamp: expect.any(Number),
97+
},
98+
]);
99+
},
100+
);

packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts renamed to packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBodySize/test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
waitForReplayRequest,
99
} from '../../../../../utils/replayHelpers';
1010

11-
sentryTest('captures request_body_size when body is sent', async ({ getLocalTestPath, page }) => {
11+
sentryTest('captures requestBodySize when body is sent', async ({ getLocalTestPath, page }) => {
1212
if (shouldSkipReplayTest()) {
1313
sentryTest.skip();
1414
}
@@ -70,7 +70,6 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
7070
url: 'http://localhost:7654/foo',
7171
},
7272
});
73-
expect(eventData!.breadcrumbs![0].data!.request_body_size).toEqual(13);
7473

7574
const replayReq1 = await replayRequestPromise1;
7675
const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
_experiments: {
8+
captureNetworkBodies: true,
9+
},
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
sampleRate: 1,
15+
// We ensure to sample for errors, so by default nothing is sent
16+
replaysSessionSampleRate: 0.0,
17+
replaysOnErrorSampleRate: 1.0,
18+
19+
integrations: [window.Replay],
20+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
5+
import {
6+
getCustomRecordingEvents,
7+
shouldSkipReplayTest,
8+
waitForReplayRequest,
9+
} from '../../../../../utils/replayHelpers';
10+
11+
sentryTest(
12+
'captures xhr requestBody & responseBody when experiment is configured',
13+
async ({ getLocalTestPath, page, browserName }) => {
14+
// These are a bit flaky on non-chromium browsers
15+
if (shouldSkipReplayTest() || browserName !== 'chromium') {
16+
sentryTest.skip();
17+
}
18+
19+
await page.route('**/foo', route => {
20+
return route.fulfill({
21+
status: 200,
22+
body: JSON.stringify({ res: 'this' }),
23+
headers: {
24+
'Content-Type': 'application/json',
25+
'Content-Length': '',
26+
},
27+
});
28+
});
29+
30+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
31+
return route.fulfill({
32+
status: 200,
33+
contentType: 'application/json',
34+
body: JSON.stringify({ id: 'test-id' }),
35+
});
36+
});
37+
38+
const requestPromise = waitForErrorRequest(page);
39+
const replayRequestPromise1 = waitForReplayRequest(page, 0);
40+
41+
const url = await getLocalTestPath({ testDir: __dirname });
42+
await page.goto(url);
43+
44+
void page.evaluate(() => {
45+
/* eslint-disable */
46+
const xhr = new XMLHttpRequest();
47+
48+
xhr.open('POST', 'http://localhost:7654/foo');
49+
xhr.setRequestHeader('Accept', 'application/json');
50+
xhr.setRequestHeader('Content-Type', 'application/json');
51+
xhr.setRequestHeader('Cache', 'no-cache');
52+
xhr.send('{"foo":"bar"}');
53+
54+
xhr.addEventListener('readystatechange', function () {
55+
if (xhr.readyState === 4) {
56+
// @ts-ignore Sentry is a global
57+
setTimeout(() => Sentry.captureException('test error', 0));
58+
}
59+
});
60+
/* eslint-enable */
61+
});
62+
63+
const request = await requestPromise;
64+
const eventData = envelopeRequestParser(request);
65+
66+
expect(eventData.exception?.values).toHaveLength(1);
67+
68+
expect(eventData?.breadcrumbs?.length).toBe(1);
69+
expect(eventData!.breadcrumbs![0]).toEqual({
70+
timestamp: expect.any(Number),
71+
category: 'xhr',
72+
type: 'http',
73+
data: {
74+
method: 'POST',
75+
request_body_size: 13,
76+
response_body_size: 14,
77+
status_code: 200,
78+
url: 'http://localhost:7654/foo',
79+
},
80+
});
81+
82+
const replayReq1 = await replayRequestPromise1;
83+
const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
84+
expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
85+
{
86+
data: {
87+
method: 'POST',
88+
requestBodySize: 13,
89+
responseBodySize: 14,
90+
requestBody: '{"foo":"bar"}',
91+
responseBody: '{"res":"this"}',
92+
statusCode: 200,
93+
},
94+
description: 'http://localhost:7654/foo',
95+
endTimestamp: expect.any(Number),
96+
op: 'resource.xhr',
97+
startTimestamp: expect.any(Number),
98+
},
99+
]);
100+
},
101+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
_experiments: {
8+
captureNetworkBodies: true,
9+
},
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
sampleRate: 1,
15+
// We ensure to sample for errors, so by default nothing is sent
16+
replaysSessionSampleRate: 0.0,
17+
replaysOnErrorSampleRate: 1.0,
18+
19+
integrations: [window.Replay],
20+
});

0 commit comments

Comments
 (0)