Skip to content

Commit ec41258

Browse files
committed
feat: Add request_body_size & response_body_size to fetch/xhr
1 parent cc26081 commit ec41258

File tree

27 files changed

+772
-131
lines changed

27 files changed

+772
-131
lines changed

packages/browser/src/integrations/breadcrumbs.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
22
/* eslint-disable max-lines */
33
import { getCurrentHub } from '@sentry/core';
4-
import type { Event as SentryEvent, HandlerDataFetch, Integration, SentryWrappedXMLHttpRequest } from '@sentry/types';
4+
import type { Event as SentryEvent, HandlerDataFetch, HandlerDataXhr, Integration } from '@sentry/types';
55
import {
66
addInstrumentationHandler,
77
getEventDescription,
@@ -10,6 +10,7 @@ import {
1010
parseUrl,
1111
safeJoin,
1212
severityLevelFromString,
13+
dropUndefinedKeys,
1314
} from '@sentry/utils';
1415

1516
import { WINDOW } from '../helpers';
@@ -216,33 +217,31 @@ function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level:
216217
/**
217218
* Creates breadcrumbs from XHR API calls
218219
*/
219-
function _xhrBreadcrumb(handlerData: HandlerData & { xhr: SentryWrappedXMLHttpRequest }): void {
220-
if (handlerData.endTimestamp) {
221-
// We only capture complete, non-sentry requests
222-
if (handlerData.xhr.__sentry_own_request__) {
223-
return;
224-
}
225-
226-
const { method, url, status_code, body } = handlerData.xhr.__sentry_xhr__ || {};
227-
228-
getCurrentHub().addBreadcrumb(
229-
{
230-
category: 'xhr',
231-
data: {
232-
method,
233-
url,
234-
status_code,
235-
},
236-
type: 'http',
237-
},
238-
{
239-
xhr: handlerData.xhr,
240-
input: body,
241-
},
242-
);
243-
220+
function _xhrBreadcrumb(handlerData: HandlerData & HandlerDataXhr): void {
221+
// We only capture complete, non-sentry requests
222+
if (!handlerData.endTimestamp || !handlerData.xhr.__sentry_xhr__) {
244223
return;
245224
}
225+
226+
const { method, url, status_code, body, request_body_size, response_body_size } = handlerData.xhr.__sentry_xhr__;
227+
228+
getCurrentHub().addBreadcrumb(
229+
{
230+
category: 'xhr',
231+
data: dropUndefinedKeys({
232+
method,
233+
url,
234+
status_code,
235+
request_body_size,
236+
response_body_size,
237+
}),
238+
type: 'http',
239+
},
240+
{
241+
xhr: handlerData.xhr,
242+
input: body,
243+
},
244+
);
246245
}
247246

248247
/**
@@ -263,7 +262,7 @@ function _fetchBreadcrumb(handlerData: HandlerData & HandlerDataFetch): void {
263262
getCurrentHub().addBreadcrumb(
264263
{
265264
category: 'fetch',
266-
data: handlerData.fetchData,
265+
data: dropUndefinedKeys(handlerData.fetchData),
267266
level: 'error',
268267
type: 'http',
269268
},
@@ -277,7 +276,7 @@ function _fetchBreadcrumb(handlerData: HandlerData & HandlerDataFetch): void {
277276
{
278277
category: 'fetch',
279278
data: {
280-
...handlerData.fetchData,
279+
...dropUndefinedKeys(handlerData.fetchData),
281280
status_code: handlerData.response && handlerData.response.status,
282281
},
283282
type: 'http',
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const xhr = new XMLHttpRequest();
2+
3+
fetch('http://localhost:7654/foo', {
4+
headers: {
5+
Accept: 'application/json',
6+
'Content-Type': 'application/json',
7+
Cache: 'no-cache',
8+
},
9+
}).then(() => {
10+
Sentry.captureException('test error');
11+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../../utils/helpers';
6+
7+
sentryTest('parses response_body_size from Content-Length header if available', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
await page.route('**/foo', route => {
11+
return route.fulfill({
12+
status: 200,
13+
body: JSON.stringify({
14+
userNames: ['John', 'Jane'],
15+
}),
16+
headers: {
17+
'Content-Type': 'application/json',
18+
'Content-Length': '789',
19+
},
20+
});
21+
});
22+
23+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
24+
25+
expect(eventData.exception?.values).toHaveLength(1);
26+
27+
expect(eventData?.breadcrumbs?.length).toBe(1);
28+
expect(eventData!.breadcrumbs![0]).toEqual({
29+
timestamp: expect.any(Number),
30+
category: 'fetch',
31+
type: 'http',
32+
data: {
33+
method: 'GET',
34+
response_body_size: 789,
35+
status_code: 200,
36+
url: 'http://localhost:7654/foo',
37+
},
38+
});
39+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const xhr = new XMLHttpRequest();
2+
3+
fetch('http://localhost:7654/foo', {
4+
headers: {
5+
Accept: 'application/json',
6+
'Content-Type': 'application/json',
7+
Cache: 'no-cache',
8+
},
9+
}).then(() => {
10+
Sentry.captureException('test error');
11+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../../utils/helpers';
6+
7+
sentryTest('adds a breadcrumb for basic fetch GET request', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
await page.route('**/foo', route => {
11+
return route.fulfill({
12+
status: 200,
13+
body: JSON.stringify({
14+
userNames: ['John', 'Jane'],
15+
}),
16+
headers: {
17+
'Content-Type': 'application/json',
18+
},
19+
});
20+
});
21+
22+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
23+
24+
expect(eventData.exception?.values).toHaveLength(1);
25+
26+
expect(eventData?.breadcrumbs?.length).toBe(1);
27+
expect(eventData!.breadcrumbs![0]).toEqual({
28+
timestamp: expect.any(Number),
29+
category: 'fetch',
30+
type: 'http',
31+
data: {
32+
method: 'GET',
33+
response_body_size: 29,
34+
status_code: 200,
35+
url: 'http://localhost:7654/foo',
36+
},
37+
});
38+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
defaultIntegrations: false,
8+
integrations: [new Sentry.Integrations.Breadcrumbs()],
9+
tracesSampleRate: 1,
10+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const xhr = new XMLHttpRequest();
2+
3+
const blob = new Blob(['<html>Hello world!!</html>'], { type: 'text/html' });
4+
5+
fetch('http://localhost:7654/foo', {
6+
method: 'POST',
7+
headers: {
8+
Accept: 'application/json',
9+
'Content-Type': 'application/json',
10+
Cache: 'no-cache',
11+
},
12+
body: blob,
13+
}).then(() => {
14+
Sentry.captureException('test error');
15+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../../utils/helpers';
6+
7+
sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
await page.route('**/foo', async route => {
11+
return route.fulfill({
12+
status: 200,
13+
body: Buffer.from('<html>Hello world</html>'),
14+
headers: {
15+
'Content-Type': 'application/json',
16+
},
17+
});
18+
});
19+
20+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
21+
22+
expect(eventData.exception?.values).toHaveLength(1);
23+
24+
expect(eventData?.breadcrumbs?.length).toBe(1);
25+
expect(eventData!.breadcrumbs![0]).toEqual({
26+
timestamp: expect.any(Number),
27+
category: 'fetch',
28+
type: 'http',
29+
data: {
30+
method: 'POST',
31+
request_body_size: 26,
32+
response_body_size: 24,
33+
status_code: 200,
34+
url: 'http://localhost:7654/foo',
35+
},
36+
});
37+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const xhr = new XMLHttpRequest();
2+
3+
fetch('http://localhost:7654/foo', {
4+
method: 'POST',
5+
headers: {
6+
Accept: 'application/json',
7+
'Content-Type': 'application/json',
8+
Cache: 'no-cache',
9+
},
10+
body: '{"foo":"bar"}',
11+
}).then(() => {
12+
Sentry.captureException('test error');
13+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../../utils/helpers';
6+
7+
sentryTest('adds a breadcrumb for basic fetch POST request', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
await page.route('**/foo', route => {
11+
return route.fulfill({
12+
status: 200,
13+
body: JSON.stringify({
14+
userNames: ['John', 'Jane'],
15+
}),
16+
headers: {
17+
'Content-Type': 'application/json',
18+
'Content-Length': '',
19+
},
20+
});
21+
});
22+
23+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
24+
25+
expect(eventData.exception?.values).toHaveLength(1);
26+
27+
expect(eventData?.breadcrumbs?.length).toBe(1);
28+
expect(eventData!.breadcrumbs![0]).toEqual({
29+
timestamp: expect.any(Number),
30+
category: 'fetch',
31+
type: 'http',
32+
data: {
33+
method: 'POST',
34+
request_body_size: 13,
35+
// No response_body_size without Content-Length header!
36+
status_code: 200,
37+
url: 'http://localhost:7654/foo',
38+
},
39+
});
40+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const xhr = new XMLHttpRequest();
2+
3+
xhr.open('GET', 'http://localhost:7654/foo', true);
4+
xhr.withCredentials = true;
5+
xhr.setRequestHeader('Accept', 'application/json');
6+
xhr.setRequestHeader('Content-Type', 'application/json');
7+
xhr.setRequestHeader('Cache', 'no-cache');
8+
xhr.send();
9+
10+
xhr.addEventListener('readystatechange', function () {
11+
if (xhr.readyState === 4) {
12+
Sentry.captureException('test error');
13+
}
14+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../../utils/helpers';
6+
7+
sentryTest('parses response_body_size from Content-Length header if available', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
await page.route('**/foo', route => {
11+
return route.fulfill({
12+
status: 200,
13+
body: JSON.stringify({
14+
userNames: ['John', 'Jane'],
15+
}),
16+
headers: {
17+
'Content-Type': 'application/json',
18+
'Content-Length': '789',
19+
},
20+
});
21+
});
22+
23+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
24+
25+
expect(eventData.exception?.values).toHaveLength(1);
26+
27+
expect(eventData?.breadcrumbs?.length).toBe(1);
28+
expect(eventData!.breadcrumbs![0]).toEqual({
29+
timestamp: expect.any(Number),
30+
category: 'xhr',
31+
type: 'http',
32+
data: {
33+
method: 'GET',
34+
response_body_size: 789,
35+
status_code: 200,
36+
url: 'http://localhost:7654/foo',
37+
},
38+
});
39+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const xhr = new XMLHttpRequest();
2+
3+
xhr.open('GET', 'http://localhost:7654/foo', true);
4+
xhr.withCredentials = true;
5+
xhr.setRequestHeader('Accept', 'application/json');
6+
xhr.setRequestHeader('Content-Type', 'application/json');
7+
xhr.setRequestHeader('Cache', 'no-cache');
8+
xhr.send();
9+
10+
xhr.addEventListener('readystatechange', function () {
11+
if (xhr.readyState === 4) {
12+
Sentry.captureException('test error');
13+
}
14+
});

0 commit comments

Comments
 (0)