-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(deno)!: Align remaining denoHttpIntegration options with httpIntegration #23692
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /** | ||
| * 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. | ||
| * | ||
| * Returns `null` when the event should be dropped, otherwise the (possibly updated) event. | ||
| */ | ||
| export function processHttpServerTransactionEvent( | ||
| event: Event, | ||
| ignoreStatusCodes: (number | [number, number])[], | ||
| ): Event | null { | ||
| if (event.type !== 'transaction') { | ||
| 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| 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('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); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,24 @@ | ||
| import { subscribe } from 'node:diagnostics_channel'; | ||
| import { errorMonitor } from 'node:events'; | ||
| import type { RequestOptions } from 'node:http'; | ||
| import type { HttpIncomingMessage, HttpServerResponse, Integration, IntegrationFn, Span } from '@sentry/core'; | ||
| import type { | ||
| Event, | ||
| HttpClientRequest, | ||
| HttpIncomingMessage, | ||
| HttpServerResponse, | ||
| Integration, | ||
| IntegrationFn, | ||
| Span, | ||
| } from '@sentry/core'; | ||
| import { | ||
| DEFAULT_IGNORE_STATUS_CODES, | ||
| defineIntegration, | ||
| getHttpClientSubscriptions, | ||
| getHttpServerSubscriptions, | ||
| getRequestOptions, | ||
| HTTP_ON_CLIENT_REQUEST, | ||
| HTTP_ON_SERVER_REQUEST, | ||
| processHttpServerTransactionEvent, | ||
| } from '@sentry/core'; | ||
|
|
||
| const INTEGRATION_NAME = 'DenoHttp' as const; | ||
|
|
@@ -91,6 +101,43 @@ export interface DenoHttpIntegrationOptions { | |
| */ | ||
| ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean; | ||
|
|
||
| /** | ||
| * Do not send transaction events for incoming HTTP 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. | ||
| * | ||
| * Filtering runs in `processEvent` on the finished transaction, not when the span is created, | ||
| * so it also applies to `Deno.serve` transactions. 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. Node's `httpIntegration` has the same | ||
| * limitation. | ||
| * | ||
| * @default `[[401, 404], [301, 303], [305, 399]]` | ||
| */ | ||
| ignoreStatusCodes?: (number | [number, number])[]; | ||
|
|
||
| /** | ||
| * Called after the span for an outgoing request is created. | ||
| * Use this to add custom attributes to the span. | ||
| */ | ||
| outgoingRequestHook?: (span: Span, request: HttpClientRequest) => void; | ||
|
|
||
| /** | ||
| * Called when the response to an outgoing request is received. | ||
| */ | ||
| outgoingResponseHook?: (span: Span, response: HttpIncomingMessage) => void; | ||
|
|
||
| /** | ||
| * Called once both the outgoing request and its response are available (after the response | ||
| * ends). Useful for adding attributes based on both objects. | ||
| */ | ||
| outgoingRequestApplyCustomAttributes?: ( | ||
| span: Span, | ||
| request: HttpClientRequest, | ||
| response: HttpIncomingMessage, | ||
| ) => void; | ||
|
|
||
| /** | ||
| * 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. | ||
|
|
@@ -106,9 +153,13 @@ export interface DenoHttpIntegrationOptions { | |
| const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { | ||
| const breadcrumbs = options.breadcrumbs ?? true; | ||
| const tracePropagation = options.tracePropagation ?? true; | ||
| const ignoreStatusCodes = options.ignoreStatusCodes ?? DEFAULT_IGNORE_STATUS_CODES; | ||
|
|
||
| return { | ||
| name: INTEGRATION_NAME, | ||
| processEvent(event: Event): Event | null { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The filter is Are we good applying that Node default to Deno including
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a weirder aspect to this, I think. As of this change, the So, to filter out I'd recommend keep this where it is is, but add the same |
||
| return processHttpServerTransactionEvent(event, ignoreStatusCodes); | ||
| }, | ||
|
RulaKhaled marked this conversation as resolved.
|
||
| setupOnce() { | ||
| const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequest } = getHttpServerSubscriptions({ | ||
| ...options, | ||
|
|
@@ -120,6 +171,7 @@ const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { | |
| ...options, | ||
| breadcrumbs, | ||
| tracePropagation, | ||
| applyCustomAttributesOnSpan: options.outgoingRequestApplyCustomAttributes, | ||
| ignoreOutgoingRequests: options.ignoreOutgoingRequests | ||
| ? (url, request) => options.ignoreOutgoingRequests!(url, getRequestOptions(request)) | ||
| : undefined, | ||
|
|
@@ -145,4 +197,8 @@ const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { | |
| */ | ||
| export const denoHttpIntegration = defineIntegration(_denoHttpIntegration) as ( | ||
| options?: DenoHttpIntegrationOptions, | ||
| ) => Integration & { name: 'DenoHttp'; setupOnce: () => void }; | ||
| ) => Integration & { | ||
| name: 'DenoHttp'; | ||
| setupOnce: () => void; | ||
| processEvent: (event: Event) => Event | null; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this has no effect, can we remove it? It sounds like it's just taking up space.