From 6ca2a3b15532d67ec4c894a6c6730af0f738e3c2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 15 Jul 2026 14:11:44 +0200 Subject: [PATCH 1/2] ref(node): Refactor some otel internals in http integrations --- .../integrations/http/client-subscriptions.ts | 2 + .../http/SentryHttpInstrumentation.ts | 1 - .../http/httpServerSpansIntegration.ts | 80 ++++++++++--------- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/packages/core/src/integrations/http/client-subscriptions.ts b/packages/core/src/integrations/http/client-subscriptions.ts index 71d78873dbb2..4a80987f6918 100644 --- a/packages/core/src/integrations/http/client-subscriptions.ts +++ b/packages/core/src/integrations/http/client-subscriptions.ts @@ -16,6 +16,7 @@ import type { SpanStatus } from '../../types/spanStatus'; import { addOutgoingRequestBreadcrumb } from './add-outgoing-request-breadcrumb'; import { + bindScopeToEmitter, getSpanStatusFromHttpCode, SPAN_STATUS_ERROR, SPAN_STATUS_UNSET, @@ -156,6 +157,7 @@ export function getHttpClientSubscriptions(options: HttpInstrumentationOptions): response.resume(); } setIncomingResponseSpanData(response, span); + bindScopeToEmitter(response); options.outgoingResponseHook?.(span, response); let finished = false; diff --git a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts index 78fd0587239c..8b17458d7fb9 100644 --- a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts @@ -189,7 +189,6 @@ export function instrumentHttpOutgoingRequests( }, outgoingResponseHook(span, response) { options.outgoingResponseHook?.(span, response); - context.bind(context.active(), response); }, errorMonitor, // Pass these in to detect OTel double-wrapping if we're enabling spans diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts index 7a242cf3299b..a4575fa3f435 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -1,6 +1,7 @@ +/* eslint-disable max-lines */ import { errorMonitor } from 'node:events'; import type { IncomingHttpHeaders } from 'node:http'; -import { context, SpanKind, trace } from '@opentelemetry/api'; +import { context } from '@opentelemetry/api'; import type { RPCMetadata } from '@opentelemetry/core'; import { RPCType, setRPCMetadata } from '@opentelemetry/core'; import { @@ -33,6 +34,10 @@ import { SPAN_STATUS_ERROR, stripUrlQueryAndFragment, isTracingSuppressed, + bindScopeToEmitter, + startInactiveSpan, + withActiveSpan, + SPAN_KIND, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import type { NodeClient } from '../../sdk/client'; @@ -141,7 +146,6 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions const host = headers.host as string | undefined; const hostname = host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') || 'localhost'; - const tracer = client.tracer; const scheme = fullUrl.startsWith('https') ? 'https' : 'http'; const method = normalizedRequest.method || request.method?.toUpperCase() || 'GET'; @@ -149,8 +153,9 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions const bestEffortTransactionName = `${method} ${httpTargetWithoutQueryFragment}`; // We use the plain tracer.startSpan here so we can pass the span kind - const span = tracer.startSpan(bestEffortTransactionName, { - kind: SpanKind.SERVER, + const span = startInactiveSpan({ + name: bestEffortTransactionName, + kind: SPAN_KIND.SERVER, attributes: { // Sentry specific attributes [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', @@ -180,42 +185,45 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions const rpcMetadata: RPCMetadata = { type: RPCType.HTTP, span }; - return context.with(setRPCMetadata(trace.setSpan(context.active(), span), rpcMetadata), () => { - context.bind(context.active(), request); - context.bind(context.active(), response); - - // Ensure we only end the span once - // E.g. error can be emitted before close is emitted - let isEnded = false; - function endSpan(status: SpanStatus): void { - if (isEnded) { - return; + return withActiveSpan(span, () => { + // TODO(v11): Get rid of RPC metadata here + return context.with(setRPCMetadata(context.active(), rpcMetadata), () => { + bindScopeToEmitter(request); + bindScopeToEmitter(response); + + // Ensure we only end the span once + // E.g. error can be emitted before close is emitted + let isEnded = false; + function endSpan(status: SpanStatus): void { + if (isEnded) { + return; + } + + isEnded = true; + + const newAttributes = getIncomingRequestAttributesOnResponse(request, response, rpcMetadata); + span.setAttributes(newAttributes); + span.setStatus(status); + span.end(); + + // Update the transaction name if the route has changed + const route = newAttributes['http.route']; + if (route) { + getIsolationScope().setTransactionName(`${request.method?.toUpperCase() || 'GET'} ${route}`); + } } - isEnded = true; - - const newAttributes = getIncomingRequestAttributesOnResponse(request, response, rpcMetadata); - span.setAttributes(newAttributes); - span.setStatus(status); - span.end(); - - // Update the transaction name if the route has changed - const route = newAttributes['http.route']; - if (route) { - getIsolationScope().setTransactionName(`${request.method?.toUpperCase() || 'GET'} ${route}`); - } - } + response.on('close', () => { + endSpan(getSpanStatusFromHttpCode(response.statusCode)); + }); + response.on(errorMonitor, () => { + const httpStatus = getSpanStatusFromHttpCode(response.statusCode); + // Ensure we def. have an error status here + endSpan(httpStatus.code === SPAN_STATUS_ERROR ? httpStatus : { code: SPAN_STATUS_ERROR }); + }); - response.on('close', () => { - endSpan(getSpanStatusFromHttpCode(response.statusCode)); + return next(); }); - response.on(errorMonitor, () => { - const httpStatus = getSpanStatusFromHttpCode(response.statusCode); - // Ensure we def. have an error status here - endSpan(httpStatus.code === SPAN_STATUS_ERROR ? httpStatus : { code: SPAN_STATUS_ERROR }); - }); - - return next(); }); }; From 2e9ba2e2f139258f8b0d896b228f3d7627b8159f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 15 Jul 2026 14:16:31 +0200 Subject: [PATCH 2/2] ref --- .../http/httpServerSpansIntegration.ts | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts index a4575fa3f435..33e02c6cc9e9 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -5,12 +5,24 @@ import { context } from '@opentelemetry/api'; import type { RPCMetadata } from '@opentelemetry/core'; import { RPCType, setRPCMetadata } from '@opentelemetry/core'; import { + HTTP_CLIENT_IP, + HTTP_FLAVOR, + HTTP_HOST, + HTTP_METHOD, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, + HTTP_SCHEME, HTTP_STATUS_CODE, + HTTP_TARGET, + HTTP_URL, + HTTP_USER_AGENT, NET_HOST_IP, + NET_HOST_NAME, NET_HOST_PORT, NET_PEER_IP, + NET_PEER_PORT, + NET_TRANSPORT, + SENTRY_HTTP_PREFETCH, } from '@sentry/conventions/attributes'; import type { Event, @@ -152,7 +164,6 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions const httpTargetWithoutQueryFragment = urlObj ? urlObj.pathname : stripUrlQueryAndFragment(fullUrl); const bestEffortTransactionName = `${method} ${httpTargetWithoutQueryFragment}`; - // We use the plain tracer.startSpan here so we can pass the span kind const span = startInactiveSpan({ name: bestEffortTransactionName, kind: SPAN_KIND.SERVER, @@ -160,18 +171,20 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions // Sentry specific attributes [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', - 'sentry.http.prefetch': isKnownPrefetchRequest(request) || undefined, + [SENTRY_HTTP_PREFETCH]: isKnownPrefetchRequest(request) || undefined, // Old Semantic Conventions attributes - added for compatibility with what `@opentelemetry/instrumentation-http` output before - 'http.url': fullUrl, - 'http.method': normalizedRequest.method, - 'http.target': urlObj ? `${urlObj.pathname}${urlObj.search}` : httpTargetWithoutQueryFragment, - 'http.host': host, - 'net.host.name': hostname, - 'http.client_ip': typeof ips === 'string' ? ips.split(',')[0] : undefined, - 'http.user_agent': userAgent, - 'http.scheme': scheme, - 'http.flavor': httpVersion, - 'net.transport': httpVersion?.toUpperCase() === 'QUIC' ? 'ip_udp' : 'ip_tcp', + /* eslint-disable typescript/no-deprecated */ + [HTTP_URL]: fullUrl, + [HTTP_METHOD]: normalizedRequest.method, + [HTTP_TARGET]: urlObj ? `${urlObj.pathname}${urlObj.search}` : httpTargetWithoutQueryFragment, + [HTTP_HOST]: host, + [NET_HOST_NAME]: hostname, + [HTTP_CLIENT_IP]: typeof ips === 'string' ? ips.split(',')[0] : undefined, + [HTTP_USER_AGENT]: userAgent, + [HTTP_SCHEME]: scheme, + [HTTP_FLAVOR]: httpVersion, + [NET_TRANSPORT]: httpVersion?.toUpperCase() === 'QUIC' ? 'ip_udp' : 'ip_tcp', + /* eslint-enable typescript/no-deprecated */ ...getRequestContentLengthAttribute(request), ...httpHeadersToSpanAttributes(normalizedRequest.headers || {}, client.getDataCollectionOptions()), }, @@ -406,7 +419,8 @@ function getIncomingRequestAttributesOnResponse( newAttributes[NET_HOST_PORT] = localPort; // eslint-disable-next-line typescript/no-deprecated newAttributes[NET_PEER_IP] = remoteAddress; - newAttributes['net.peer.port'] = remotePort; + // oxlint-disable-next-line typescript/no-deprecated + newAttributes[NET_PEER_PORT] = remotePort; } // eslint-disable-next-line typescript/no-deprecated newAttributes[HTTP_STATUS_CODE] = statusCode;