Skip to content

Commit 8d07e33

Browse files
committed
feat: Add request_body_size & response_body_size to fetch/xhr
1 parent e358e16 commit 8d07e33

File tree

33 files changed

+938
-131
lines changed

33 files changed

+938
-131
lines changed

packages/browser/src/integrations/breadcrumbs.ts

Lines changed: 21 additions & 25 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,
@@ -216,33 +216,29 @@ function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level:
216216
/**
217217
* Creates breadcrumbs from XHR API calls
218218
*/
219-
function _xhrBreadcrumb(handlerData: HandlerData & { xhr: XMLHttpRequest & SentryWrappedXMLHttpRequest }): void {
220-
if (handlerData.endTimestamp) {
221-
// We only capture complete, non-sentry requests
222-
if (handlerData.xhr.__sentry_own_request__) {
223-
return;
224-
}
219+
function _xhrBreadcrumb(handlerData: HandlerData & HandlerDataXhr): void {
220+
// We only capture complete, non-sentry requests
221+
if (!handlerData.endTimestamp || !handlerData.xhr.__sentry_xhr__) {
222+
return;
223+
}
225224

226-
const { method, url, status_code, body } = handlerData.xhr.__sentry_xhr__ || {};
225+
const { method, url, status_code, body } = handlerData.xhr.__sentry_xhr__;
227226

228-
getCurrentHub().addBreadcrumb(
229-
{
230-
category: 'xhr',
231-
data: {
232-
method,
233-
url,
234-
status_code,
235-
},
236-
type: 'http',
227+
getCurrentHub().addBreadcrumb(
228+
{
229+
category: 'xhr',
230+
data: {
231+
method,
232+
url,
233+
status_code,
237234
},
238-
{
239-
xhr: handlerData.xhr,
240-
input: body,
241-
},
242-
);
243-
244-
return;
245-
}
235+
type: 'http',
236+
},
237+
{
238+
xhr: handlerData.xhr,
239+
input: body,
240+
},
241+
);
246242
}
247243

248244
/**

packages/core/src/baseclient.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable max-lines */
22
import type {
3+
Breadcrumb,
4+
BreadcrumbHint,
35
Client,
46
ClientOptions,
57
DataCategory,
@@ -363,6 +365,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
363365
/** @inheritdoc */
364366
public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void;
365367

368+
/** @inheritdoc */
369+
public on(hook: 'beforeBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void;
370+
366371
/** @inheritdoc */
367372
public on(hook: string, callback: unknown): void {
368373
if (!this._hooks[hook]) {
@@ -379,6 +384,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
379384
/** @inheritdoc */
380385
public emit(hook: 'beforeEnvelope', envelope: Envelope): void;
381386

387+
/** @inheritdoc */
388+
public emit(hook: 'beforeBreadcrumb', breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void;
389+
382390
/** @inheritdoc */
383391
public emit(hook: string, ...rest: unknown[]): void {
384392
if (this._hooks[hook]) {

packages/core/src/hub.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ export class Hub implements HubInterface {
271271

272272
if (finalBreadcrumb === null) return;
273273

274+
if (client.emit) {
275+
client.emit('beforeBreadcrumb', finalBreadcrumb, hint);
276+
}
277+
274278
scope.addBreadcrumb(finalBreadcrumb, maxBreadcrumbs);
275279
}
276280

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+
});
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { ExtendedNetworkBreadcrumbs } from '@sentry/integrations';
3+
4+
window.Sentry = Sentry;
5+
6+
Sentry.init({
7+
dsn: 'https://[email protected]/1337',
8+
defaultIntegrations: false,
9+
integrations: [new Sentry.Integrations.Breadcrumbs(), new ExtendedNetworkBreadcrumbs()],
10+
sampleRate: 1,
11+
});
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+
});
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+
});
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)