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
1 change: 1 addition & 0 deletions .cursor/BUGBOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Keep reviews high-signal. Prefer actionable, high-confidence findings over specu
- What to do instead: Use an existing reference to the scope or client. For example, this is possible in most `Integration` hooks.
- Flag hard-coded attribute names and span ops (e.g. `'sentry.origin'`, `'sentry.op'`, `'http.request.method'`, `'db.system.name'`) when `@sentry/conventions` exports a constant for them. Use the constant from `@sentry/conventions/attributes` (e.g. `SENTRY_ORIGIN`, `SENTRY_OP`) or `@sentry/conventions/op` instead. Do not flag values that have no constant in `@sentry/conventions`.
- Flag unnecessary `span.setAttribute(s)` calls: If data is already available at span start, it must be set via the `attributes` option of `startSpan`, `startSpanManual`, `startInactiveSpan` or `startIdleSpan` calls. This ensures that as much context as possible is available when `tracesSampler` or `ignoreSpans` SDK options are applied. If a `span.setAttribute(s)` call happens at a later time than right after span start and the attribute value can only be computed at that time, do not flag it.
- Flag a new option on an integration for incoming HTTP requests that is not in `ServerRequestOptions` (`packages/core/src/integrations/http/server-request-options.ts`), when it applies to more than one runtime. These integrations are Node `httpIntegration`, Deno `denoHttpIntegration` and `denoServeIntegration`, Bun `bunHttpServerIntegration` and `bunServerIntegration`, and Cloudflare `httpServerIntegration`. Add the option to `ServerRequestOptions`, then support it in each runtime or list it as unsupported in that runtime's `CheckServerRequestOptions` check. Also flag a runtime option that matches a shared option in purpose but has a different name or meaning.

### Code quality

Expand Down
20 changes: 18 additions & 2 deletions packages/bun/src/integrations/bunHttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import * as http from 'node:http';
import * as https from 'node:https';
import type { IntegrationFn, Span } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import type { HttpIncomingMessage, HttpServerResponse } from '@sentry/core/server';
import type {
AssertTrue,
CheckServerRequestOptions,
HttpIncomingMessage,
HttpServerResponse,
} from '@sentry/core/server';
import { getHttpServerSubscriptions, HTTP_ON_SERVER_REQUEST } from '@sentry/core/server';

