Skip to content

Commit 496fdbc

Browse files
committed
feat(replay): Add experiment to capture request/response bodies
1 parent 849297a commit 496fdbc

File tree

37 files changed

+1897
-345
lines changed

37 files changed

+1897
-345
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: 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 text 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: 'response body',
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+
/* eslint-disable */
44+
fetch('http://localhost:7654/foo', {
45+
method: 'POST',
46+
headers: {
47+
Accept: 'application/json',
48+
'Content-Type': 'application/json',
49+
Cache: 'no-cache',
50+
},
51+
body: 'input body',
52+
}).then(() => {
53+
// @ts-ignore Sentry is a global
54+
Sentry.captureException('test error');
55+
});
56+
/* eslint-enable */
57+
});
58+
59+
const request = await requestPromise;
60+
const eventData = envelopeRequestParser(request);
61+
62+
expect(eventData.exception?.values).toHaveLength(1);
63+
64+
expect(eventData?.breadcrumbs?.length).toBe(1);
65+
expect(eventData!.breadcrumbs![0]).toEqual({
66+
timestamp: expect.any(Number),
67+
category: 'fetch',
68+
type: 'http',
69+
data: {
70+
method: 'POST',
71+
request_body_size: 10,
72+
response_body_size: 13,
73+
status_code: 200,
74+
url: 'http://localhost:7654/foo',
75+
},
76+
});
77+
78+
const replayReq1 = await replayRequestPromise1;
79+
const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
80+
expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
81+
{
82+
data: {
83+
method: 'POST',
84+
statusCode: 200,
85+
request: {
86+
size: 10,
87+
body: 'input body',
88+
},
89+
response: {
90+
size: 13,
91+
body: 'response body',
92+
},
93+
},
94+
description: 'http://localhost:7654/foo',
95+
endTimestamp: expect.any(Number),
96+
op: 'resource.fetch',
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+
});
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 JSON 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: JSON.stringify({ res: 'this' }),
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+
/* eslint-disable */
44+
fetch('http://localhost:7654/foo', {
45+
method: 'POST',
46+
headers: {
47+
Accept: 'application/json',
48+
'Content-Type': 'application/json',
49+
Cache: 'no-cache',
50+
},
51+
body: '{"foo":"bar"}',
52+
}).then(() => {
53+
// @ts-ignore Sentry is a global
54+
Sentry.captureException('test error');
55+
});
56+
/* eslint-enable */
57+
});
58+
59+
const request = await requestPromise;
60+
const eventData = envelopeRequestParser(request);
61+
62+
expect(eventData.exception?.values).toHaveLength(1);
63+
64+
expect(eventData?.breadcrumbs?.length).toBe(1);
65+
expect(eventData!.breadcrumbs![0]).toEqual({
66+
timestamp: expect.any(Number),
67+
category: 'fetch',
68+
type: 'http',
69+
data: {
70+
method: 'POST',
71+
request_body_size: 13,
72+
response_body_size: 14,
73+
status_code: 200,
74+
url: 'http://localhost:7654/foo',
75+
},
76+
});
77+
78+
const replayReq1 = await replayRequestPromise1;
79+
const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
80+
expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
81+
{
82+
data: {
83+
method: 'POST',
84+
statusCode: 200,
85+
request: {
86+
size: 13,
87+
body: { foo: 'bar' },
88+
},
89+
response: {
90+
size: 14,
91+
body: { res: 'this' },
92+
},
93+
},
94+
description: 'http://localhost:7654/foo',
95+
endTimestamp: expect.any(Number),
96+
op: 'resource.fetch',
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+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
statusCode: 200,
88+
request: {
89+
size: 16,
90+
body: 'name=Anne&age=32',
91+
},
92+
response: {
93+
size: 24,
94+
body: '<html>Hello world</html>',
95+
},
96+
},
97+
description: 'http://localhost:7654/foo',
98+
endTimestamp: expect.any(Number),
99+
op: 'resource.fetch',
100+
startTimestamp: expect.any(Number),
101+
},
102+
]);
103+
},
104+
);

packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,10 @@ sentryTest('parses response_body_size from Content-Length header if available',
7979
{
8080
data: {
8181
method: 'GET',
82-
responseBodySize: 789,
8382
statusCode: 200,
83+
response: {
84+
size: 789,
85+
},
8486
},
8587
description: 'http://localhost:7654/foo',
8688
endTimestamp: expect.any(Number),

packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ sentryTest('does not capture response_body_size without Content-Length header',
7878
{
7979
data: {
8080
method: 'GET',
81-
responseBodySize: 29,
8281
statusCode: 200,
82+
response: {
83+
size: 29,
84+
},
8385
},
8486
description: 'http://localhost:7654/foo',
8587
endTimestamp: expect.any(Number),

0 commit comments

Comments
 (0)