diff --git a/dev-packages/node-integration-tests/suites/express/tracing/instrument-filterStatusCode.mjs b/dev-packages/node-integration-tests/suites/express/tracing/instrument-filterStatusCode.mjs index 884d250718ae..6638b7e92cb8 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/instrument-filterStatusCode.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/instrument-filterStatusCode.mjs @@ -9,7 +9,7 @@ Sentry.init({ transport: loggingTransport, integrations: [ Sentry.httpIntegration({ - dropSpansForIncomingRequestStatusCodes: [499, [300, 399]], + ignoreStatusCodes: [499, [300, 399]], }), ], }); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/instrument.mjs b/dev-packages/node-integration-tests/suites/express/tracing/instrument.mjs index d58a18e483c7..c727f3046e61 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/instrument.mjs @@ -11,7 +11,7 @@ Sentry.init({ transport: loggingTransport, integrations: [ Sentry.httpIntegration({ - ignoreIncomingRequestBody: url => { + ignoreRequestBody: url => { if (url.includes('/test-post-ignore-body')) { return true; } diff --git a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-always.mjs b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-always.mjs index ffeead2bc0b5..2ed2c4b03868 100644 --- a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-always.mjs +++ b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-always.mjs @@ -7,5 +7,5 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - integrations: [Sentry.httpIntegration({ maxIncomingRequestBodySize: 'always' })], + integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'always' })], }); diff --git a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-medium.mjs b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-medium.mjs index 7f8aa77414bb..2ea1a18c449c 100644 --- a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-medium.mjs +++ b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-medium.mjs @@ -7,5 +7,5 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - integrations: [Sentry.httpIntegration({ maxIncomingRequestBodySize: 'medium' })], + integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'medium' })], }); diff --git a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-none.mjs b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-none.mjs index 74139dcce86d..b3c2c06d3dd9 100644 --- a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-none.mjs +++ b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-none.mjs @@ -9,8 +9,8 @@ Sentry.init({ transport: loggingTransport, integrations: [ Sentry.httpIntegration({ - maxIncomingRequestBodySize: 'none', - ignoreIncomingRequestBody: url => url.includes('/ignore-request-body'), + maxRequestBodySize: 'none', + ignoreRequestBody: url => url.includes('/ignore-request-body'), }), ], }); diff --git a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-small.mjs b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-small.mjs index 5e056b9dff30..2331b64953b4 100644 --- a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-small.mjs +++ b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/instrument-small.mjs @@ -8,5 +8,5 @@ Sentry.init({ tracesSampleRate: 1.0, transport: loggingTransport, dataCollection: { httpBodies: [] }, - integrations: [Sentry.httpIntegration({ maxIncomingRequestBodySize: 'small' })], + integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'small' })], }); diff --git a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/test.ts b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/test.ts index 6e5843b54b9b..2cc5595f4d20 100644 --- a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/test.ts +++ b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/test.ts @@ -12,7 +12,7 @@ const MAX_GENERAL = 1024 * 1024; // 1MB const MAX_MEDIUM = 10_000; const MAX_SMALL = 1000; -describe('express with httpIntegration and not defined maxIncomingRequestBodySize', () => { +describe('express with httpIntegration and not defined maxRequestBodySize', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -60,7 +60,7 @@ describe('express with httpIntegration and not defined maxIncomingRequestBodySiz }); }); -describe('express with httpIntegration, disabled httpBodies, and explicit maxIncomingRequestBodySize', () => { +describe('express with httpIntegration, disabled httpBodies, and explicit maxRequestBodySize', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -88,7 +88,7 @@ describe('express with httpIntegration, disabled httpBodies, and explicit maxInc }); }); -describe('express with httpIntegration and maxIncomingRequestBodySize: "none"', () => { +describe('express with httpIntegration and maxRequestBodySize: "none"', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -114,7 +114,7 @@ describe('express with httpIntegration and maxIncomingRequestBodySize: "none"', await runner.completed(); }); - test('does not capture any request bodies with "none" setting and "ignoreIncomingRequestBody"', async () => { + test('does not capture any request bodies with "none" setting and "ignoreRequestBody"', async () => { const runner = createRunner() .expect({ transaction: { @@ -149,7 +149,7 @@ describe('express with httpIntegration and maxIncomingRequestBodySize: "none"', }); }); -describe('express with httpIntegration and maxIncomingRequestBodySize: "always"', () => { +describe('express with httpIntegration and maxRequestBodySize: "always"', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -197,7 +197,7 @@ describe('express with httpIntegration and maxIncomingRequestBodySize: "always"' }); }); -describe('express with httpIntegration and maxIncomingRequestBodySize: "small"', () => { +describe('express with httpIntegration and maxRequestBodySize: "small"', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -266,7 +266,7 @@ describe('express with httpIntegration and maxIncomingRequestBodySize: "small"', }); }); -describe('express with httpIntegration and maxIncomingRequestBodySize: "medium"', () => { +describe('express with httpIntegration and maxRequestBodySize: "medium"', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs index 96aea9ff619e..6908a019faa3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs @@ -10,9 +10,9 @@ Sentry.init({ integrations: [ Sentry.httpIntegration({ - incomingRequestSpanHook: (span, req, res) => { - span.setAttribute('incomingRequestSpanHook', 'yes'); - Sentry.setExtra('incomingRequestSpanHookCalled', { + onSpanCreated: (span, req, res) => { + span.setAttribute('onSpanCreated', 'yes'); + Sentry.setExtra('onSpanCreatedCalled', { reqUrl: req.url, reqMethod: req.method, resUrl: res.req.url, diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js new file mode 100644 index 000000000000..63d708ef7a75 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js @@ -0,0 +1,64 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +const url = process.env.SERVER_URL; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, + + integrations: [ + Sentry.httpIntegration({ + // Each hook derives its attribute from the objects it is handed, so a hook that fires with + // the wrong span, request or response fails the assertion rather than passing silently. + outgoingRequestHook: (span, request) => { + span.setAttribute('outgoingRequestHook', request.method); + }, + outgoingResponseHook: (span, response) => { + span.setAttribute('outgoingResponseHook', response.statusCode); + }, + outgoingRequestApplyCustomAttributes: (span, request, response) => { + span.setAttribute('outgoingRequestApplyCustomAttributes', `${request.method} ${response.statusCode}`); + }, + }), + ], +}); + +const http = require('http'); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/testOutgoing', (_req, response) => { + makeHttpRequest(`${url}/api/users/42`).then(() => { + response.send({ response: 'done' }); + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); + +function makeHttpRequest(url) { + return new Promise((resolve, reject) => { + http + .get(url, res => { + res.on('data', () => {}); + res.on('end', () => { + resolve(); + }); + }) + .on('error', error => { + reject(error); + }); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts index e1053a706d16..74d99f0f9eb7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -20,9 +20,9 @@ describe('httpIntegration', () => { cleanupChildProcesses(); }); - describe('instrumentation options', () => { + describe('onSpanCreated option', () => { createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument-options.mjs', (createRunner, test) => { - test('allows to configure incomingRequestSpanHook', async () => { + test('allows to configure onSpanCreated', async () => { const runner = createRunner() .expect({ transaction: { @@ -33,14 +33,14 @@ describe('httpIntegration', () => { data: { 'url.full': expect.stringMatching(/\/test$/), 'http.response.status_code': 200, - incomingRequestSpanHook: 'yes', + onSpanCreated: 'yes', }, op: 'http.server', status: 'ok', }, }, extra: expect.objectContaining({ - incomingRequestSpanHookCalled: { + onSpanCreatedCalled: { reqUrl: expect.stringMatching(/\/test$/), reqMethod: 'GET', resUrl: expect.stringMatching(/\/test$/), @@ -56,6 +56,33 @@ describe('httpIntegration', () => { }); }); + describe('outgoing request span hooks', () => { + test('runs outgoingRequestHook, outgoingResponseHook and outgoingRequestApplyCustomAttributes', async () => { + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/users/42', () => {}, 200) + .start(); + + const runner = createRunner(__dirname, 'server-outgoingHooks.js') + .withEnv({ SERVER_URL }) + .expect({ + transaction: event => { + const clientSpans = event.spans?.filter(span => span.op === 'http.client'); + expect(clientSpans).toHaveLength(1); + + // All three hooks run before the span ends, so every attribute has to survive to the envelope. + const data = clientSpans![0]?.data; + expect(data?.['outgoingRequestHook']).toBe('GET'); + expect(data?.['outgoingResponseHook']).toBe(200); + expect(data?.['outgoingRequestApplyCustomAttributes']).toBe('GET 200'); + }, + }) + .start(); + runner.makeRequest('get', '/testOutgoing'); + await runner.completed(); + closeTestServer(); + }); + }); + describe('http.server spans', () => { createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => { test('captures correct attributes for GET requests', async () => { diff --git a/docs/migration/v11-end-state.md b/docs/migration/v11-end-state.md index e127ce331818..299a2d084422 100644 --- a/docs/migration/v11-end-state.md +++ b/docs/migration/v11-end-state.md @@ -516,11 +516,13 @@ Two consequences to be aware of when upgrading: - **Issue grouping:** Grouping in Sentry differs for events with and without stack traces, so you may see new issue groups after upgrading. - **Release health:** Events with a stack trace are counted as errors, so a `captureMessage` call (including messages emitted by `captureConsoleIntegration`) now marks the current session as _errored_. This affects errored-session counts but does **not** mark sessions as crashed, so crash-free session rate is unaffected. If you use `captureMessage` for purely informational output, consider using Sentry Logs instead, which is better suited and does not affect release health. -### Incoming HTTP span hooks moved to `incomingRequestSpanHook` +### Incoming HTTP span hooks moved to `onSpanCreated` Affected SDKs: `@sentry/node` and dependents. -The deprecated `httpIntegration` / `httpServerSpansIntegration` hooks `instrumentation.requestHook`, `instrumentation.responseHook`, and `instrumentation.applyCustomAttributesOnSpan` no longer run for incoming request spans. Use `incomingRequestSpanHook` (on `httpIntegration`) or `onSpanCreated` (on `httpServerSpansIntegration`) instead: +The deprecated `httpIntegration` / `httpServerSpansIntegration` hooks `instrumentation.requestHook`, `instrumentation.responseHook`, and `instrumentation.applyCustomAttributesOnSpan` no longer run for incoming request spans. Use `onSpanCreated` instead. For outgoing request spans, `httpIntegration` has `outgoingRequestHook`, `outgoingResponseHook`, and `outgoingRequestApplyCustomAttributes`. + +In v10 these hooks ran for both directions, so which replacement you want depends on which spans your hook was mutating: ```js // before @@ -532,15 +534,20 @@ Sentry.httpIntegration({ }, }); -// after +// after — incoming (server) spans Sentry.httpIntegration({ - incomingRequestSpanHook: (span, req, res) => { + onSpanCreated: (span, req, res) => { span.setAttribute('custom', true); }, }); -``` -`httpIntegration`'s `instrumentation` option is still honored for **outgoing** request spans. +// after — outgoing (client) spans +Sentry.httpIntegration({ + outgoingRequestHook: (span, req) => { + span.setAttribute('custom', true); + }, +}); +``` ### Node HTTP transport `keepAlive` defaults to `true` @@ -843,7 +850,54 @@ Sentry.init({ - (Express) The deprecated `patchExpressModule(options)` signature was removed. Use `patchExpressModule(moduleExports, getOptions)` instead. - The `@sentry/node-core/light/otlp` entry point was removed, along with its optional `@opentelemetry/exporter-trace-otlp-http` peer dependency. `otlpIntegration` is now exported directly from every server-side SDK, so `Sentry.otlpIntegration()` needs no extra import or install. - The `otlpIntegration` options `setupOtlpTracesExporter` and `collectorUrl` were removed, and the integration no longer sets up a span exporter, span processor, or tracer provider. Configure your own exporter and point it at `Sentry.getOtlpTracesEndpoint(dsn)`, or at your collector's URL if you route through one. See [Connecting Sentry to your OpenTelemetry traces](#connecting-sentry-to-your-opentelemetry-traces). -- The deprecated `httpServerSpansIntegration` `instrumentation.{requestHook,responseHook,applyCustomAttributesOnSpan}` option was removed. Use `onSpanCreated`, or `httpIntegration({ incomingRequestSpanHook })`, to mutate incoming request spans. +- The deprecated `httpServerSpansIntegration` `instrumentation.{requestHook,responseHook,applyCustomAttributesOnSpan}` option was removed. Use `onSpanCreated` instead. `httpServerSpansIntegration` only covers incoming requests; the outgoing hooks (`outgoingRequestHook`, `outgoingResponseHook`, `outgoingRequestApplyCustomAttributes`) are on `httpIntegration`. + +#### `httpIntegration` options were consolidated + +`httpIntegration` option names now match `httpServerIntegration` / `httpServerSpansIntegration` and the other server SDKs. The deprecated `instrumentation` hooks were removed. + +| Removed option | Replacement | +| --------------------------------------------- | ------------------------------------------------------------------------------- | +| `trackIncomingRequestsAsSessions` | `sessions` | +| `maxIncomingRequestBodySize` | `maxRequestBodySize` | +| `ignoreIncomingRequestBody` | `ignoreRequestBody` | +| `dropSpansForIncomingRequestStatusCodes` | `ignoreStatusCodes` | +| `incomingRequestSpanHook` | `onSpanCreated` | +| `instrumentation.requestHook` | `onSpanCreated` (incoming) or `outgoingRequestHook` (outgoing) | +| `instrumentation.responseHook` | `onSpanCreated` (incoming) or `outgoingResponseHook` (outgoing) | +| `instrumentation.applyCustomAttributesOnSpan` | `onSpanCreated` (incoming) or `outgoingRequestApplyCustomAttributes` (outgoing) | + +```js +// before +Sentry.httpIntegration({ + trackIncomingRequestsAsSessions: false, + maxIncomingRequestBodySize: 'small', + ignoreIncomingRequestBody: url => url.includes('/health'), + dropSpansForIncomingRequestStatusCodes: [404], + incomingRequestSpanHook: (span, req, res) => { + span.setAttribute('custom', true); + }, + instrumentation: { + responseHook: () => { + void flushIfServerless(); + }, + }, +}); + +// after +Sentry.httpIntegration({ + sessions: false, + maxRequestBodySize: 'small', + ignoreRequestBody: url => url.includes('/health'), + ignoreStatusCodes: [404], + onSpanCreated: (span, req, res) => { + span.setAttribute('custom', true); + }, + outgoingResponseHook: () => { + void flushIfServerless(); + }, +}); +``` ### `@sentry/cloudflare` diff --git a/packages/core/src/integrations/http/types.ts b/packages/core/src/integrations/http/types.ts index 1ff9e18a0dbd..1dbb9cc54731 100644 --- a/packages/core/src/integrations/http/types.ts +++ b/packages/core/src/integrations/http/types.ts @@ -223,7 +223,7 @@ export interface HttpInstrumentationOptions { sessions?: boolean; /** - * Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate. + * Number of milliseconds until sessions tracked with `sessions` will be flushed as a session aggregate. * * Defaults to `60000` (60s). */ diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 773b87cc33f7..597909b8d7db 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -1,6 +1,6 @@ import { subscribe } from 'node:diagnostics_channel'; import { context, trace } from '@opentelemetry/api'; -import type { ClientRequest, IncomingMessage, ServerResponse } from 'node:http'; +import type { ClientRequest, IncomingMessage } from 'node:http'; import type { HttpClientRequest, HttpIncomingMessage, HttpInstrumentationOptions, Span } from '@sentry/core'; import { getHttpClientSubscriptions, @@ -18,7 +18,7 @@ const FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL = (NODE_VERSION.major === 23 && NODE_VERSION.minor >= 2) || NODE_VERSION.major >= 24; -interface OutgoingHttpRequestInstrumentationOptions { +export interface OutgoingHttpRequestInstrumentationOptions { /** * Whether breadcrumbs should be recorded for outgoing requests. * @@ -27,9 +27,7 @@ interface OutgoingHttpRequestInstrumentationOptions { breadcrumbs?: boolean; /** - * Whether to create spans for outgoing requests (user preference). - * This only takes effect if `createSpansForOutgoingRequests` is not disabled. - * If `createSpansForOutgoingRequests` is explicitly set false, this option is ignored. + * Whether to create spans for outgoing requests. * * @default `true` */ @@ -40,14 +38,7 @@ interface OutgoingHttpRequestInstrumentationOptions { * * @default `true` */ - propagateTraceInOutgoingRequests?: boolean; - - /** - * @deprecated Use spans option instead. - * - * @default `true` - */ - createSpansForOutgoingRequests?: boolean; + propagateTrace?: boolean; /** * Do not instrument outgoing HTTP requests to URLs where the given callback returns `true`. @@ -62,7 +53,6 @@ interface OutgoingHttpRequestInstrumentationOptions { /** * Hooks for outgoing request spans, only called when spans are created for outgoing requests * (i.e. when `spans` is enabled). - * These mirror the OTEL HttpInstrumentation hooks for backwards compatibility. */ outgoingRequestHook?: (span: Span, request: ClientRequest | HttpClientRequest) => void; outgoingResponseHook?: (span: Span, response: IncomingMessage | HttpIncomingMessage) => void; @@ -73,82 +63,21 @@ interface OutgoingHttpRequestInstrumentationOptions { ) => void; } -export type SentryHttpInstrumentationOptions = OutgoingHttpRequestInstrumentationOptions & { - // All options below do not do anything anymore in this instrumentation, and will be removed in the future. - // They are only kept here for backwards compatibility - the respective functionality is now handled by the httpServerIntegration/httpServerSpansIntegration. - - /** - * @deprecated This no longer does anything. - */ - extractIncomingTraceFromHeader?: boolean; - - /** - * @deprecated This no longer does anything. - */ - ignoreStaticAssets?: boolean; - - /** - * @deprecated This no longer does anything. - */ - disableIncomingRequestSpans?: boolean; - - /** - * @deprecated This no longer does anything. - */ - ignoreSpansForIncomingRequests?: (urlPath: string, request: IncomingMessage) => boolean; - - /** - * @deprecated This no longer does anything. - */ - ignoreIncomingRequestBody?: (url: string, request: http.RequestOptions) => boolean; - - /** - * @deprecated This no longer does anything. - */ - maxIncomingRequestBodySize?: 'none' | 'small' | 'medium' | 'always'; - - /** - * @deprecated This no longer does anything. - */ - trackIncomingRequestsAsSessions?: boolean; - - /** - * @deprecated This no longer does anything. - */ - instrumentation?: { - requestHook?: (span: Span, req: ClientRequest | IncomingMessage) => void; - responseHook?: (span: Span, response: IncomingMessage | ServerResponse) => void; - applyCustomAttributesOnSpan?: ( - span: Span, - request: ClientRequest | IncomingMessage, - response: IncomingMessage | ServerResponse, - ) => void; - }; - - /** - * @deprecated This no longer does anything. - */ - sessionFlushingDelayMS?: number; -}; - /** * This instruments the http modules for outgoing requests. * It uses the diagnostics channel if available, otherwise it falls back to monkey-patching. * * The instrumentation will start spans, create breadcrumbs, and propagate trace headers in outgoing requests (depending on the settings). - * - * @TODO Cleanup options in v11 */ export function instrumentHttpOutgoingRequests( instrumentationOptions: OutgoingHttpRequestInstrumentationOptions = {}, ): void { const { outgoingRequestApplyCustomAttributes: applyCustomAttributesOnSpan, ...options } = instrumentationOptions; const patchOptions = { - propagateTrace: options.propagateTraceInOutgoingRequests ?? true, applyCustomAttributesOnSpan, ...options, - // oxlint-disable-next-line typescript/no-deprecated - spans: options.createSpansForOutgoingRequests !== false && (options.spans ?? true), + propagateTrace: options.propagateTrace ?? true, + spans: options.spans ?? true, ignoreOutgoingRequests(url, request) { return isTracingSuppressed() || !!options.ignoreOutgoingRequests?.(url, getRequestOptions(request)); }, diff --git a/packages/node/src/integrations/http/httpServerIntegration.ts b/packages/node/src/integrations/http/httpServerIntegration.ts index 21e697a7bdf0..3d5610a5cfa4 100644 --- a/packages/node/src/integrations/http/httpServerIntegration.ts +++ b/packages/node/src/integrations/http/httpServerIntegration.ts @@ -39,7 +39,7 @@ export interface HttpServerIntegrationOptions { sessions?: boolean; /** - * Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate. + * Number of milliseconds until sessions tracked with `sessions` will be flushed as a session aggregate. * * Defaults to `60000` (60s). */ diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index 36b71950c721..ca720400dea3 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -1,5 +1,5 @@ import type { RequestOptions } from 'node:http'; -import type { HttpClientRequest, HttpIncomingMessage, HttpServerResponse, Span } from '@sentry/core'; +import type { HttpClientRequest, Span } from '@sentry/core'; import { URL_FULL } from '@sentry/conventions/attributes'; import { defineIntegration, @@ -13,14 +13,12 @@ import type { HttpServerIntegrationOptions } from './httpServerIntegration'; import { httpServerIntegration } from './httpServerIntegration'; import type { HttpServerSpansIntegrationOptions } from './httpServerSpansIntegration'; import { httpServerSpansIntegration } from './httpServerSpansIntegration'; -import type { SentryHttpInstrumentationOptions } from './SentryHttpInstrumentation'; +import type { OutgoingHttpRequestInstrumentationOptions } from './SentryHttpInstrumentation'; import { instrumentHttpOutgoingRequests } from './SentryHttpInstrumentation'; const INTEGRATION_NAME = 'Http' as const; -// TODO(v11): Consolidate all the various HTTP integration options into one, -// and deprecate the duplicated and aliased options. -interface HttpOptions { +interface HttpOptions extends HttpServerIntegrationOptions, HttpServerSpansIntegrationOptions { /** * Whether breadcrumbs should be recorded for outgoing requests. * Defaults to true @@ -36,21 +34,6 @@ interface HttpOptions { */ spans?: boolean; - /** - * Whether the integration should create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming requests to track the health and crash-free rate of your releases in Sentry. - * Read more about Release Health: https://docs.sentry.io/product/releases/health/ - * - * Defaults to `true`. - */ - trackIncomingRequestsAsSessions?: boolean; - - /** - * Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate. - * - * Defaults to `60000` (60s). - */ - sessionFlushingDelayMS?: number; - /** * Whether to inject trace propagation headers (sentry-trace, baggage, traceparent) into outgoing HTTP requests. * @@ -75,84 +58,28 @@ interface HttpOptions { ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean; /** - * Do not capture spans for incoming HTTP requests to URLs where the given callback returns `true`. - * Spans will be non recording if tracing is disabled. - * - * The `urlPath` param consists of the URL path and query string (if any) of the incoming request. - * For example: `'/users/details?id=123'` - * - * The `request` param contains the original {@type IncomingMessage} object of the incoming request. - * You can use it to filter on additional properties like method, headers, etc. - */ - ignoreIncomingRequests?: (urlPath: string, request: HttpIncomingMessage) => boolean; - - /** - * 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. - */ - incomingRequestSpanHook?: (span: Span, request: HttpIncomingMessage, response: HttpServerResponse) => void; - - /** - * Whether to automatically ignore common static asset requests like favicon.ico, robots.txt, etc. - * This helps reduce noise in your transactions. - * - * @default `true` - */ - 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]]` - */ - dropSpansForIncomingRequestStatusCodes?: (number | [number, number])[]; - - /** - * Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`. - * 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. + * If true, do not generate spans for incoming requests at all. + * This is used by Remix to avoid generating spans for incoming requests, as it generates its own spans. */ - ignoreIncomingRequestBody?: (url: string, request: RequestOptions) => boolean; + disableIncomingRequestSpans?: boolean; /** - * Controls the maximum size of incoming HTTP request bodies attached to events. - * - * Available options: - * - 'none': No request bodies will be attached - * - 'small': Request bodies up to 1,000 bytes will be attached - * - 'medium': Request bodies up to 10,000 bytes will be attached (default) - * - 'always': Request bodies will always be attached - * - * Note that even with 'always' setting, bodies exceeding 1MB will never be attached - * for performance and security reasons. - * - * @default 'medium' + * Called after an outgoing request span is created. + * Only invoked when spans are created for outgoing requests. */ - maxIncomingRequestBodySize?: 'none' | 'small' | 'medium' | 'always'; + outgoingRequestHook?: OutgoingHttpRequestInstrumentationOptions['outgoingRequestHook']; /** - * If true, do not generate spans for incoming requests at all. - * This is used by Remix to avoid generating spans for incoming requests, as it generates its own spans. + * Called when the outgoing request receives a response. + * Only invoked when spans are created for outgoing requests. */ - disableIncomingRequestSpans?: boolean; + outgoingResponseHook?: OutgoingHttpRequestInstrumentationOptions['outgoingResponseHook']; /** - * Hooks for outgoing HTTP request spans. - * These no longer run for incoming request spans; use `incomingRequestSpanHook` for those. + * Called when both the outgoing request and response are available. + * Only invoked when spans are created for outgoing requests. */ - instrumentation?: { - requestHook?: (span: Span, req: HttpIncomingMessage | HttpClientRequest) => void; - responseHook?: (span: Span, response: HttpIncomingMessage | HttpServerResponse) => void; - applyCustomAttributesOnSpan?: ( - span: Span, - request: HttpIncomingMessage | HttpClientRequest, - response: HttpIncomingMessage | HttpServerResponse, - ) => void; - }; + outgoingRequestApplyCustomAttributes?: OutgoingHttpRequestInstrumentationOptions['outgoingRequestApplyCustomAttributes']; } /** @@ -161,25 +88,10 @@ interface HttpOptions { */ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => { const spans = options.spans ?? true; - const disableIncomingRequestSpans = options.disableIncomingRequestSpans; - const enableServerSpans = spans && !disableIncomingRequestSpans; - - const serverOptions = { - sessions: options.trackIncomingRequestsAsSessions, - sessionFlushingDelayMS: options.sessionFlushingDelayMS, - ignoreRequestBody: options.ignoreIncomingRequestBody, - maxRequestBodySize: options.maxIncomingRequestBodySize, - } satisfies HttpServerIntegrationOptions; - - const serverSpansOptions: HttpServerSpansIntegrationOptions = { - ignoreIncomingRequests: options.ignoreIncomingRequests, - ignoreStaticAssets: options.ignoreStaticAssets, - ignoreStatusCodes: options.dropSpansForIncomingRequestStatusCodes, - onSpanCreated: options.incomingRequestSpanHook, - }; + const enableServerSpans = spans && !options.disableIncomingRequestSpans; - const server = httpServerIntegration(serverOptions); - const serverSpans = httpServerSpansIntegration(serverSpansOptions); + const server = httpServerIntegration(options); + const serverSpans = httpServerSpansIntegration(options); return { name: INTEGRATION_NAME, @@ -193,12 +105,10 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => setupOnce() { server.setupOnce(); - const sentryHttpInstrumentationOptions: SentryHttpInstrumentationOptions = { + const outgoingRequestOptions: OutgoingHttpRequestInstrumentationOptions = { breadcrumbs: options.breadcrumbs, spans, - propagateTraceInOutgoingRequests: options.tracePropagation ?? true, - // oxlint-disable-next-line typescript/no-deprecated -- deprecated alias kept until removal - createSpansForOutgoingRequests: spans, + propagateTrace: options.tracePropagation ?? true, ignoreOutgoingRequests: options.ignoreOutgoingRequests, outgoingRequestHook: (span: Span, request: HttpClientRequest) => { // Sanitize data URLs to prevent long base64 strings in span attributes @@ -211,17 +121,17 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => [URL_FULL]: sanitizedUrl, }); } - options.instrumentation?.requestHook?.(span, request); + options.outgoingRequestHook?.(span, request); }, - outgoingResponseHook: options.instrumentation?.responseHook, - outgoingRequestApplyCustomAttributes: options.instrumentation?.applyCustomAttributesOnSpan, + outgoingResponseHook: options.outgoingResponseHook, + outgoingRequestApplyCustomAttributes: options.outgoingRequestApplyCustomAttributes, }; // This is Sentry-specific instrumentation for outgoing request // breadcrumbs & trace propagation. It uses the diagnostic channels on // node versions that support it, falling back to monkey-patching when // needed. - instrumentHttpOutgoingRequests(sentryHttpInstrumentationOptions); + instrumentHttpOutgoingRequests(outgoingRequestOptions); }, processEvent(event) { // Always run this, even if spans are disabled diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 7ff099f291e9..90f3972c8a01 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,19 +1,7 @@ import * as path from 'node:path'; -import type { Client, Event, EventProcessor, Integration } from '@sentry/core'; -import { - applySdkMetadata, - debug, - DEFAULT_ENVIRONMENT, - DEV_ENVIRONMENT, - flushIfServerless, - getGlobalScope, -} from '@sentry/core'; -import { - getDefaultIntegrations as getDefaultNodeIntegrations, - httpIntegration, - init as initNode, - type NodeOptions, -} from '@sentry/node'; +import type { Client, Event, EventProcessor } from '@sentry/core'; +import { applySdkMetadata, debug, DEFAULT_ENVIRONMENT, DEV_ENVIRONMENT, getGlobalScope } from '@sentry/core'; +import { init as initNode } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; import type { SentryNuxtServerOptions } from '../common/types'; @@ -34,7 +22,6 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { const sentryOptions = { environment: options.environment ?? process.env.SENTRY_ENVIRONMENT ?? envFallback, - defaultIntegrations: getNuxtDefaultIntegrations(options), ...options, }; @@ -101,22 +88,6 @@ export function clientSourceMapErrorFilter(options: SentryNuxtServerOptions): Ev ); } -function getNuxtDefaultIntegrations(options: NodeOptions): Integration[] { - return [ - ...getDefaultNodeIntegrations(options).filter(integration => integration.name !== 'Http'), - // The httpIntegration is added as defaultIntegration, so users can still overwrite it - httpIntegration({ - incomingRequestSpanHook: () => { - // Flush eagerly on serverless platforms, where the function may be frozen before the transport - // sends, handing the flush to a platform `waitUntil` where one exists so it doesn't block. On a - // long-running server this is a no-op, so pending outcomes keep aggregating on the flush interval - // instead of shipping one client_report envelope per response. - void flushIfServerless(); - }, - }), - ]; -} - /** * Checks if the event is a cache event. */ diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index a83a857dbfe2..c812cc1fe441 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -41,14 +41,14 @@ describe('Nuxt Server SDK', () => { expect(init({})).not.toBeUndefined(); }); - it('uses default integrations when not provided in options', () => { + it('delegates default integrations to initNode when not provided in options', () => { + // Resolving them here would pin the selection to the raw options, before `initNode` + // resolves `SENTRY_TRACES_SAMPLE_RATE`, and would drop the performance integrations + // for anyone enabling tracing purely through the environment. init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); expect(nodeInit).toHaveBeenCalledTimes(1); - const callArgs = nodeInit.mock.calls[0]?.[0]; - expect(callArgs).toBeDefined(); - expect(callArgs?.defaultIntegrations).toBeDefined(); - expect(Array.isArray(callArgs?.defaultIntegrations)).toBe(true); + expect(nodeInit).toHaveBeenCalledWith(expect.not.objectContaining({ defaultIntegrations: expect.anything() })); }); it('allows options.defaultIntegrations to override default integrations', () => {