const INTEGRATION_NAME = 'BunHttpServer' as const;
Expand Down Expand Up @@ -37,8 +42,12 @@ interface BunHttpServerOptions {

/**
* Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`.
*
* The `request` parameter is the incoming `node:http` `IncomingMessage`. The `RequestOptions` part of the type is
* deprecated and will be removed in v12. Its fields that `IncomingMessage` does not have (such as `path` or
* `hostname`) are always `undefined`.
*/
ignoreRequestBody?: (url: string, request: http.RequestOptions) => boolean;
ignoreRequestBody?: (url: string, request: http.RequestOptions & HttpIncomingMessage) => boolean;

/**
* Controls the maximum size of incoming HTTP request bodies attached to events.
Expand Down Expand Up @@ -73,6 +82,13 @@ interface BunHttpServerOptions {
onSpanEnd?: (span: Span, request: HttpIncomingMessage, response: HttpServerResponse) => void;
}

// Checks these options against the shared `ServerRequestOptions`. A new
// shared field fails to compile here, until this integration supports
// it or lists it as unsupported.
type _ServerRequestOptionsCheck = AssertTrue<
CheckServerRequestOptions<BunHttpServerOptions, HttpIncomingMessage, HttpServerResponse>
>;

let hasPatched = false;

const _bunHttpServerIntegration = ((options: BunHttpServerOptions = {}) => {
Expand Down
22 changes: 22 additions & 0 deletions packages/bun/src/integrations/bunserver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import type { Integration, IntegrationFn, MaxRequestBodySize, SpanAttributes } from '@sentry/core';
import {
captureBodyFromWinterCGRequest,
Expand All @@ -20,6 +21,7 @@ import {
filterCollectedUrl,
filterCollectedUrlQuery,
} from '@sentry/core';
import type { AssertTrue, CheckServerRequestOptions } from '@sentry/core/server';
import { classifyResponseStreaming, getClientIPAddress } from '@sentry/core/server';
import type { Server, ServeOptions } from 'bun';
import {
Expand Down Expand Up @@ -55,6 +57,26 @@ export type BunServerIntegrationOptions = {
maxRequestBodySize?: MaxRequestBodySize;
};

// Checks these options against the shared `ServerRequestOptions`. A new
// shared field fails to compile here, until this integration supports
// it or lists it as unsupported.
type _ServerRequestOptionsCheck = AssertTrue<
CheckServerRequestOptions<
BunServerIntegrationOptions,
Request,
Response,
// Not supported yet.
| 'spans'
| 'sessions'
| 'sessionFlushingDelayMS'
| 'ignoreRequestBody'
| 'ignoreIncomingRequests'
| 'ignoreStaticAssets'
| 'onSpanCreated'
| 'onSpanEnd'
>
>;

const _bunServerIntegration = ((options: BunServerIntegrationOptions = {}) => {
return {
name: INTEGRATION_NAME,
Expand Down
22 changes: 22 additions & 0 deletions packages/cloudflare/src/integrations/httpServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Client, IntegrationFn, MaxRequestBodySize } from '@sentry/core';
import { captureBodyFromWinterCGRequest, defineIntegration, getIsolationScope } from '@sentry/core';
import type { AssertTrue, CheckServerRequestOptions } from '@sentry/core/server';

const INTEGRATION_NAME = 'HttpServer' as const;

Expand Down Expand Up @@ -40,6 +41,27 @@ export interface HttpServerIntegrationOptions {
ignoreRequestBody?: (url: string, request: Request) => boolean;
}

// Checks these options against the shared `ServerRequestOptions`. A new
// shared field fails to compile here, until this integration supports
// it or lists it as unsupported.
type _ServerRequestOptionsCheck = AssertTrue<
CheckServerRequestOptions<
HttpServerIntegrationOptions,
Request,
Response,
// Not supported yet.
| 'spans'
| 'ignoreIncomingRequests'
| 'ignoreStaticAssets'
| 'onSpanCreated'
| 'onSpanEnd'
// Not supported. A worker has no long-lived process to run the timer
// that flushes session aggregates.
| 'sessions'
| 'sessionFlushingDelayMS'
>
>;

interface HttpServerIntegrationInstance {
name: string;
maxRequestBodySize: MaxRequestBodySize | undefined;
Expand Down
128 changes: 128 additions & 0 deletions packages/core/src/integrations/http/server-request-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { Span } from '../../types/span';
import type { MaxRequestBodySize } from '../../utils/request';

/**
* Options that every integration for incoming HTTP requests should
* support, in each runtime.
*
* `TRequest` and `TResponse` are the request and response types that the
* runtime passes to callbacks: `HttpIncomingMessage` and
* `HttpServerResponse` for `node:http`, or the web `Request` and
* `Response` for `Deno.serve`, `Bun.serve` and Cloudflare.
*
* Runtime options types do not extend this type. Each one declares its
* own fields, with docs for that runtime, and uses
* {@link CheckServerRequestOptions} to check them against this type.
* Then a new field here fails to compile in each runtime, until the
* runtime supports it or lists it as unsupported.
*/
export interface ServerRequestOptions<TRequest, TResponse> {
/**
* Whether to create spans for incoming requests.
* Defaults to the client's tracing configuration.
*/
spans?: boolean;

/**
* Whether to create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming
* requests, to track the health and crash-free rate of releases.
*
* @default `true`
*/
sessions?: boolean;

/**
* Number of milliseconds until sessions are flushed as a session aggregate.
*
* @default `60000` (60s)
*/
sessionFlushingDelayMS?: number;

/**
* Controls the maximum size of incoming request bodies attached to events.
*
* @default 'medium'
*/
maxRequestBodySize?: MaxRequestBodySize;

/**
* Do not capture the request body for incoming requests where the given callback returns `true`.
*
* `url` is the full URL of the request, including the query string.
*/
ignoreRequestBody?: (url: string, request: TRequest) => boolean;

/**
* Do not create spans for incoming requests where the given callback returns `true`.
*
* `urlPath` is the path of the request, including the query string.
*/
ignoreIncomingRequests?: (urlPath: string, request: TRequest) => boolean;

/**
* Whether to ignore common static asset requests (favicon.ico, robots.txt, etc.)
* when creating spans.
*
* @default `true`
*/
ignoreStaticAssets?: boolean;

/**
* A hook to change the span for an incoming request, after the span is created.
*/
onSpanCreated?: (span: Span, request: TRequest) => void;

/**
* A hook to change the span for an incoming request one last time, before it ends.
*/
onSpanEnd?: (span: Span, request: TRequest, response: TResponse) => void;
}

/**
* `true` when `TOptions` matches {@link ServerRequestOptions}, else a
* string that names the problem. Use it with a type that accepts only
* `true`, so a mismatch fails to compile:
*
* ```ts
* type _Check = AssertTrue<CheckServerRequestOptions<MyOptions, Request, Response, 'sessions'>>;
* ```
*
* `TUnsupported` lists the fields that the runtime does not support.
*
* The check fails when:
* - `TOptions` does not declare a field, and `TUnsupported` does not
* list it.
* - `TUnsupported` lists a field that `TOptions` declares.
* - A callback written for the shared type does not fit the field in
* `TOptions`.
*/
export type CheckServerRequestOptions<
TOptions,
TRequest,
TResponse,
TUnsupported extends keyof ServerRequestOptions<TRequest, TResponse> = never,
> = [MissingFields<TOptions, TRequest, TResponse, TUnsupported>] extends [never]
? [Extract<TUnsupported, keyof TOptions>] extends [never]
? SupportedFields<
ServerRequestOptions<TRequest, TResponse>,
TRequest,
TResponse,
TUnsupported
> extends SupportedFields<TOptions, TRequest, TResponse, TUnsupported>
? true
: 'A field type does not match ServerRequestOptions'
: `Listed as unsupported, but declared: ${Extract<TUnsupported, keyof TOptions> & string}`
: `Missing field: ${MissingFields<TOptions, TRequest, TResponse, TUnsupported> & string}`;

type MissingFields<TOptions, TRequest, TResponse, TUnsupported> = Exclude<
keyof ServerRequestOptions<TRequest, TResponse>,
TUnsupported | keyof TOptions
>;

type SupportedFields<T, TRequest, TResponse, TUnsupported> = Pick<
T,
Extract<keyof T, Exclude<keyof ServerRequestOptions<TRequest, TResponse>, TUnsupported>>
>;

/** Accepts only `true`. Use it with {@link CheckServerRequestOptions}. */
export type AssertTrue<T extends true> = T;
5 changes: 5 additions & 0 deletions packages/core/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export type {
HttpServerResponse,
HttpModuleExport,
} from './integrations/http/types';
export type {
AssertTrue,
CheckServerRequestOptions,
ServerRequestOptions,
} from './integrations/http/server-request-options';
export { createFetchIntegration } from './integrations/fetch';
export type { FetchIntegrationOptions } from './integrations/fetch';
export { classifyResponseStreaming } from './utils/responseStreaming';
Expand Down
6 changes: 6 additions & 0 deletions packages/deno/src/integrations/deno-serve.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Event, IntegrationFn, MaxRequestBodySize, Span } from '@sentry/core';
import { debug, defineIntegration } from '@sentry/core';
import type { AssertTrue, CheckServerRequestOptions } from '@sentry/core/server';
import { DEFAULT_IGNORE_STATUS_CODES, processHttpServerTransactionEvent } from '@sentry/core/server';
import type { RequestHandlerWrapperOptions } from '../wrap-deno-request-handler';
import { wrapDenoRequestHandler } from '../wrap-deno-request-handler';
Expand Down Expand Up @@ -99,6 +100,11 @@ export type DenoServeIntegrationOptions = {
ignoreStatusCodes?: (number | [number, number])[];
};

