Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,39 @@ options were already named `tracePropagation`.
This is unrelated to `propagateTraceparent` (whether the W3C `traceparent` header is sent alongside `sentry-trace`) and
`tracePropagationTargets` (which URLs receive trace headers). Both keep their names.

### Deno server transactions are dropped for some 3xx/4xx status codes

Affected SDKs: `@sentry/deno`.

`denoHttpIntegration` and `denoServeIntegration` now honor `ignoreStatusCodes`, using the same default list as
`httpIntegration` in the other server SDKs: incoming request transactions whose response status falls in
`[[401, 404], [301, 303], [305, 399]]` are dropped. Previously the option was declared but never read, so these
transactions were always kept.

Each integration owns the option for the requests it instruments — `denoHttpIntegration` for `node:http`,
`denoServeIntegration` for `Deno.serve` — so setting it on one does not affect the other. Pass your own list to change
which codes are dropped, or an empty array to keep everything:

```js
Sentry.init({
dsn: '__DSN__',
integrations: [
Sentry.denoHttpIntegration({ ignoreStatusCodes: [] }),
Sentry.denoServeIntegration({ ignoreStatusCodes: [] }),
],
});
```

This filter runs on transaction events (`processEvent`), so it only takes effect when `traceLifecycle` is `'static'`.
The default `'stream'` lifecycle does not produce transaction events, and typical Deno apps are unaffected. Node's
`httpIntegration` has the same limitation.

Transactions that are kept now also carry the HTTP status in the top-level `response` context, as in the other server
SDKs.

`denoHttpIntegration` additionally accepts the outgoing request hooks `outgoingRequestHook`, `outgoingResponseHook` and
`outgoingRequestApplyCustomAttributes`, matching `httpIntegration`.

### `tracePropagationTargets` matching is now case-insensitive

Affected SDKs: All SDKs.
Expand Down
84 changes: 84 additions & 0 deletions packages/core/src/integrations/http/server-transaction-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Shared post-processing for transaction events produced by server span instrumentation.
*
* Node's `httpServerSpansIntegration` and Deno's `denoHttpIntegration` both create their
* server spans outside of the OTel SDK span exporter, so neither gets the exporter's
* status code handling for free. Both run this from their `processEvent` hook instead.
*/

import { HTTP_RESPONSE_STATUS_CODE } from '@sentry/conventions/attributes';
import { DEBUG_BUILD } from '../../debug-build';
import type { Event } from '../../types/event';
import { debug } from '../../utils/debug-logger';

/**
* Status codes for which server transactions are dropped unless `ignoreStatusCodes` says otherwise.
*
* 300 and 304 are possibly valid status codes we do not want to filter, hence the split ranges.
*/
export const DEFAULT_IGNORE_STATUS_CODES: (number | [number, number])[] = [
[401, 404],
[301, 303],
[305, 399],
];

/**
* If the given status code should be filtered for the given list of status codes/ranges.
*/
export function shouldFilterStatusCode(statusCode: number, dropForStatusCodes: (number | [number, number])[]): boolean {
return dropForStatusCodes.some(code => {
if (typeof code === 'number') {
return code === statusCode;
}

const [min, max] = code;
return statusCode >= min && statusCode <= max;
});
}

