Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Set log level for Fetch/XHR breadcrumbs based on status code #13711

Merged
merged 9 commits into from
Sep 23, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fetch('http://sentry-test.io/foo').then(() => {
Sentry.captureException('test error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('captures Breadcrumb with log level for 4xx response code', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', async route => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
});
});

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);

expect(eventData?.breadcrumbs?.length).toBe(1);
expect(eventData!.breadcrumbs![0]).toEqual({
timestamp: expect.any(Number),
category: 'fetch',
type: 'http',
data: {
method: 'GET',
status_code: 404,
url: 'http://sentry-test.io/foo',
},
level: 'warning',
});

await page.route('**/foo', async route => {
await route.fulfill({
status: 500,
contentType: 'text/plain',
body: 'Internal Server Error',
});
});
});

sentryTest('captures Breadcrumb with log level for 5xx response code', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', async route => {
await route.fulfill({
status: 500,
contentType: 'text/plain',
body: 'Internal Server Error',
});
});

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);

expect(eventData?.breadcrumbs?.length).toBe(1);
expect(eventData!.breadcrumbs![0]).toEqual({
timestamp: expect.any(Number),
category: 'fetch',
type: 'http',
data: {
method: 'GET',
status_code: 500,
url: 'http://sentry-test.io/foo',
},
level: 'error',
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const xhr = new XMLHttpRequest();

xhr.open('GET', 'http://sentry-test.io/foo');
xhr.send();

xhr.addEventListener('readystatechange', function () {
if (xhr.readyState === 4) {
Sentry.captureException('test error');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('captures Breadcrumb with log level for 4xx response code', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', async route => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
});
});

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);

expect(eventData?.breadcrumbs?.length).toBe(1);
expect(eventData!.breadcrumbs![0]).toEqual({
timestamp: expect.any(Number),
category: 'xhr',
type: 'http',
data: {
method: 'GET',
status_code: 404,
url: 'http://sentry-test.io/foo',
},
level: 'warning',
});

await page.route('**/foo', async route => {
await route.fulfill({
status: 500,
contentType: 'text/plain',
body: 'Internal Server Error',
});
});
});

sentryTest('captures Breadcrumb with log level for 5xx response code', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', async route => {
await route.fulfill({
status: 500,
contentType: 'text/plain',
body: 'Internal Server Error',
});
});

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);

expect(eventData?.breadcrumbs?.length).toBe(1);
expect(eventData!.breadcrumbs![0]).toEqual({
timestamp: expect.any(Number),
category: 'xhr',
type: 'http',
data: {
method: 'GET',
status_code: 500,
url: 'http://sentry-test.io/foo',
},
level: 'error',
});
});
7 changes: 7 additions & 0 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
import {
addConsoleInstrumentationHandler,
addFetchInstrumentationHandler,
getBreadcrumbLogLevelFromHttpStatusCode,
getComponentName,
getEventDescription,
htmlTreeAsString,
Expand Down Expand Up @@ -247,11 +248,14 @@ function _getXhrBreadcrumbHandler(client: Client): (handlerData: HandlerDataXhr)
endTimestamp,
};

const level = getBreadcrumbLogLevelFromHttpStatusCode(status_code);

addBreadcrumb(
{
category: 'xhr',
data,
type: 'http',
level,
},
hint,
);
Expand Down Expand Up @@ -309,11 +313,14 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe
startTimestamp,
endTimestamp,
};
const level = getBreadcrumbLogLevelFromHttpStatusCode(data.status_code);

addBreadcrumb(
{
category: 'fetch',
data,
type: 'http',
level,
},
hint,
);
Expand Down
10 changes: 9 additions & 1 deletion packages/cloudflare/src/integrations/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import type {
IntegrationFn,
Span,
} from '@sentry/types';
import { LRUMap, addFetchInstrumentationHandler, stringMatchesSomePattern } from '@sentry/utils';
import {
LRUMap,
addFetchInstrumentationHandler,
getBreadcrumbLogLevelFromHttpStatusCode,
stringMatchesSomePattern,
} from '@sentry/utils';

const INTEGRATION_NAME = 'Fetch';

Expand Down Expand Up @@ -144,11 +149,14 @@ function createBreadcrumb(handlerData: HandlerDataFetch): void {
startTimestamp,
endTimestamp,
};
const level = getBreadcrumbLogLevelFromHttpStatusCode(data.status_code);

