Skip to content

Commit

Permalink
feat: Set log level for Fetch/XHR breadcrumbs based on status code (#…
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
Zen-cronic and lforst authored Sep 23, 2024
1 parent c0a5a3e commit 1f898b6
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 9 deletions.
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 @@ -15,7 +15,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 @@ -243,14 +248,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

0 comments on commit 1f898b6

Please sign in to comment.