Skip to content

Commit 1f898b6

Browse files
Zen-croniclforst
andauthored
feat: Set log level for Fetch/XHR breadcrumbs based on status code (#13711)
Fixes #13359 - [x] If you've added code that should be tested, please add tests. - [x] Ensure your code lints and the test suite passes (`yarn lint`) & (`yarn test`). --------- Signed-off-by: Kaung Zin Hein <[email protected]> Co-authored-by: Luca Forstner <[email protected]>
1 parent c0a5a3e commit 1f898b6

File tree

14 files changed

+236
-9
lines changed

14 files changed

+236
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fetch('http://sentry-test.io/foo').then(() => {
2+
Sentry.captureException('test error');
3+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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('captures Breadcrumb with log level for 4xx response code', async ({ getLocalTestUrl, page }) => {
8+
const url = await getLocalTestUrl({ testDir: __dirname });
9+
10+
await page.route('**/foo', async route => {
11+
await route.fulfill({
12+
status: 404,
13+
contentType: 'text/plain',
14+
body: 'Not Found!',
15+
});
16+
});
17+
18+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
19+
20+
expect(eventData.exception?.values).toHaveLength(1);
21+
22+
expect(eventData?.breadcrumbs?.length).toBe(1);
23+
expect(eventData!.breadcrumbs![0]).toEqual({
24+
timestamp: expect.any(Number),
25+
category: 'fetch',
26+
type: 'http',
27+
data: {
28+
method: 'GET',
29+
status_code: 404,
30+
url: 'http://sentry-test.io/foo',
31+
},
32+
level: 'warning',
33+
});
34+
35+
await page.route('**/foo', async route => {
36+
await route.fulfill({
37+
status: 500,
38+
contentType: 'text/plain',
39+
body: 'Internal Server Error',
40+
});
41+
});
42+
});
43+
44+
sentryTest('captures Breadcrumb with log level for 5xx response code', async ({ getLocalTestUrl, page }) => {
45+
const url = await getLocalTestUrl({ testDir: __dirname });
46+
47+
await page.route('**/foo', async route => {
48+
await route.fulfill({
49+
status: 500,
50+
contentType: 'text/plain',
51+
body: 'Internal Server Error',
52+
});
53+
});
54+
55+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
56+
57+
expect(eventData.exception?.values).toHaveLength(1);
58+
59+
expect(eventData?.breadcrumbs?.length).toBe(1);
60+
expect(eventData!.breadcrumbs![0]).toEqual({
61+
timestamp: expect.any(Number),
62+
category: 'fetch',
63+
type: 'http',
64+
data: {
65+
method: 'GET',
66+
status_code: 500,
67+
url: 'http://sentry-test.io/foo',
68+
},
69+
level: 'error',
70+
});
71+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const xhr = new XMLHttpRequest();
2+
3+
xhr.open('GET', 'http://sentry-test.io/foo');
4+
xhr.send();
5+
6+
xhr.addEventListener('readystatechange', function () {
7+
if (xhr.readyState === 4) {
8+
Sentry.captureException('test error');
9+
}
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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('captures Breadcrumb with log level for 4xx response code', async ({ getLocalTestUrl, page }) => {
8+
const url = await getLocalTestUrl({ testDir: __dirname });
9+
10+
await page.route('**/foo', async route => {
11+
await route.fulfill({
12+
status: 404,
13+
contentType: 'text/plain',
14+
body: 'Not Found!',
15+
});
16+
});
17+
18+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
19+
20+
expect(eventData.exception?.values).toHaveLength(1);
21+
22+
expect(eventData?.breadcrumbs?.length).toBe(1);
23+
expect(eventData!.breadcrumbs![0]).toEqual({
24+
timestamp: expect.any(Number),
25+
category: 'xhr',
26+
type: 'http',
27+
data: {
28+
method: 'GET',
29+
status_code: 404,
30+
url: 'http://sentry-test.io/foo',
31+
},
32+
level: 'warning',
33+
});
34+
35+
await page.route('**/foo', async route => {
36+
await route.fulfill({
37+
status: 500,
38+
contentType: 'text/plain',
39+
body: 'Internal Server Error',
40+
});
41+
});
42+
});
43+
44+
sentryTest('captures Breadcrumb with log level for 5xx response code', async ({ getLocalTestUrl, page }) => {
45+
const url = await getLocalTestUrl({ testDir: __dirname });
46+
47+
await page.route('**/foo', async route => {
48+
await route.fulfill({
49+
status: 500,
50+
contentType: 'text/plain',
51+
body: 'Internal Server Error',
52+
});
53+
});
54+
55+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
56+
57+
expect(eventData.exception?.values).toHaveLength(1);
58+
59+
expect(eventData?.breadcrumbs?.length).toBe(1);
60+
expect(eventData!.breadcrumbs![0]).toEqual({
61+
timestamp: expect.any(Number),
62+
category: 'xhr',
63+
type: 'http',
64+
data: {
65+
method: 'GET',
66+
status_code: 500,
67+
url: 'http://sentry-test.io/foo',
68+
},
69+
level: 'error',
70+
});
71+
});

packages/browser/src/integrations/breadcrumbs.ts

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
import {
2424
addConsoleInstrumentationHandler,
2525
addFetchInstrumentationHandler,
26+
getBreadcrumbLogLevelFromHttpStatusCode,
2627
getComponentName,
2728
getEventDescription,
2829
htmlTreeAsString,
@@ -247,11 +248,14 @@ function _getXhrBreadcrumbHandler(client: Client): (handlerData: HandlerDataXhr)
247248
endTimestamp,
248249
};
249250

251+
const level = getBreadcrumbLogLevelFromHttpStatusCode(status_code);
252+
250253
addBreadcrumb(
251254
{
252255
category: 'xhr',
253256
data,
254257
type: 'http',
258+
level,
255259
},
256260
hint,
257261
);
@@ -309,11 +313,14 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe
309313
startTimestamp,
310314
endTimestamp,
311315
};
316+
const level = getBreadcrumbLogLevelFromHttpStatusCode(data.status_code);
317+
312318
addBreadcrumb(
313319
{
314320
category: 'fetch',
315321
data,
316322
type: 'http',
323+
level,
317324
},
318325
hint,
319326
);

packages/cloudflare/src/integrations/fetch.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import type {
77
IntegrationFn,
88
Span,
99
} from '@sentry/types';
10-
import { LRUMap, addFetchInstrumentationHandler, stringMatchesSomePattern } from '@sentry/utils';
10+
import {
11+
LRUMap,
12+
addFetchInstrumentationHandler,
13+
getBreadcrumbLogLevelFromHttpStatusCode,
14+
stringMatchesSomePattern,
15+
} from '@sentry/utils';
1116

1217
const INTEGRATION_NAME = 'Fetch';
1318

@@ -144,11 +149,14 @@ function createBreadcrumb(handlerData: HandlerDataFetch): void {
144149
startTimestamp,
145150
endTimestamp,
146151
};
152+
const level = getBreadcrumbLogLevelFromHttpStatusCode(data.status_code);
153+
147154
addBreadcrumb(
148155
{
149156
category: 'fetch',
150157
data,
151158
type: 'http',
159+
level,
152160
},
153161
hint,
154162
);

packages/deno/src/integrations/breadcrumbs.ts

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
import {
1212
addConsoleInstrumentationHandler,
1313
addFetchInstrumentationHandler,
14+
getBreadcrumbLogLevelFromHttpStatusCode,
1415
getEventDescription,
1516
safeJoin,
1617
severityLevelFromString,
@@ -178,11 +179,14 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe
178179
startTimestamp,
179180
endTimestamp,
180181
};
182+
const level = getBreadcrumbLogLevelFromHttpStatusCode(data.status_code);
183+
181184
addBreadcrumb(
182185
{
183186
category: 'fetch',
184187
data,
185188
type: 'http',
189+
level,
186190
},
187191
hint,
188192
);

packages/node/src/integrations/http.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import {
1515
import { getClient } from '@sentry/opentelemetry';
1616
import type { IntegrationFn, SanitizedRequestData } from '@sentry/types';
1717

18-
import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils';
18+
import {
19+
getBreadcrumbLogLevelFromHttpStatusCode,
20+
getSanitizedUrlString,
21+
parseUrl,
22+
stripUrlQueryAndFragment,
23+
} from '@sentry/utils';
1924
import type { NodeClient } from '../sdk/client';
2025
import { setIsolationScope } from '../sdk/scope';
2126
import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module';
@@ -243,14 +248,18 @@ function _addRequestBreadcrumb(
243248
}
244249

245250
const data = getBreadcrumbData(request);
251+
const statusCode = response.statusCode;
252+
const level = getBreadcrumbLogLevelFromHttpStatusCode(statusCode);
253+
246254
addBreadcrumb(
247255
{
248256
category: 'http',
249257
data: {
250-
status_code: response.statusCode,
258+
status_code: statusCode,
251259
...data,
252260
},
253261
type: 'http',
262+
level,
254263
},
255264
{
256265
event: 'response',

packages/node/src/integrations/node-fetch.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
33
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, addBreadcrumb, defineIntegration } from '@sentry/core';
44
import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry';
55
import type { IntegrationFn, SanitizedRequestData } from '@sentry/types';
6-
import { getSanitizedUrlString, parseUrl } from '@sentry/utils';
6+
import { getBreadcrumbLogLevelFromHttpStatusCode, getSanitizedUrlString, parseUrl } from '@sentry/utils';
77

88
interface NodeFetchOptions {
99
/**
@@ -56,15 +56,18 @@ export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchInte
5656
/** Add a breadcrumb for outgoing requests. */
5757
function addRequestBreadcrumb(request: UndiciRequest, response: UndiciResponse): void {
5858
const data = getBreadcrumbData(request);
59+
const statusCode = response.statusCode;
60+
const level = getBreadcrumbLogLevelFromHttpStatusCode(statusCode);
5961

6062
addBreadcrumb(
6163
{
6264
category: 'http',
6365
data: {
64-
status_code: response.statusCode,
66+
status_code: statusCode,
6567
...data,
6668
},
6769
type: 'http',
70+
level,
6871
},
6972
{
7073
event: 'response',

packages/types/src/scope.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ export interface Scope {
189189
clear(): this;
190190

191191
/**
192-
* Sets the breadcrumbs in the scope
193-
* @param breadcrumbs Breadcrumb
192+
* Adds a breadcrumb to the scope
193+
* @param breadcrumb Breadcrumb
194194
* @param maxBreadcrumbs number of max breadcrumbs to merged into event.
195195
*/
196196
addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this;
@@ -201,7 +201,7 @@ export interface Scope {
201201
getLastBreadcrumb(): Breadcrumb | undefined;
202202

203203
/**
204-
* Clears all currently set Breadcrumbs.
204+
* Clears all breadcrumbs from the scope.
205205
*/
206206
clearBreadcrumbs(): this;
207207

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { SeverityLevel } from '@sentry/types';
2+
3+
/**
4+
* Determine a breadcrumb's log level (only `warning` or `error`) based on an HTTP status code.
5+
*/
6+
export function getBreadcrumbLogLevelFromHttpStatusCode(statusCode: number | undefined): SeverityLevel | undefined {
7+
// NOTE: undefined defaults to 'info' in Sentry
8+
if (statusCode === undefined) {
9+
return undefined;
10+
} else if (statusCode >= 400 && statusCode < 500) {
11+
return 'warning';
12+
} else if (statusCode >= 500) {
13+
return 'error';
14+
} else {
15+
return undefined;
16+
}
17+
}

packages/utils/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './aggregate-errors';
22
export * from './array';
3+
export * from './breadcrumb-log-level';
34
export * from './browser';
45
export * from './dsn';
56
export * from './error';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getBreadcrumbLogLevelFromHttpStatusCode } from '../src/breadcrumb-log-level';
2+
3+
describe('getBreadcrumbLogLevelFromHttpStatusCode()', () => {
4+
it.each([
5+
['warning', '4xx', 403],
6+
['error', '5xx', 500],
7+
[undefined, '3xx', 307],
8+
[undefined, '2xx', 200],
9+
[undefined, '1xx', 103],
10+
[undefined, '0', 0],
11+
[undefined, 'undefined', undefined],
12+
])('should return `%s` for %s', (output, _codeRange, input) => {
13+
expect(getBreadcrumbLogLevelFromHttpStatusCode(input)).toEqual(output);
14+
});
15+
});

0 commit comments

Comments
 (0)