/**
* Drop transaction events whose HTTP status code matches `ignoreStatusCodes`, and surface the
* status as the top-level `response` context on the ones that are kept.
*
* Pass `spanOrigin` to only act on transactions produced by a specific instrumentation, so that
* an integration owning this option does not filter transactions created by a different one.
* When omitted, every transaction carrying an HTTP status code is considered.
*
* Returns `null` when the event should be dropped, otherwise the (possibly updated) event.
*/
export function processHttpServerTransactionEvent(
event: Event,
ignoreStatusCodes: (number | [number, number])[],
spanOrigin?: string,
): Event | null {
if (event.type !== 'transaction') {
return event;
}

if (spanOrigin !== undefined && event.contexts?.trace?.origin !== spanOrigin) {
return event;
}

const statusCode = event.contexts?.trace?.data?.[HTTP_RESPONSE_STATUS_CODE];
if (typeof statusCode !== 'number') {
return event;
}

if (shouldFilterStatusCode(statusCode, ignoreStatusCodes)) {
DEBUG_BUILD && debug.log('Dropping transaction due to status code', statusCode);
return null;
}

// Surface the HTTP status as the top-level `response` context. The OTel SDK span exporter
// already does this on its path; doing it here covers transactions produced by tracer
// providers that bypass that exporter (Node's `SentryTracerProvider`, Deno's).
event.contexts = {
...event.contexts,
response: {
...event.contexts?.response,
status_code: statusCode,
},
};

return event;
}
9 changes: 0 additions & 9 deletions packages/core/src/integrations/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,6 @@ export interface HttpInstrumentationOptions {
*/
ignoreStaticAssets?: boolean;

/**
* Do not capture spans for incoming HTTP requests with the given status codes.
* By default, spans with some 3xx and 4xx status codes are ignored (see @default).
* Expects an array of status codes or a range of status codes, e.g. [[300,399], 404] would ignore 3xx and 404 status codes.
*
* @default `[[401, 404], [301, 303], [305, 399]]`
*/
ignoreStatusCodes?: (number | [number, number])[];

/**
* A hook that can be used to mutate the span for incoming requests.
* This is triggered after the span is created, but before it is recorded.
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/server-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export { getSqlQuerySummary as _INTERNAL_getSqlQuerySummary } from './utils/sql'
export { patchHttpModuleClient } from './integrations/http/client-patch';
export { getHttpClientSubscriptions } from './integrations/http/client-subscriptions';
export { getHttpServerSubscriptions, isStaticAssetRequest } from './integrations/http/server-subscription';
export {
DEFAULT_IGNORE_STATUS_CODES,
processHttpServerTransactionEvent,
} from './integrations/http/server-transaction-event';
export { recordRequestSession } from './integrations/http/record-request-session';
export { addOutgoingRequestBreadcrumb } from './integrations/http/add-outgoing-request-breadcrumb';
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, it } from 'vitest';
import type { Event } from '../../../../src/types/event';
import {
DEFAULT_IGNORE_STATUS_CODES,
processHttpServerTransactionEvent,
shouldFilterStatusCode,
} from '../../../../src/integrations/http/server-transaction-event';

function transaction(statusCode?: number, contexts: Record<string, unknown> = {}): Event {
return {
type: 'transaction',
contexts: {
...contexts,
trace: { data: statusCode === undefined ? {} : { 'http.response.status_code': statusCode } },
},
} as Event;
}

describe('shouldFilterStatusCode', () => {
it('matches plain status codes', () => {
expect(shouldFilterStatusCode(404, [404])).toBe(true);
expect(shouldFilterStatusCode(500, [404])).toBe(false);
});

it('matches inclusive ranges', () => {
expect(shouldFilterStatusCode(300, [[300, 399]])).toBe(true);
expect(shouldFilterStatusCode(399, [[300, 399]])).toBe(true);
expect(shouldFilterStatusCode(400, [[300, 399]])).toBe(false);
});

it('matches a mix of codes and ranges', () => {
expect(shouldFilterStatusCode(404, [[300, 399], 404])).toBe(true);
});

it('never matches on an empty list', () => {
expect(shouldFilterStatusCode(404, [])).toBe(false);
});

it.each([
[300, false],
[301, true],
[303, true],
[304, false],
[305, true],
[399, true],
[401, true],
[404, true],
[405, false],
[200, false],
[500, false],
])('applies the default list correctly to %i', (statusCode, expected) => {
expect(shouldFilterStatusCode(statusCode, DEFAULT_IGNORE_STATUS_CODES)).toBe(expected);
});
});

describe('processHttpServerTransactionEvent', () => {
it('drops transactions whose status code is ignored', () => {
expect(processHttpServerTransactionEvent(transaction(404), [404])).toBeNull();
});

it('lifts the status code into the top-level `response` context', () => {
const event = processHttpServerTransactionEvent(transaction(200), []);
expect(event?.contexts?.response).toEqual({ status_code: 200 });
});

it('preserves existing `response` context fields', () => {
const event = processHttpServerTransactionEvent(transaction(201, { response: { body_size: 42 } }), []);
expect(event?.contexts?.response).toEqual({ body_size: 42, status_code: 201 });
});

it('leaves the event untouched when there is no status code', () => {
const event = processHttpServerTransactionEvent(transaction(undefined), []);
expect(event?.contexts?.response).toBeUndefined();
});

it('ignores events from a different span origin when spanOrigin is given', () => {
const event = {
...transaction(404),
contexts: { trace: { origin: 'auto.http.deno', data: { 'http.response.status_code': 404 } } },
} as Event;
// Would be dropped without the gate; the origin does not match, so it passes through.
expect(processHttpServerTransactionEvent(event, [404], 'auto.http.server')).toBe(event);
});

it('acts on events whose span origin matches', () => {
const event = {
...transaction(404),
contexts: { trace: { origin: 'auto.http.server', data: { 'http.response.status_code': 404 } } },
} as Event;
expect(processHttpServerTransactionEvent(event, [404], 'auto.http.server')).toBeNull();
});

it('acts on every origin when spanOrigin is omitted', () => {
const event = {
...transaction(404),
contexts: { trace: { origin: 'auto.http.deno', data: { 'http.response.status_code': 404 } } },
} as Event;
expect(processHttpServerTransactionEvent(event, [404])).toBeNull();
});

it('leaves non-transaction events untouched, even with an ignored status code', () => {
const event = { type: undefined, contexts: { trace: { data: { 'http.response.status_code': 404 } } } } as Event;
expect(processHttpServerTransactionEvent(event, [404])).toBe(event);
});
});
26 changes: 24 additions & 2 deletions packages/deno/src/integrations/deno-serve.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IntegrationFn, MaxRequestBodySize } from '@sentry/core';
import { debug, defineIntegration } from '@sentry/core';
import type { Event, IntegrationFn, MaxRequestBodySize } from '@sentry/core';
import { debug, DEFAULT_IGNORE_STATUS_CODES, defineIntegration, processHttpServerTransactionEvent } from '@sentry/core';
import type { RequestHandlerWrapperOptions } from '../wrap-deno-request-handler';
import { wrapDenoRequestHandler } from '../wrap-deno-request-handler';

Expand All @@ -15,6 +15,21 @@ export type DenoServeIntegrationOptions = {
* @default 'medium'
*/
maxRequestBodySize?: MaxRequestBodySize;

/**
* Do not capture spans for incoming `Deno.serve` requests with the given status codes.
* By default, some 3xx and 4xx status codes are dropped (see @default).
* Expects an array of status codes or a range of status codes, e.g. [[300,399], 404] would ignore 3xx and 404 status codes.
*
* Applies only to spans this integration creates. `node:http` requests are covered by
* `denoHttpIntegration`'s own option of the same name. Pass `[]` to keep everything.
*
* Only takes effect with `traceLifecycle: 'static'`. The default `'stream'` lifecycle does not
* produce transaction events, so the filter does not run.
*
* @default `[[401, 404], [301, 303], [305, 399]]`
*/
ignoreStatusCodes?: (number | [number, number])[];
};

export type ServeParams =
Expand Down Expand Up @@ -72,9 +87,16 @@ const instrumentedDenoServe = (serve: typeof Deno.serve): typeof Deno.serve =>
});

const _denoServeIntegration = ((options: DenoServeIntegrationOptions = {}) => {
const ignoreStatusCodes = options.ignoreStatusCodes ?? DEFAULT_IGNORE_STATUS_CODES;

return {
name: INTEGRATION_NAME,
maxRequestBodySize: options.maxRequestBodySize,
processEvent(event: Event): Event | null {
// Gated on this integration's own span origin so it does not filter `node:http`
// transactions, which `denoHttpIntegration` owns via its own `ignoreStatusCodes`.
return processHttpServerTransactionEvent(event, ignoreStatusCodes, 'auto.http.deno');
},
setupOnce() {
const originalServe = Deno.serve;
const wrappedServe = instrumentedDenoServe(originalServe);
Expand Down
Loading
Loading