From ed112411412003152e0d143011abfc4e16375d92 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 19 Mar 2025 12:57:06 +0100 Subject: [PATCH] fix(node): Ensure incoming traces are propagated without HttpInstrumentation --- .../http/SentryHttpInstrumentation.ts | 39 ++++++++++++++----- packages/node/src/integrations/http/index.ts | 27 +++++++------ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 8c7b729b8828..3db9df73a196 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -1,8 +1,5 @@ /* eslint-disable max-lines */ -import type * as http from 'node:http'; -import type { IncomingMessage, RequestOptions } from 'node:http'; -import type * as https from 'node:https'; -import type { EventEmitter } from 'node:stream'; +import { context, propagation } from '@opentelemetry/api'; import { VERSION } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; @@ -12,6 +9,7 @@ import { generateSpanId, getBreadcrumbLogLevelFromHttpStatusCode, getClient, + getCurrentScope, getIsolationScope, getSanitizedUrlString, httpRequestToRequestData, @@ -19,17 +17,20 @@ import { parseUrl, stripUrlQueryAndFragment, withIsolationScope, - withScope, } from '@sentry/core'; +import type * as http from 'node:http'; +import type { IncomingMessage, RequestOptions } from 'node:http'; +import type * as https from 'node:https'; +import type { EventEmitter } from 'node:stream'; import { DEBUG_BUILD } from '../../debug-build'; import { getRequestUrl } from '../../utils/getRequestUrl'; -import { getRequestInfo } from './vendor/getRequestInfo'; import { stealthWrap } from './utils'; +import { getRequestInfo } from './vendor/getRequestInfo'; type Http = typeof http; type Https = typeof https; -type SentryHttpInstrumentationOptions = InstrumentationConfig & { +export type SentryHttpInstrumentationOptions = InstrumentationConfig & { /** * Whether breadcrumbs should be recorded for requests. * @@ -37,6 +38,15 @@ type SentryHttpInstrumentationOptions = InstrumentationConfig & { */ breadcrumbs?: boolean; + /** + * Whether to extract the trace ID from the `sentry-trace` header for incoming requests. + * By default this is done by the HttpInstrumentation, but if that is not added (e.g. because tracing is disabled, ...) + * then this instrumentation can take over. + * + * @default `false` + */ + extractIncomingTraceFromHeader?: boolean; + /** * Do not capture breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. * For the scope of this instrumentation, this callback only controls breadcrumb creation. @@ -185,9 +195,18 @@ export class SentryHttpInstrumentation extends InstrumentationBase { - return withScope(scope => { - // Set a new propagationSpanId for this request - scope.getPropagationContext().propagationSpanId = generateSpanId(); + // Set a new propagationSpanId for this request + // We rely on the fact that `withIsolationScope()` will implicitly also fork the current scope + // This way we can save an "unnecessary" `withScope()` invocation + getCurrentScope().getPropagationContext().propagationSpanId = generateSpanId(); + + // If we don't want to extract the trace from the header, we can skip this + if (!instrumentation.getConfig().extractIncomingTraceFromHeader) { + return original.apply(this, [event, ...args]); + } + + const ctx = propagation.extract(context.active(), normalizedRequest.headers); + return context.with(ctx, () => { return original.apply(this, [event, ...args]); }); }); diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index 6e7581b75c8d..0df3fc56b480 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -10,6 +10,7 @@ import type { HTTPModuleRequestIncomingMessage } from '../../transports/http-mod import type { NodeClientOptions } from '../../types'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; import { getRequestUrl } from '../../utils/getRequestUrl'; +import type { SentryHttpInstrumentationOptions } from './SentryHttpInstrumentation'; import { SentryHttpInstrumentation } from './SentryHttpInstrumentation'; import { SentryHttpInstrumentationBeforeOtel } from './SentryHttpInstrumentationBeforeOtel'; @@ -102,19 +103,12 @@ const instrumentSentryHttpBeforeOtel = generateInstrumentOnce(`${INTEGRATION_NAM return new SentryHttpInstrumentationBeforeOtel(); }); -const instrumentSentryHttp = generateInstrumentOnce<{ - breadcrumbs?: HttpOptions['breadcrumbs']; - ignoreOutgoingRequests?: HttpOptions['ignoreOutgoingRequests']; - trackIncomingRequestsAsSessions?: HttpOptions['trackIncomingRequestsAsSessions']; - sessionFlushingDelayMS?: HttpOptions['sessionFlushingDelayMS']; -}>(`${INTEGRATION_NAME}.sentry`, options => { - return new SentryHttpInstrumentation({ - breadcrumbs: options?.breadcrumbs, - ignoreOutgoingRequests: options?.ignoreOutgoingRequests, - trackIncomingRequestsAsSessions: options?.trackIncomingRequestsAsSessions, - sessionFlushingDelayMS: options?.sessionFlushingDelayMS, - }); -}); +const instrumentSentryHttp = generateInstrumentOnce( + `${INTEGRATION_NAME}.sentry`, + options => { + return new SentryHttpInstrumentation(options); + }, +); export const instrumentOtelHttp = generateInstrumentOnce(INTEGRATION_NAME, config => { const instrumentation = new HttpInstrumentation(config); @@ -161,7 +155,12 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => // This is the Sentry-specific instrumentation that isolates requests & creates breadcrumbs // Note that this _has_ to be wrapped after the OTEL instrumentation, // otherwise the isolation will not work correctly - instrumentSentryHttp(options); + instrumentSentryHttp({ + ...options, + // If spans are not instrumented, it means the HttpInstrumentation has not been added + // In that case, we want to handle incoming trace extraction ourselves + extractIncomingTraceFromHeader: !instrumentSpans, + }); }, }; });