addBreadcrumb(
{
category: 'fetch',
data,
type: 'http',
level,
},
hint,
);
Expand Down
4 changes: 4 additions & 0 deletions packages/deno/src/integrations/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
import {
addConsoleInstrumentationHandler,
addFetchInstrumentationHandler,
getBreadcrumbLogLevelFromHttpStatusCode,
getEventDescription,
safeJoin,
severityLevelFromString,
Expand Down Expand Up @@ -178,11 +179,14 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe
startTimestamp,
endTimestamp,
};
const level = getBreadcrumbLogLevelFromHttpStatusCode(data.status_code);

addBreadcrumb(
{
category: 'fetch',
data,
type: 'http',
level,
},
hint,
);
Expand Down
13 changes: 11 additions & 2 deletions packages/node/src/integrations/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import {
import { getClient } from '@sentry/opentelemetry';
import type { IntegrationFn, SanitizedRequestData } from '@sentry/types';

import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils';
import {
getBreadcrumbLogLevelFromHttpStatusCode,
getSanitizedUrlString,
parseUrl,
stripUrlQueryAndFragment,
} from '@sentry/utils';
import type { NodeClient } from '../sdk/client';
import { setIsolationScope } from '../sdk/scope';
import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module';
Expand Down Expand Up @@ -229,14 +234,18 @@ function _addRequestBreadcrumb(
}

const data = getBreadcrumbData(request);
const statusCode = response.statusCode;
const level = getBreadcrumbLogLevelFromHttpStatusCode(statusCode);

addBreadcrumb(
{
category: 'http',
data: {
status_code: response.statusCode,
status_code: statusCode,
...data,
},
type: 'http',
level,
},
{
event: 'response',
Expand Down
7 changes: 5 additions & 2 deletions packages/node/src/integrations/node-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, addBreadcrumb, defineIntegration } from '@sentry/core';
import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry';
import type { IntegrationFn, SanitizedRequestData } from '@sentry/types';
import { getSanitizedUrlString, parseUrl } from '@sentry/utils';
import { getBreadcrumbLogLevelFromHttpStatusCode, getSanitizedUrlString, parseUrl } from '@sentry/utils';

interface NodeFetchOptions {
/**
Expand Down Expand Up @@ -56,15 +56,18 @@ export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchInte
/** Add a breadcrumb for outgoing requests. */
function addRequestBreadcrumb(request: UndiciRequest, response: UndiciResponse): void {
const data = getBreadcrumbData(request);
const statusCode = response.statusCode;
const level = getBreadcrumbLogLevelFromHttpStatusCode(statusCode);

addBreadcrumb(
{
category: 'http',
data: {
status_code: response.statusCode,
status_code: statusCode,
...data,
},
type: 'http',
level,
},
{
event: 'response',
Expand Down
6 changes: 3 additions & 3 deletions packages/types/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ export interface Scope {
clear(): this;

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

/**
* Clears all currently set Breadcrumbs.
* Clears all breadcrumbs from the scope.
*/
clearBreadcrumbs(): this;

Expand Down
17 changes: 17 additions & 0 deletions packages/utils/src/breadcrumb-log-level.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { SeverityLevel } from '@sentry/types';

/**
* Determine a breadcrumb's log level (only `warning` or `error`) based on an HTTP status code.
*/
export function getBreadcrumbLogLevelFromHttpStatusCode(statusCode: number | undefined): SeverityLevel | undefined {
// NOTE: undefined defaults to 'info' in Sentry
if (statusCode === undefined) {
return undefined;
} else if (statusCode >= 400 && statusCode < 500) {
return 'warning';
} else if (statusCode >= 500) {
return 'error';
} else {
return undefined;
}
}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './aggregate-errors';
export * from './array';
export * from './breadcrumb-log-level';
export * from './browser';
export * from './dsn';
export * from './error';
Expand Down
15 changes: 15 additions & 0 deletions packages/utils/test/breadcrumb-log-level.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getBreadcrumbLogLevelFromHttpStatusCode } from '../src/breadcrumb-log-level';

describe('getBreadcrumbLogLevelFromHttpStatusCode()', () => {
it.each([
['warning', '4xx', 403],
['error', '5xx', 500],
[undefined, '3xx', 307],
[undefined, '2xx', 200],
[undefined, '1xx', 103],
[undefined, '0', 0],
[undefined, 'undefined', undefined],
])('should return `%s` for %s', (output, _codeRange, input) => {
expect(getBreadcrumbLogLevelFromHttpStatusCode(input)).toEqual(output);
});
});
Loading
Loading