From 1da2110f42772b920b131b79cd5a37b84aa1d086 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 9 Mar 2026 12:35:57 +0100 Subject: [PATCH] feat(node-core,node): Add tracePropagation option to http and fetch integrations Add a `tracePropagation` option to `httpIntegration` and `nativeNodeFetchIntegration` that allows disabling Sentry's trace header injection (sentry-trace, baggage, traceparent) while still creating breadcrumbs. This is useful when `skipOpenTelemetrySetup: true` is configured and an external OTel setup handles trace propagation, avoiding duplicate headers. Closes: #19689 --- .../fetch-no-trace-propagation/instrument.mjs | 12 ++++ .../fetch-no-trace-propagation/scenario.mjs | 12 ++++ .../fetch-no-trace-propagation/test.ts | 66 +++++++++++++++++++ .../http-no-trace-propagation/instrument.mjs | 12 ++++ .../http-no-trace-propagation/scenario.mjs | 28 ++++++++ .../http-no-trace-propagation/test.ts | 66 +++++++++++++++++++ .../fetch-no-trace-propagation/instrument.mjs | 9 +++ .../fetch-no-trace-propagation/scenario.mjs | 12 ++++ .../fetch-no-trace-propagation/test.ts | 66 +++++++++++++++++++ .../http-no-trace-propagation/instrument.mjs | 9 +++ .../http-no-trace-propagation/scenario.mjs | 28 ++++++++ .../http-no-trace-propagation/test.ts | 66 +++++++++++++++++++ .../node-core/src/integrations/http/index.ts | 12 +++- .../SentryNodeFetchInstrumentation.ts | 15 ++++- .../src/integrations/node-fetch/index.ts | 7 ++ .../src/light/integrations/httpIntegration.ts | 17 ++++- .../nativeNodeFetchIntegration.ts | 17 ++++- packages/node/src/integrations/http.ts | 14 +++- packages/node/src/integrations/node-fetch.ts | 11 ++++ 19 files changed, 472 insertions(+), 7 deletions(-) create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/test.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/test.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs new file mode 100644 index 000000000000..5d893943996a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [Sentry.nativeNodeFetchIntegration({ tracePropagation: false })], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/scenario.mjs new file mode 100644 index 000000000000..34abb96fbafa --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/scenario.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node-core'; + +async function run() { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + + Sentry.captureException(new Error('foo')); +} + +run(); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/test.ts new file mode 100644 index 000000000000..dad27aa5dc17 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/test.ts @@ -0,0 +1,66 @@ +import { createTestServer } from '@sentry-internal/test-utils'; +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; + +describe('outgoing fetch with tracePropagation disabled', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('does not inject trace headers but still creates breadcrumbs', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['baggage']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['baggage']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 200, + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 200, + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs new file mode 100644 index 000000000000..149f77fe762a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [Sentry.httpIntegration({ tracePropagation: false, spans: false })], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/scenario.mjs new file mode 100644 index 000000000000..600fdf5ea245 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/scenario.mjs @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +async function run() { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + + Sentry.captureException(new Error('foo')); +} + +run(); + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/test.ts new file mode 100644 index 000000000000..5ba5c63da74f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-trace-propagation/test.ts @@ -0,0 +1,66 @@ +import { createTestServer } from '@sentry-internal/test-utils'; +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; + +describe('outgoing http with tracePropagation disabled', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('does not inject trace headers but still creates breadcrumbs', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['baggage']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['baggage']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 200, + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 200, + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs new file mode 100644 index 000000000000..1330c5c2baed --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [Sentry.nativeNodeFetchIntegration({ tracePropagation: false })], + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/scenario.mjs new file mode 100644 index 000000000000..6835e3d4ca6f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/scenario.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node'; + +async function run() { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + + Sentry.captureException(new Error('foo')); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/test.ts new file mode 100644 index 000000000000..dad27aa5dc17 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/test.ts @@ -0,0 +1,66 @@ +import { createTestServer } from '@sentry-internal/test-utils'; +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; + +describe('outgoing fetch with tracePropagation disabled', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('does not inject trace headers but still creates breadcrumbs', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['baggage']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['baggage']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 200, + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 200, + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs new file mode 100644 index 000000000000..3eacba8fd6c8 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [Sentry.httpIntegration({ tracePropagation: false, spans: false })], + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/scenario.mjs new file mode 100644 index 000000000000..85621ba88d37 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/scenario.mjs @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/node'; +import * as http from 'http'; + +async function run() { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + + Sentry.captureException(new Error('foo')); +} + +run(); + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/test.ts new file mode 100644 index 000000000000..5ba5c63da74f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/test.ts @@ -0,0 +1,66 @@ +import { createTestServer } from '@sentry-internal/test-utils'; +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; + +describe('outgoing http with tracePropagation disabled', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('does not inject trace headers but still creates breadcrumbs', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['baggage']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['baggage']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 200, + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 200, + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/packages/node-core/src/integrations/http/index.ts b/packages/node-core/src/integrations/http/index.ts index 19859b68f3c0..34cb86704415 100644 --- a/packages/node-core/src/integrations/http/index.ts +++ b/packages/node-core/src/integrations/http/index.ts @@ -41,6 +41,16 @@ interface HttpOptions { */ sessionFlushingDelayMS?: number; + /** + * Whether to inject trace propagation headers (sentry-trace, baggage, traceparent) into outgoing HTTP requests. + * + * When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs + * (if `breadcrumbs` is enabled). + * + * @default `true` + */ + tracePropagation?: boolean; + /** * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. @@ -141,7 +151,7 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => const httpInstrumentationOptions: SentryHttpInstrumentationOptions = { breadcrumbs: options.breadcrumbs, - propagateTraceInOutgoingRequests: true, + propagateTraceInOutgoingRequests: options.tracePropagation ?? true, ignoreOutgoingRequests: options.ignoreOutgoingRequests, }; diff --git a/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index cb972353cabf..513874f6733c 100644 --- a/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -20,6 +20,17 @@ export type SentryNodeFetchInstrumentationOptions = InstrumentationConfig & { */ breadcrumbs?: boolean; + /** + * Whether to inject trace propagation headers (sentry-trace, baggage, traceparent) into outgoing fetch requests. + * + * When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs + * (if `breadcrumbs` is enabled). This is useful when `skipOpenTelemetrySetup: true` is configured and you want + * to avoid duplicate trace headers being injected by both Sentry and OpenTelemetry's UndiciInstrumentation. + * + * @default `true` + */ + tracePropagation?: boolean; + /** * Do not capture breadcrumbs or inject headers for outgoing fetch requests to URLs where the given callback returns `true`. * The same option can be passed to the top-level httpIntegration where it controls both, breadcrumb and @@ -118,7 +129,9 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase { maxRequestBodySize: options.maxRequestBodySize ?? 'medium', ignoreRequestBody: options.ignoreRequestBody, breadcrumbs: options.breadcrumbs ?? true, + tracePropagation: options.tracePropagation ?? true, ignoreOutgoingRequests: options.ignoreOutgoingRequests, }; @@ -212,7 +223,7 @@ function instrumentServer( function onOutgoingRequestCreated( request: ClientRequest, - options: { ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean }, + options: { tracePropagation: boolean; ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean }, propagationDecisionMap: LRUMap, ignoreOutgoingRequestsMap: WeakMap, ): void { @@ -223,7 +234,9 @@ function onOutgoingRequestCreated( return; } - addTracePropagationHeadersToOutgoingRequest(request, propagationDecisionMap); + if (options.tracePropagation) { + addTracePropagationHeadersToOutgoingRequest(request, propagationDecisionMap); + } } function onOutgoingRequestFinish( diff --git a/packages/node-core/src/light/integrations/nativeNodeFetchIntegration.ts b/packages/node-core/src/light/integrations/nativeNodeFetchIntegration.ts index 7b6a884369ff..9661b028207f 100644 --- a/packages/node-core/src/light/integrations/nativeNodeFetchIntegration.ts +++ b/packages/node-core/src/light/integrations/nativeNodeFetchIntegration.ts @@ -19,6 +19,16 @@ export interface NativeNodeFetchIntegrationOptions { */ breadcrumbs?: boolean; + /** + * Whether to inject trace propagation headers (sentry-trace, baggage, traceparent) into outgoing fetch requests. + * + * When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs + * (if `breadcrumbs` is enabled). + * + * @default `true` + */ + tracePropagation?: boolean; + /** * Do not capture breadcrumbs or inject headers for outgoing fetch requests to URLs * where the given callback returns `true`. @@ -31,6 +41,7 @@ export interface NativeNodeFetchIntegrationOptions { const _nativeNodeFetchIntegration = ((options: NativeNodeFetchIntegrationOptions = {}) => { const _options = { breadcrumbs: options.breadcrumbs ?? true, + tracePropagation: options.tracePropagation ?? true, ignoreOutgoingRequests: options.ignoreOutgoingRequests, }; @@ -69,7 +80,7 @@ export const nativeNodeFetchIntegration = _nativeNodeFetchIntegration as ( function onUndiciRequestCreated( request: UndiciRequest, - options: { ignoreOutgoingRequests?: (url: string) => boolean }, + options: { tracePropagation: boolean; ignoreOutgoingRequests?: (url: string) => boolean }, propagationDecisionMap: LRUMap, ignoreOutgoingRequestsMap: WeakMap, ): void { @@ -80,7 +91,9 @@ function onUndiciRequestCreated( return; } - addTracePropagationHeadersToFetchRequest(request, propagationDecisionMap); + if (options.tracePropagation) { + addTracePropagationHeadersToFetchRequest(request, propagationDecisionMap); + } } function onUndiciResponseHeaders( diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 7c2cadf9eb43..7bde0ae40a21 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -57,6 +57,17 @@ interface HttpOptions { */ sessionFlushingDelayMS?: number; + /** + * Whether to inject trace propagation headers (sentry-trace, baggage, traceparent) into outgoing HTTP requests. + * + * When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs + * (if `breadcrumbs` is enabled). This is useful when `skipOpenTelemetrySetup: true` is configured and you want + * to avoid duplicate trace headers being injected by both Sentry and OpenTelemetry's HttpInstrumentation. + * + * @default `true` + */ + tracePropagation?: boolean; + /** * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. @@ -246,7 +257,8 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => const sentryHttpInstrumentationOptions = { breadcrumbs: options.breadcrumbs, - propagateTraceInOutgoingRequests: !useOtelHttpInstrumentation, + propagateTraceInOutgoingRequests: + typeof options.tracePropagation === 'boolean' ? options.tracePropagation : !useOtelHttpInstrumentation, ignoreOutgoingRequests: options.ignoreOutgoingRequests, } satisfies SentryHttpInstrumentationOptions; diff --git a/packages/node/src/integrations/node-fetch.ts b/packages/node/src/integrations/node-fetch.ts index a226ae3e6da5..2a1e1cac9098 100644 --- a/packages/node/src/integrations/node-fetch.ts +++ b/packages/node/src/integrations/node-fetch.ts @@ -32,6 +32,17 @@ interface NodeFetchOptions extends Pick