From d93b991d83973cb9920cafae26d09a57e7048e7e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 8 Jul 2026 14:55:10 -0400 Subject: [PATCH 1/2] feat(server-utils): Add requiresParentSpan option to bindTracingChannelToSpan Hoists the repeated "skip when there's no active span" guard out of the individual `getSpan` callbacks into a `requiresParentSpan` option on the binding helper. Migrates mysql2, ioredis, postgres, and postgres-js onto it. --- .../integrations/tracing-channel/ioredis.ts | 30 +++---- .../tracing-channel/postgres-js.ts | 11 +-- .../integrations/tracing-channel/postgres.ts | 12 +-- .../src/mysql2/mysql2-dc-subscriber.ts | 85 +++++++++---------- packages/server-utils/src/tracing-channel.ts | 21 ++++- 5 files changed, 81 insertions(+), 78 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/ioredis.ts b/packages/server-utils/src/integrations/tracing-channel/ioredis.ts index 48b0dca08df8..052d15c56a66 100644 --- a/packages/server-utils/src/integrations/tracing-channel/ioredis.ts +++ b/packages/server-utils/src/integrations/tracing-channel/ioredis.ts @@ -8,7 +8,6 @@ import type { IntegrationFn, Span } from '@sentry/core'; import { debug, defineIntegration, - getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, waitForTracingChannelBinding, @@ -96,10 +95,6 @@ const _ioredisChannelIntegration = ((options: IORedisChannelIntegrationOptions = bindTracingChannelToSpan( commandChannel, data => { - // ioredis' `requireParentSpan` default: only create a span under an active span. - if (!getActiveSpan()) { - return undefined; - } const command = data.arguments?.[0] as RedisCommand | undefined; if (!command || typeof command !== 'object') { return undefined; @@ -113,6 +108,8 @@ const _ioredisChannelIntegration = ((options: IORedisChannelIntegrationOptions = }); }, { + // ioredis' `requireParentSpan` default: only create a span under an active span. + requiresParentSpan: true, beforeSpanEnd(span, data) { if ('error' in data || !responseHook) { return; @@ -125,17 +122,18 @@ const _ioredisChannelIntegration = ((options: IORedisChannelIntegrationOptions = }, ); - bindTracingChannelToSpan(connectChannel, data => { - if (!getActiveSpan()) { - return undefined; - } - const { host, port } = getConnectionOptions(data.self); - return startInactiveSpan({ - name: 'connect', - op: 'db', - attributes: { ...connectionAttributes(host, port), [DB_STATEMENT]: 'connect' }, - }); - }); + bindTracingChannelToSpan( + connectChannel, + data => { + const { host, port } = getConnectionOptions(data.self); + return startInactiveSpan({ + name: 'connect', + op: 'db', + attributes: { ...connectionAttributes(host, port), [DB_STATEMENT]: 'connect' }, + }); + }, + { requiresParentSpan: true }, + ); }); }, }; diff --git a/packages/server-utils/src/integrations/tracing-channel/postgres-js.ts b/packages/server-utils/src/integrations/tracing-channel/postgres-js.ts index 0370dba92454..0a2440937643 100644 --- a/packages/server-utils/src/integrations/tracing-channel/postgres-js.ts +++ b/packages/server-utils/src/integrations/tracing-channel/postgres-js.ts @@ -9,7 +9,6 @@ import { _INTERNAL_setPostgresOperationName, debug, defineIntegration, - getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_KIND, SPAN_STATUS_ERROR, @@ -257,15 +256,12 @@ const _postgresJsChannelIntegration = ((options: PostgresJsChannelIntegrationOpt return undefined; } - // Opt out of: re-entrant `handle()` calls (then/catch/finally re-invoke - // it, guarded by `executed`), queries already wrapped by the portable - // `instrumentPostgresJsSql`, and (by default) queries with no parent span. + // Opt out of re-entrant `handle()` calls (then/catch/finally re-invoke it, guarded by + // `executed`) and queries already wrapped by the portable `instrumentPostgresJsSql`. The + // parent-span requirement is applied via `requiresParentSpan` below. if (query.executed === true || (query as Record)[QUERY_FROM_INSTRUMENTED_SQL]) { return undefined; } - if (requireParentSpan !== false && !getActiveSpan()) { - return undefined; - } const fullQuery = _INTERNAL_reconstructPostgresQuery(query.strings); const sanitizedSqlQuery = _INTERNAL_sanitizeSqlQuery(fullQuery); @@ -306,6 +302,7 @@ const _postgresJsChannelIntegration = ((options: PostgresJsChannelIntegrationOpt return span; }, { + requiresParentSpan: requireParentSpan !== false, deferSpanEnd({ data }) { // `handle` is async: its promise settles on dispatch (asyncEnd), long // before the query does. The resolve/reject wrappers own the ending. diff --git a/packages/server-utils/src/integrations/tracing-channel/postgres.ts b/packages/server-utils/src/integrations/tracing-channel/postgres.ts index c9834bcea34e..2c5bb7cf4a6e 100644 --- a/packages/server-utils/src/integrations/tracing-channel/postgres.ts +++ b/packages/server-utils/src/integrations/tracing-channel/postgres.ts @@ -4,7 +4,6 @@ import { bindScopeToEmitter, debug, defineIntegration, - getActiveSpan, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_KIND, @@ -121,12 +120,6 @@ function subscribeQueryLikeChannel( bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(channelName), data => { - // Only instrument when there's an active span; returning `undefined` opts this call out entirely, - // leaving the active context untouched (e.g. connects issued during app startup). - if (!getActiveSpan()) { - return undefined; - } - // Capture the caller's scope while still synchronously inside the call, for the streamed path: // pg dispatches a `Submittable` emitter's events outside the original async scope, so `deferSpanEnd` // replays this scope onto that emitter. @@ -143,6 +136,9 @@ function subscribeQueryLikeChannel( // `query` can return a streamable `Submittable`, so only it defers. deferStreamedResult ? { + // Only instrument under an active span, leaving the context untouched otherwise + // (e.g. connects issued during app startup). + requiresParentSpan: true, // Streamable `Submittable` (e.g. `client.query(new Query())`) // returns an emitter that orchestrion stores on `ctx.result` while // firing no async events; the query isn't done until the emitter @@ -169,7 +165,7 @@ function subscribeQueryLikeChannel( return true; }, } - : undefined, + : { requiresParentSpan: true }, ); } diff --git a/packages/server-utils/src/mysql2/mysql2-dc-subscriber.ts b/packages/server-utils/src/mysql2/mysql2-dc-subscriber.ts index bfe952ec6da5..bf3c1fb462a3 100644 --- a/packages/server-utils/src/mysql2/mysql2-dc-subscriber.ts +++ b/packages/server-utils/src/mysql2/mysql2-dc-subscriber.ts @@ -9,7 +9,6 @@ import { } from '@sentry/conventions/attributes'; import { _INTERNAL_sanitizeSqlQuery, - getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, @@ -93,51 +92,49 @@ export function subscribeMysql2DiagnosticChannels(tracingChannel: MySQL2TracingC } function setupQueryChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string): void { - bindTracingChannelToSpan(tracingChannel(channelName), data => { - // Only instrument when there's an active span - if (!getActiveSpan()) { - return undefined; - } + bindTracingChannelToSpan( + tracingChannel(channelName), + data => { + // mysql2 does not sanitize its channel payload, so the statement may carry + // raw user values (on the `query` channel they are inlined). Strip every + // literal before it leaves the process; `values` is never attached. + const queryText = data.query ? _INTERNAL_sanitizeSqlQuery(data.query) : undefined; + const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase(); - // mysql2 does not sanitize its channel payload, so the statement may carry - // raw user values (on the `query` channel they are inlined). Strip every - // literal before it leaves the process; `values` is never attached. - const queryText = data.query ? _INTERNAL_sanitizeSqlQuery(data.query) : undefined; - const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase(); - - return startInactiveSpan({ - name: queryText || 'mysql2.query', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', - [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, - [DB_QUERY_TEXT]: queryText, - [DB_OPERATION_NAME]: operation, - [DB_NAMESPACE]: data.database || undefined, - [SERVER_ADDRESS]: data.serverAddress, - [SERVER_PORT]: data.serverPort, - }, - }); - }); + return startInactiveSpan({ + name: queryText || 'mysql2.query', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', + [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, + [DB_QUERY_TEXT]: queryText, + [DB_OPERATION_NAME]: operation, + [DB_NAMESPACE]: data.database || undefined, + [SERVER_ADDRESS]: data.serverAddress, + [SERVER_PORT]: data.serverPort, + }, + }); + }, + { requiresParentSpan: true }, + ); } function setupConnectChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string, spanName: string): void { - bindTracingChannelToSpan(tracingChannel(channelName), data => { - // Only instrument when there's an active span. - if (!getActiveSpan()) { - return undefined; - } - - return startInactiveSpan({ - name: spanName, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', - [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, - [DB_NAMESPACE]: data.database || undefined, - [SERVER_ADDRESS]: data.serverAddress, - [SERVER_PORT]: data.serverPort, - }, - }); - }); + bindTracingChannelToSpan( + tracingChannel(channelName), + data => { + return startInactiveSpan({ + name: spanName, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', + [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, + [DB_NAMESPACE]: data.database || undefined, + [SERVER_ADDRESS]: data.serverAddress, + [SERVER_PORT]: data.serverPort, + }, + }); + }, + { requiresParentSpan: true }, + ); } diff --git a/packages/server-utils/src/tracing-channel.ts b/packages/server-utils/src/tracing-channel.ts index fc3b5cfc3f06..09287930d4f9 100644 --- a/packages/server-utils/src/tracing-channel.ts +++ b/packages/server-utils/src/tracing-channel.ts @@ -1,7 +1,14 @@ import type { TracingChannel, TracingChannelSubscribers } from 'node:diagnostics_channel'; import type { AsyncLocalStorage } from 'node:async_hooks'; import type { ExclusiveEventHintOrCaptureContext, Span } from '@sentry/core'; -import { debug, captureException, SPAN_STATUS_ERROR, getAsyncContextStrategy, getMainCarrier } from '@sentry/core'; +import { + debug, + captureException, + SPAN_STATUS_ERROR, + getAsyncContextStrategy, + getMainCarrier, + getActiveSpan, +} from '@sentry/core'; import { DEBUG_BUILD } from './debug-build'; import { ERROR_TYPE } from '@sentry/conventions/attributes'; @@ -58,6 +65,12 @@ export interface TracingChannelLifeCycleOptions { /** Ends the span: `end()` on success, `end(error)` on failure. Idempotent. */ end: (error?: unknown) => void; }) => boolean; + + /** + * Apply span/scope binding only if there is a parent span, similar to returning `undefined` in the `getSpan` callback. + * If you need to perform some conditional checking on the parent span before deciding, do it in the `getSpan` callback. + */ + requiresParentSpan?: boolean; } /** Returned by {@link bindTracingChannelToSpan}: the bound channel plus a teardown handle. */ @@ -90,7 +103,7 @@ export function bindTracingChannelToSpan( getSpan: (data: TracingChannelPayloadWithSpan) => Span | undefined, opts?: TracingChannelLifeCycleOptions, ): TracingChannelBindingHandle { - const handle = bindSpanToChannelStore(channel, getSpan); + const handle = bindSpanToChannelStore(channel, getSpan, opts); const beforeSpanEnd = opts?.beforeSpanEnd; const deferSpanEnd = opts?.deferSpanEnd; @@ -191,6 +204,7 @@ export function bindTracingChannelToSpan( function bindSpanToChannelStore( channel: TracingChannel, getSpan: (data: TracingChannelPayloadWithSpan) => Span | undefined, + opts?: Pick, ): TracingChannelBindingHandle { // Grabs the tracing channel binding defined by the AsyncContext strategy implementation const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.(); @@ -219,7 +233,8 @@ function bindSpanToChannelStore( // callback-style channels (see `_sentryCallerStore`). data._sentryCallerStore = asyncLocalStorage.getStore(); - const span = getSpan(data); + const shouldGetSpan = !opts?.requiresParentSpan || getActiveSpan(); + const span = shouldGetSpan ? getSpan(data) : undefined; if (!span) { // Leave the active context untouched so nested operations keep parenting to the enclosing span. return data._sentryCallerStore as TData; From 17b4fd9b1b7d5e25adaa5fad21eeee7795540bda Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 8 Jul 2026 16:55:09 -0400 Subject: [PATCH 2/2] chore: Bump @sentry/node diagnostics channel injection size limit to 136 KB --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index c0c4a2c25a32..41a2047a7b8c 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -400,7 +400,7 @@ module.exports = [ import: createImport('init', 'experimentalUseDiagnosticsChannelInjection'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '135 KB', + limit: '136 KB', disablePlugins: ['@size-limit/esbuild'], }, {