// Checks these options against the shared `ServerRequestOptions`. A new
// shared field fails to compile here, until this integration supports
// it or lists it as unsupported.
type _ServerRequestOptionsCheck = AssertTrue<CheckServerRequestOptions<DenoServeIntegrationOptions, Request, Response>>;

export type ServeParams =
// [(Request) => Response]
| [Deno.ServeHandler<Deno.NetAddr>]
Expand Down
15 changes: 14 additions & 1 deletion packages/deno/src/integrations/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { errorMonitor } from 'node:events';
import type { RequestOptions } from 'node:http';
import type { Event, Integration, IntegrationFn, Span } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import type { HttpClientRequest, HttpIncomingMessage, HttpServerResponse } from '@sentry/core/server';
import type {
AssertTrue,
CheckServerRequestOptions,
HttpClientRequest,
HttpIncomingMessage,
HttpServerResponse,
} from '@sentry/core/server';
import {
DEFAULT_IGNORE_STATUS_CODES,
getHttpClientSubscriptions,
Expand Down Expand Up @@ -151,6 +157,13 @@ export interface DenoHttpIntegrationOptions {
onSpanEnd?: (span: Span, request: HttpIncomingMessage, response: HttpServerResponse) => void;
}

// Checks these options against the shared `ServerRequestOptions`. A new
// shared field fails to compile here, until this integration supports
// it or lists it as unsupported.
type _ServerRequestOptionsCheck = AssertTrue<
CheckServerRequestOptions<DenoHttpIntegrationOptions, HttpIncomingMessage, HttpServerResponse>
>;

const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => {
const breadcrumbs = options.breadcrumbs ?? true;
const tracePropagation = options.tracePropagation ?? true;
Expand Down
14 changes: 5 additions & 9 deletions packages/node/src/integrations/http/httpServerIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ export interface HttpServerIntegrationOptions {
* This can be useful for long running requests where the body is not needed and we want to avoid capturing it.
*
* @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request.
* @param request Contains the {@type RequestOptions} object used to make the incoming request.
* @param request The incoming request, a `node:http` `IncomingMessage`. Use `request.url`, `request.method`,
* `request.headers`, etc. The `RequestOptions` part of the type is deprecated and will be removed in v12. Its
* fields that `IncomingMessage` does not have (such as `path` or `hostname`) are always `undefined`.
*/
ignoreRequestBody?: (url: string, request: RequestOptions) => boolean;
ignoreRequestBody?: (url: string, request: RequestOptions & HttpIncomingMessage) => boolean;

/**
* Controls the maximum size of incoming HTTP request bodies attached to events.
Expand Down Expand Up @@ -84,13 +86,7 @@ const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}) =>
// core subscription helper. Explicitly opt out so the helper does not
// double-create spans when the client has tracing enabled.
spans: false as const,
// Cast: core uses HttpIncomingMessage; node consumers pass
// RequestOptions-typed callbacks.
// The two are structurally compatible for the fields the callback reads
// (url, method, headers).
ignoreRequestBody: options.ignoreRequestBody as
| ((url: string, request: HttpIncomingMessage) => boolean)
| undefined,
ignoreRequestBody: options.ignoreRequestBody,

/**
* Hook called by core's `instrumentServer` to wrap the upstream
Expand Down
19 changes: 19 additions & 0 deletions packages/node/src/integrations/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import {
hasSpanStreamingEnabled,
stripDataUrlContent,
} from '@sentry/core';
import type {
AssertTrue,
CheckServerRequestOptions,
HttpIncomingMessage,
HttpServerResponse,
} from '@sentry/core/server';
import { getRequestUrlFromClientRequest } from '@sentry/core/server';
import type { NodeClient } from '../../sdk/client';
import type { HttpServerIntegrationOptions } from './httpServerIntegration';
Expand Down Expand Up @@ -83,6 +89,19 @@ interface HttpOptions extends HttpServerIntegrationOptions, HttpServerSpansInteg
outgoingRequestApplyCustomAttributes?: OutgoingHttpRequestInstrumentationOptions['outgoingRequestApplyCustomAttributes'];
}

// Checks these options against the shared `ServerRequestOptions`. A new
// shared field fails to compile here, until this integration supports
// it or lists it as unsupported.
type _ServerRequestOptionsCheck = AssertTrue<
CheckServerRequestOptions<
HttpOptions,
HttpIncomingMessage,
HttpServerResponse,
// Not supported yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: To double check. Going up the PR stack, I see that onSpanEnd is being supported then right? So this is here only temporarily?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'onSpanEnd'
>
>;

/**
* The http integration instruments Node's internal http and https modules.
* It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span.
Expand Down
Loading