From 368dc830bcf9c2debcea03b1e0ffe439b23d3f3e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 14:13:27 +0100 Subject: [PATCH 1/7] fix: Use onlyIfParent for remaining database instrumentations Co-Authored-By: GPT-6 --- .../instrumentDurableObjectStorage.ts | 1 + .../instrumentDurableObjectSyncKvStorage.ts | 1 + .../instrumentations/instrumentSqlStorage.ts | 9 +- .../instrumentations/worker/instrumentD1.ts | 2 + .../instrumentations/worker/instrumentKV.ts | 1 + .../src/runtime/utils/instrumentDatabase.ts | 1 + .../src/runtime/utils/instrumentStorage.ts | 1 + .../src/integrations/aws-sdk/index.ts | 3 +- .../src/integrations/dataloader.ts | 10 +-- .../src/integrations/generic-pool.ts | 1 + .../server-utils/src/integrations/knex.ts | 5 +- .../src/integrations/mongodb/index.ts | 54 +++++------- .../src/integrations/mongodb/mongodb-span.ts | 1 + .../mongoose/mongoose-dc-subscriber.ts | 1 + .../mongoose/mongoose-legacy-span.ts | 1 + .../server-utils/src/integrations/mysql.ts | 1 + .../src/integrations/mysql2/index.ts | 49 +++++------ .../mysql2/mysql2-dc-subscriber.ts | 88 +++++++++---------- .../server-utils/src/integrations/neon.ts | 14 ++- .../nitro/captureStorageEvents.ts | 1 + .../src/integrations/postgres-js.ts | 5 +- .../server-utils/src/integrations/postgres.ts | 6 +- .../src/integrations/postgresjs.ts | 14 +-- .../src/integrations/prisma/orchestrion.ts | 7 +- .../src/integrations/prisma/tracing-helper.ts | 3 + .../src/integrations/redis/index.ts | 3 + .../redis/ioredis-channel-subscriber.ts | 30 +++---- .../integrations/redis/redis-dc-subscriber.ts | 3 + 28 files changed, 145 insertions(+), 171 deletions(-) diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts index e1d1a3fc1025..d67d189e9cf0 100644 --- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts @@ -73,6 +73,7 @@ export function instrumentDurableObjectStorage( { // Use underscore naming to match Cloudflare's native instrumentation (e.g., "durable_object_storage_get") name: `durable_object_storage_${methodName}`, + onlyIfParent: true, attributes: { [SENTRY_OP]: DB, [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts index eb21b57a2fbf..ff26fbf431a6 100644 --- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts @@ -26,6 +26,7 @@ export function instrumentDurableObjectSyncKvStorage(syncKv: SyncKvStorage): Syn return startSpan( { name: `durable_object_storage_kv_${methodName}`, + onlyIfParent: true, attributes: { [SENTRY_OP]: DB, [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', diff --git a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts index 8147a2a2bdbb..38d8e477e7be 100644 --- a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts @@ -39,7 +39,7 @@ export function instrumentSqlStorage(sql: SqlStorage): SqlStorage { // steps. We still start the span so it records its dropped span // outcome. A query that may target a `cf_` or `pi_` table takes the full path, // because an internal query must start no span. - return startSpan({ name: 'exec', attributes: SPAN_ATTRIBUTES }, callOriginal); + return startSpan({ name: 'exec', onlyIfParent: true, attributes: SPAN_ATTRIBUTES }, callOriginal); } const sanitizedQuery = sanitizeSqlQuery(query); @@ -56,6 +56,7 @@ export function instrumentSqlStorage(sql: SqlStorage): SqlStorage { return startSpan( { name: querySummary || sanitizedQuery, + onlyIfParent: true, attributes: { ...SPAN_ATTRIBUTES, 'db.query.text': sanitizedQuery, @@ -71,8 +72,8 @@ export function instrumentSqlStorage(sql: SqlStorage): SqlStorage { } /** - * Returns `true` when `startSpan` creates a non-recording span: spans are disabled, or the active span - * is not sampled. Must agree with `createChildOrRootSpan` and `_startChildSpan` in `@sentry/core`. + * Returns `true` when `startSpan` creates a non-recording span: spans are disabled, no parent exists, + * or the active span is not sampled. Must agree with `createChildOrRootSpan` and `_startChildSpan` in `@sentry/core`. */ function childSpanWillNotBeRecorded(): boolean { if (!hasSpansEnabled()) { @@ -80,5 +81,5 @@ function childSpanWillNotBeRecorded(): boolean { } const activeSpan = getActiveSpan(); - return !!activeSpan && !spanIsSampled(activeSpan); + return !activeSpan || !spanIsSampled(activeSpan); } diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts index 42b01428816c..5ba05274abff 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts @@ -135,6 +135,7 @@ function createStartSpanOptions(queryText: string | undefined, type: D1QueryType return { name, + onlyIfParent: true, attributes: { [SENTRY_OP]: DB_QUERY, 'db.system.name': 'cloudflare-d1', @@ -174,6 +175,7 @@ function instrumentBatch( return startSpan( { name: 'D1 batch', + onlyIfParent: true, attributes: { [SENTRY_OP]: DB_QUERY, 'db.system.name': 'cloudflare-d1', diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentKV.ts b/packages/cloudflare/src/instrumentations/worker/instrumentKV.ts index 685b9fc46726..5a19cedb84ff 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentKV.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentKV.ts @@ -44,6 +44,7 @@ function createSpanOptions(bindingName: string, operation: KVOperation, args: un const keys = getCacheKeys(operation, args); return { name: cacheOperation, + onlyIfParent: true, attributes: { [SENTRY_OP]: cacheOperation, [CACHE_OPERATION]: cacheOperationName, diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index 71149f91ff19..83103f046d13 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -271,6 +271,7 @@ function createStartSpanOptions( return { name, + onlyIfParent: true, attributes: { [DB_QUERY_TEXT]: queryText, [DB_QUERY_SUMMARY]: querySummary, diff --git a/packages/nuxt/src/runtime/utils/instrumentStorage.ts b/packages/nuxt/src/runtime/utils/instrumentStorage.ts index 1480c1b7b32d..0f8456e04347 100644 --- a/packages/nuxt/src/runtime/utils/instrumentStorage.ts +++ b/packages/nuxt/src/runtime/utils/instrumentStorage.ts @@ -249,6 +249,7 @@ function createSpanStartOptions( return { // With span streaming, span names have to be low cardinality, so we can't fall back to the cache keys. name: client && hasSpanStreamingEnabled(client) ? cacheOperation : keys.join(', '), + onlyIfParent: true, attributes, }; } diff --git a/packages/server-utils/src/integrations/aws-sdk/index.ts b/packages/server-utils/src/integrations/aws-sdk/index.ts index 10951179dd04..c6b3831e9a5c 100644 --- a/packages/server-utils/src/integrations/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/aws-sdk/index.ts @@ -9,7 +9,7 @@ import { HTTP_RESPONSE_STATUS_CODE, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { RPC } from '@sentry/conventions/op'; +import { DB, RPC } from '@sentry/conventions/op'; import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { awsSdkModuleNames } from '../../orchestrion/config/aws-sdk'; @@ -116,6 +116,7 @@ function instrumentAwsSdk(servicesExtensions: ServicesExtensions): void { const span = startInactiveSpan({ name: requestMetadata.spanName ?? `${normalizedRequest.serviceName}.${normalizedRequest.commandName}`, op, + onlyIfParent: op === DB, attributes: { [SENTRY_KIND]: 'client', [SENTRY_ORIGIN]: getSpanOrigin(op), diff --git a/packages/server-utils/src/integrations/dataloader.ts b/packages/server-utils/src/integrations/dataloader.ts index 5fb50a577325..63983e9b32b3 100644 --- a/packages/server-utils/src/integrations/dataloader.ts +++ b/packages/server-utils/src/integrations/dataloader.ts @@ -179,9 +179,7 @@ function subscribeConstruct(): void { function subscribeLoad(): void { const channel = diagnosticsChannel.tracingChannel(CHANNELS.DATALOADER_LOAD); - bindTracingChannelToSpan(channel, data => startInactiveSpanFor(data.self, 'load', data.arguments[0]), { - requiresParentSpan: true, - }); + bindTracingChannelToSpan(channel, data => startInactiveSpanFor(data.self, 'load', data.arguments[0])); channel.end.subscribe(message => { const data = message as TracingChannelPayloadWithSpan; @@ -194,10 +192,8 @@ function subscribeLoad(): void { } function subscribeSimpleOperation(channelName: ChannelName, operation: Operation): void { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => startInactiveSpanFor(data.self, operation, data.arguments[0]), - { requiresParentSpan: true }, + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => + startInactiveSpanFor(data.self, operation, data.arguments[0]), ); } diff --git a/packages/server-utils/src/integrations/generic-pool.ts b/packages/server-utils/src/integrations/generic-pool.ts index ca27d9bd29ff..da8a853d2c51 100644 --- a/packages/server-utils/src/integrations/generic-pool.ts +++ b/packages/server-utils/src/integrations/generic-pool.ts @@ -35,6 +35,7 @@ function instrumentGenericPool(): void { diagnosticsChannel.tracingChannel(CHANNELS.GENERIC_POOL_ACQUIRE), () => startInactiveSpan({ + onlyIfParent: true, name: 'generic-pool.acquire', attributes: { [SENTRY_OP]: DB, diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index bdc3d4dac16f..5aa05530100a 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -150,10 +150,6 @@ function subscribeQuery(): void { // The builder captures the span active when it was created (see `subscribeBuilder`). // `onlyIfParent`: only instrument queries that run as part of an existing trace, matching OTel. const parentSpan = builder?.[parentSpanSymbol] ?? getActiveSpan(); - if (!parentSpan) { - return undefined; - } - const query = data.arguments[0]; const client = runner?.client; const connection = client?.config?.connection; @@ -193,6 +189,7 @@ function subscribeQuery(): void { : (dbStatement ?? getName(dbNameSpace, operation, table) ?? 'knex.query'); return startInactiveSpan({ + onlyIfParent: true, name: spanName, parentSpan, attributes, diff --git a/packages/server-utils/src/integrations/mongodb/index.ts b/packages/server-utils/src/integrations/mongodb/index.ts index 593638025c6f..b27a3c46c933 100644 --- a/packages/server-utils/src/integrations/mongodb/index.ts +++ b/packages/server-utils/src/integrations/mongodb/index.ts @@ -64,23 +64,17 @@ function instrumentMongodb(): void { * (both publish to this channel). */ function subscribeV4Command(): void { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.MONGODB_COMMAND), - data => { - const args = data.arguments ?? []; - const ns = args[0] as MongodbNamespace | undefined; - const cmd = args[1] as Record | undefined; - // Skip handshake/heartbeat commands (matches otel). - if (!ns || !cmd || typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { - return undefined; - } - const operation = Object.keys(cmd)[0]; - return startMongoSpan(getV4SpanAttributes(data.self, ns, cmd, operation, ORIGIN)); - }, - // Matches otel's `shouldSkipInstrumentation`: only trace when there is - // an active parent span, to avoid emitting orphaned mongodb spans. - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.MONGODB_COMMAND), data => { + const args = data.arguments ?? []; + const ns = args[0] as MongodbNamespace | undefined; + const cmd = args[1] as Record | undefined; + // Skip handshake/heartbeat commands (matches otel). + if (!ns || !cmd || typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { + return undefined; + } + const operation = Object.keys(cmd)[0]; + return startMongoSpan(getV4SpanAttributes(data.self, ns, cmd, operation, ORIGIN)); + }); } /** @@ -150,21 +144,17 @@ function subscribeV3Wireprotocol(): void { } function bindV3(channelName: string, extract: (args: unknown[]) => V3CallInfo | undefined): void { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => { - const args = data.arguments; - if (!args) { - return undefined; - } - const info = extract(args); - if (!info || typeof info.ns !== 'string') { - return undefined; - } - return startMongoSpan(getV3SpanAttributes(info.ns, info.topology, info.command, info.operation, ORIGIN)); - }, - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => { + const args = data.arguments; + if (!args) { + return undefined; + } + const info = extract(args); + if (!info || typeof info.ns !== 'string') { + return undefined; + } + return startMongoSpan(getV3SpanAttributes(info.ns, info.topology, info.command, info.operation, ORIGIN)); + }); } /** diff --git a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts index 2f0a86721314..240c0ca4a1b2 100644 --- a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts +++ b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts @@ -235,6 +235,7 @@ export function startMongoSpan(attributes: SpanAttributes): Span { : (attributes[DB_QUERY_TEXT] as string) || `mongodb.${attributes[DB_OPERATION_NAME] || 'command'}`; return startInactiveSpan({ + onlyIfParent: true, name, attributes: { [SENTRY_OP]: DB, diff --git a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts index e7e9e3c44c71..cc2d800bce51 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts @@ -130,6 +130,7 @@ function setupChannel(tracingChannel: MongooseTracingChannelFactory, channelName : `mongoose.${data.operation}`; return startInactiveSpan({ + onlyIfParent: true, name, attributes: { [SENTRY_ORIGIN]: ORIGIN, diff --git a/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts b/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts index d22e7c8a1063..d77a8935ff36 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts @@ -73,6 +73,7 @@ export function startMongooseLegacySpan({ : `mongoose.${modelName}.${operation}`; return startInactiveSpan({ + onlyIfParent: true, name, // Set this explicitly, for platforms lacking `inferDbSpanData` attributes, diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index d3e8010cb1ba..2c3a3ca1d110 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -93,6 +93,7 @@ function instrumentMysql(): void { : (queryText ?? 'mysql.query'); return startInactiveSpan({ + onlyIfParent: true, name, attributes: { [SENTRY_OP]: DB, diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index 5f337d21a41d..85169d82ddd1 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -74,34 +74,31 @@ function instrumentMysql2Orchestrion(): void { } function subscribeQueryChannel(channelName: ChannelName): void { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => { - const statement = getQueryText(data.arguments); - const connectionAttributes = getConnectionAttributes(data.self?.config); - const { queryText, querySummary } = sanitizeSqlQueryWithSummary(statement, 'mysql'); + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => { + const statement = getQueryText(data.arguments); + const connectionAttributes = getConnectionAttributes(data.self?.config); + const { queryText, querySummary } = sanitizeSqlQueryWithSummary(statement, 'mysql'); - const client = getClient(); - const name = - client && hasSpanStreamingEnabled(client) - ? querySummary || (connectionAttributes[DB_NAMESPACE] as string | undefined) || DB_SYSTEM_VALUE_MYSQL - : (queryText ?? 'mysql2.query'); + const client = getClient(); + const name = + client && hasSpanStreamingEnabled(client) + ? querySummary || (connectionAttributes[DB_NAMESPACE] as string | undefined) || DB_SYSTEM_VALUE_MYSQL + : (queryText ?? 'mysql2.query'); - return startInactiveSpan({ - name, - attributes: { - [SENTRY_KIND]: 'client', - [SENTRY_ORIGIN]: ORIGIN, - [SENTRY_OP]: DB, - [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL, - [DB_QUERY_TEXT]: queryText || undefined, - [DB_QUERY_SUMMARY]: querySummary, - ...connectionAttributes, - }, - }); - }, - { requiresParentSpan: true }, - ); + return startInactiveSpan({ + onlyIfParent: true, + name, + attributes: { + [SENTRY_KIND]: 'client', + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: DB, + [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL, + [DB_QUERY_TEXT]: queryText || undefined, + [DB_QUERY_SUMMARY]: querySummary, + ...connectionAttributes, + }, + }); + }); } /** diff --git a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts index 72d065956713..19216e3c3115 100644 --- a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts @@ -92,56 +92,50 @@ export function subscribeMysql2DiagnosticChannels(tracingChannel: MySQL2TracingC } function setupQueryChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string): void { - 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, querySummary } = sanitizeSqlQueryWithSummary(data.query, 'mysql'); - const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase(); + 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, querySummary } = sanitizeSqlQueryWithSummary(data.query, 'mysql'); + const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase(); - const client = getClient(); - const name = - client && hasSpanStreamingEnabled(client) - ? querySummary || data.database || DB_SYSTEM_NAME_VALUE_MYSQL - : queryText || 'mysql2.query'; + const client = getClient(); + const name = + client && hasSpanStreamingEnabled(client) + ? querySummary || data.database || DB_SYSTEM_NAME_VALUE_MYSQL + : queryText || 'mysql2.query'; - return startInactiveSpan({ - name, - attributes: { - [SENTRY_ORIGIN]: ORIGIN, - [SENTRY_OP]: DB, - [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, - [DB_QUERY_TEXT]: queryText, - [DB_QUERY_SUMMARY]: querySummary, - [DB_OPERATION_NAME]: operation, - [DB_NAMESPACE]: data.database || undefined, - [SERVER_ADDRESS]: data.serverAddress, - [SERVER_PORT]: data.serverPort, - }, - }); - }, - { requiresParentSpan: true }, - ); + return startInactiveSpan({ + onlyIfParent: true, + name, + attributes: { + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: DB, + [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, + [DB_QUERY_TEXT]: queryText, + [DB_QUERY_SUMMARY]: querySummary, + [DB_OPERATION_NAME]: operation, + [DB_NAMESPACE]: data.database || undefined, + [SERVER_ADDRESS]: data.serverAddress, + [SERVER_PORT]: data.serverPort, + }, + }); + }); } function setupConnectChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string, spanName: string): void { - bindTracingChannelToSpan( - tracingChannel(channelName), - data => { - return startInactiveSpan({ - name: spanName, - attributes: { - [SENTRY_ORIGIN]: ORIGIN, - [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 }, - ); + bindTracingChannelToSpan(tracingChannel(channelName), data => { + return startInactiveSpan({ + onlyIfParent: true, + name: spanName, + attributes: { + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: DB, + [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, + [DB_NAMESPACE]: data.database || undefined, + [SERVER_ADDRESS]: data.serverAddress, + [SERVER_PORT]: data.serverPort, + }, + }); + }); } diff --git a/packages/server-utils/src/integrations/neon.ts b/packages/server-utils/src/integrations/neon.ts index f7e66ff953a2..3caf4a3e19e4 100644 --- a/packages/server-utils/src/integrations/neon.ts +++ b/packages/server-utils/src/integrations/neon.ts @@ -60,15 +60,11 @@ function instrumentNeon(): void { // the pg span shape. bindPgQueryChannel(CHANNELS.NEON_QUERY, { origin: ORIGIN }); - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.NEON_HTTP_QUERY), - data => { - const span = startInactiveSpan(httpQuerySpanOptions(data.arguments)); - httpQuerySpans.add(span); - return span; - }, - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.NEON_HTTP_QUERY), data => { + const span = startInactiveSpan({ ...httpQuerySpanOptions(data.arguments), onlyIfParent: true }); + httpQuerySpans.add(span); + return span; + }); // The connection string is resolved inside the HTTP executor, so the query span is active // when this settles. The executor's own context never sees the string. diff --git a/packages/server-utils/src/integrations/nitro/captureStorageEvents.ts b/packages/server-utils/src/integrations/nitro/captureStorageEvents.ts index d419951912fa..e7e177667767 100644 --- a/packages/server-utils/src/integrations/nitro/captureStorageEvents.ts +++ b/packages/server-utils/src/integrations/nitro/captureStorageEvents.ts @@ -89,6 +89,7 @@ function setupStorageTracingChannel(operation: TracedOperation): void { const client = getClient(); return startInactiveSpan({ + onlyIfParent: true, // With span streaming, span names have to be low cardinality, so we can't fall back to the cache keys. name: client && hasSpanStreamingEnabled(client) ? OPERATION_SPAN_OPS[operation] : cacheKeys.join(', ') || operation, diff --git a/packages/server-utils/src/integrations/postgres-js.ts b/packages/server-utils/src/integrations/postgres-js.ts index 0ac51daa8f54..d86ca544eddf 100644 --- a/packages/server-utils/src/integrations/postgres-js.ts +++ b/packages/server-utils/src/integrations/postgres-js.ts @@ -270,8 +270,7 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { // 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. + // `instrumentPostgresJsSql`. if (query.executed === true || (query as Record)[QUERY_FROM_INSTRUMENTED_SQL]) { return undefined; } @@ -294,6 +293,7 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { // `sentry.kind: client` matches the mysql/pg channel subscribers. const span = startInactiveSpan({ + onlyIfParent: requireParentSpan !== false, name, attributes: { [SENTRY_OP]: DB, @@ -327,7 +327,6 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { 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/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 9dc4e62bf01e..1715ce88ef8b 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -142,6 +142,7 @@ function subscribeQueryLikeChannel( // `sentry.kind` matches across the OTel and diagnostics-channel paths. const spanOptions = getSpanOptions(data); return startInactiveSpan({ + onlyIfParent: true, ...spanOptions, attributes: { ...spanOptions.attributes, [SENTRY_KIND]: 'client' }, }); @@ -153,9 +154,6 @@ 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 @@ -182,7 +180,7 @@ function subscribeQueryLikeChannel( return true; }, } - : { requiresParentSpan: true }, + : undefined, ); } diff --git a/packages/server-utils/src/integrations/postgresjs.ts b/packages/server-utils/src/integrations/postgresjs.ts index 8588ed1a56ea..36b81c64b6ea 100644 --- a/packages/server-utils/src/integrations/postgresjs.ts +++ b/packages/server-utils/src/integrations/postgresjs.ts @@ -5,7 +5,6 @@ import { debug, - getActiveSpan, getClient, hasSpanStreamingEnabled, isObjectLike, @@ -240,7 +239,7 @@ function _wrapSingleQueryHandle( const wrappedHandle = async function (this: { executed?: boolean }, ...args: unknown[]): Promise { // postgres.js calls handle() from then/catch/finally — only the first call executes SQL, // subsequent calls are no-ops (guarded by this.executed). Skip span creation for no-ops. - if (this.executed || !_shouldCreateSpans(options)) { + if (this.executed) { return originalHandle.apply(this, args); } @@ -262,6 +261,7 @@ function _wrapSingleQueryHandle( return startSpanManual( { name, + onlyIfParent: options.requireParentSpan, attributes: { [SENTRY_OP]: DB, [SENTRY_ORIGIN]: 'auto.db.postgresjs', @@ -339,16 +339,6 @@ function _wrapSingleQueryHandle( query.handle = wrappedHandle; } -/** - * Determines whether a span should be created based on the current context. - * If `requireParentSpan` is set to true in the options, a span will - * only be created if there is a parent span available. - */ -function _shouldCreateSpans(options: PostgresJsSqlInstrumentationOptions): boolean { - const hasParentSpan = getActiveSpan() !== undefined; - return hasParentSpan || !options.requireParentSpan; -} - /** * Reconstructs the full SQL query from template strings with PostgreSQL placeholders. * diff --git a/packages/server-utils/src/integrations/prisma/orchestrion.ts b/packages/server-utils/src/integrations/prisma/orchestrion.ts index de209f694d71..3c03327c5f0d 100644 --- a/packages/server-utils/src/integrations/prisma/orchestrion.ts +++ b/packages/server-utils/src/integrations/prisma/orchestrion.ts @@ -52,11 +52,7 @@ export function instrumentPrismaV8(options: PrismaV8InstrumentationOptions): voi bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(channelName), data => startOperationSpan(method, data), - { - // A stray query outside a request must not become a segment of its own. - requiresParentSpan: true, - ...(isLazy ? { deferSpanEnd: deferSpanEndUntilConsumed } : {}), - }, + isLazy ? { deferSpanEnd: deferSpanEndUntilConsumed } : undefined, ); } } @@ -71,6 +67,7 @@ function startOperationSpan(method: string, data: PrismaTerminalChannelContext): const table = stringOrUndefined(data.self?.tableName); const span = startInactiveSpan({ + onlyIfParent: true, name: OPERATION_SPAN_NAME, parentSpan, attributes: { diff --git a/packages/server-utils/src/integrations/prisma/tracing-helper.ts b/packages/server-utils/src/integrations/prisma/tracing-helper.ts index 218e7c788134..140747a4b992 100644 --- a/packages/server-utils/src/integrations/prisma/tracing-helper.ts +++ b/packages/server-utils/src/integrations/prisma/tracing-helper.ts @@ -189,6 +189,7 @@ function createResolvedEngineSpans(): void { const attributes = buildSpanAttributes(engineSpan.name, engineSpan.attributes); const span = startInactiveSpan({ + onlyIfParent: true, name: buildSpanName(engineSpan.name, attributes), attributes: { ...attributes, @@ -295,6 +296,7 @@ export class ActiveTracingHelper implements TracingHelper { const attributes = buildSpanAttributes(name, options.attributes); const spanOptions = { name: buildSpanName(name, attributes), + onlyIfParent: true, attributes, kind: options.kind, links: options.links, @@ -332,6 +334,7 @@ function dispatchEngineSpan( startSpanManual( { name: buildSpanName(engineSpan.name, attributes), + onlyIfParent: true, attributes: { ...attributes, [SENTRY_KIND]: engineSpan.kind === 'client' ? 'client' : undefined, diff --git a/packages/server-utils/src/integrations/redis/index.ts b/packages/server-utils/src/integrations/redis/index.ts index 7176a14b57ec..701a260286b8 100644 --- a/packages/server-utils/src/integrations/redis/index.ts +++ b/packages/server-utils/src/integrations/redis/index.ts @@ -116,6 +116,7 @@ function startCommandSpan( const cacheProperties = getRedisCacheAttributes(commandName, commandArgs, attributes, cacheOptions); return startInactiveSpan({ + onlyIfParent: true, name: cacheProperties?.name ?? streamedName ?? (dbStatement || `redis-${commandName}`), attributes: { [SENTRY_KIND]: 'client', @@ -244,6 +245,7 @@ function bindNodeRedisConnectChannel(): void { bindTracingChannelToSpan(channel, data => { const options = (data.self as NodeRedisClient | undefined)?.options; return startInactiveSpan({ + onlyIfParent: true, name: 'redis-connect', attributes: { [SENTRY_KIND]: 'client', @@ -268,6 +270,7 @@ function bindNodeRedisBatchChannel( const options = (data.self as NodeRedisClient | undefined)?.options; const operation = getOperation(data); return startInactiveSpan({ + onlyIfParent: true, name: operation, attributes: { [SENTRY_KIND]: 'client', diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index d918d080f749..2eb19c7b2820 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -92,6 +92,7 @@ export function startIORedisCommandSpan( const cacheProperties = getRedisCacheAttributes(command.name, command.args ?? [], attributes, cacheOptions); return startInactiveSpan({ + onlyIfParent: true, name: cacheProperties?.name ?? streamedName ?? statement, attributes: { ...attributes, ...cacheProperties?.attributes }, }); @@ -112,8 +113,6 @@ export function instrumentIoredis(options: RedisCacheOptions): void { ); bindTracingChannelToSpan(commandChannel, data => startIORedisCommandSpan(data, options), { - // ioredis' `requireParentSpan` default: only create a span under an active span. - requiresParentSpan: true, beforeSpanEnd(span, data) { if (!('error' in data)) { applyCacheResponseAttributes(span, data.result); @@ -121,19 +120,16 @@ export function instrumentIoredis(options: RedisCacheOptions): void { }, }); - bindTracingChannelToSpan( - connectChannel, - data => { - const { host, port } = getConnectionOptions(data.self); - return startInactiveSpan({ - name: 'connect', - attributes: { - [SENTRY_KIND]: 'client', - ...connectionAttributes(host, port), - [SENTRY_OP]: DB, - }, - }); - }, - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(connectChannel, data => { + const { host, port } = getConnectionOptions(data.self); + return startInactiveSpan({ + onlyIfParent: true, + name: 'connect', + attributes: { + [SENTRY_KIND]: 'client', + ...connectionAttributes(host, port), + [SENTRY_OP]: DB, + }, + }); + }); } diff --git a/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts b/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts index 5db9dc2e5458..88d7ad0846ab 100644 --- a/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts @@ -160,6 +160,7 @@ function setupCommandChannel( }; const cacheProperties = getRedisCacheAttributes(data.command, args, attributes, cacheOptions); return startInactiveSpan({ + onlyIfParent: true, name: cacheProperties?.name ?? streamedName ?? `redis-${data.command}`, attributes: { ...attributes, ...cacheProperties?.attributes }, }); @@ -181,6 +182,7 @@ function setupBatchChannel( bindTracingChannelToSpan(tracingChannel(channelName), data => { const operation = getOperationName(data); return startInactiveSpan({ + onlyIfParent: true, name: operation, attributes: { [SENTRY_ORIGIN]: ORIGIN, @@ -200,6 +202,7 @@ function setupBatchChannel( function setupConnectChannel(tracingChannel: RedisTracingChannelFactory, channelName: string): void { bindTracingChannelToSpan(tracingChannel(channelName), data => { return startInactiveSpan({ + onlyIfParent: true, name: 'redis-connect', attributes: { [SENTRY_ORIGIN]: ORIGIN, From 5099302a495c7b803234ea4a30125192ee0f9792 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 14:33:52 +0100 Subject: [PATCH 2/7] test(node): Connect Redis clients inside parent spans Co-Authored-By: GPT-6 --- .../ioredis-dc/scenario-ioredis-5-11.mjs | 4 +-- .../tracing/ioredis-dc/scenario-ioredis-6.mjs | 4 +-- .../suites/tracing/ioredis-dc/test.ts | 30 ++++++++++++------- .../tracing/redis-cache/scenario-redis-4.mjs | 6 ++-- .../tracing/redis-cache/scenario-redis-5.mjs | 4 ++- .../suites/tracing/redis-cache/test.ts | 28 ++++++++++------- .../redis-dc/scenario-redis-5-tracing.mjs | 4 ++- .../suites/tracing/redis-dc/test.ts | 16 +++++----- 8 files changed, 59 insertions(+), 37 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-5-11.mjs b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-5-11.mjs index 402adb6f364f..8d975717f0c1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-5-11.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-5-11.mjs @@ -4,14 +4,14 @@ async function run() { const { default: Redis } = await import('ioredis-5'); const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true }); - await redisClient.connect(); - await Sentry.startSpan( { name: 'Test Span IORedis DC', op: 'test-span-ioredis-dc', }, async () => { + await redisClient.connect(); + try { await redisClient.set('dc-test-key', 'test-value'); await redisClient.set('dc-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-6.mjs b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-6.mjs index 994c679d9d3a..c281afde049d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-6.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-6.mjs @@ -4,14 +4,14 @@ async function run() { const { default: Redis } = await import('ioredis-6'); const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true }); - await redisClient.connect(); - await Sentry.startSpan( { name: 'Test Span IORedis DC', op: 'test-span-ioredis-dc', }, async () => { + await redisClient.connect(); + try { await redisClient.set('dc-test-key', 'test-value'); await redisClient.set('dc-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts index fe1cc5bb7b09..a35f47f322d4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts @@ -21,7 +21,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; -import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; +import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB, DB_QUERY } from '@sentry/conventions/op'; import { afterAll, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -101,18 +101,26 @@ describeWithDockerCompose( await createTestRunner() .expect({ span: container => { - // The connect span opens its own segment but shares the trace with the test span, - // so both segments arrive in the same container. - expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ - 'redis-connect', - SEGMENT_NAME, - ]); + const segments = container.items.filter(item => item.is_segment); + expect(segments.map(item => item.name)).toEqual([SEGMENT_NAME]); + const parent = segments[0]!; + const connection = container.items.find(item => item.name === 'redis-connect')!; + expect(connection).toEqual({ + ...(streamedSpan('redis-connect', DB, {}) as Record), + parent_span_id: parent.span_id, + trace_id: parent.trace_id, + }); + + const handshakes = container.items.filter(item => item.parent_span_id === connection.span_id); + expect(handshakes.map(item => item.attributes[DB_OPERATION_NAME]?.value)).toEqual( + label === 'v6' ? ['hello', 'client', 'client', 'info'] : ['client', 'client', 'info'], + ); - // ioredis' own handshake commands (`client SETINFO`, `info`) are emitted on the - // channel too, but belong to the connect segment — the test span's children are the - // commands the scenario issues. const spans = container.items.filter( - item => !item.is_segment && item.attributes[SENTRY_SEGMENT_NAME]?.value === SEGMENT_NAME, + item => + !item.is_segment && + item.span_id !== connection.span_id && + item.parent_span_id !== connection.span_id, ); expect(spans).toEqual([ diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.mjs index 592602d7222e..d148751d6773 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.mjs @@ -2,11 +2,11 @@ import * as Sentry from '@sentry/node'; import { createClient } from 'redis-4'; async function run() { - const redisClient = await createClient({ + const redisClient = createClient({ socket: { port: 6383, }, - }).connect(); + }); await Sentry.startSpan( { @@ -14,6 +14,8 @@ async function run() { op: 'test-span-redis-4', }, async () => { + await redisClient.connect(); + try { await redisClient.set('redis-test-key', 'test-value'); await redisClient.set('redis-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-5.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-5.mjs index a32aee1c3af2..8a4b5ec1d9af 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-5.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-5.mjs @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node'; import { createClient } from 'redis-5'; async function run() { - const redisClient = await createClient({ socket: { port: 6383 } }).connect(); + const redisClient = createClient({ socket: { port: 6383 } }); await Sentry.startSpan( { @@ -10,6 +10,8 @@ async function run() { op: 'test-span-redis-5', }, async () => { + await redisClient.connect(); + try { await redisClient.set('redis-5-test-key', 'test-value'); await redisClient.set('redis-5-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index a6f3271d2356..22d3be2ed943 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts @@ -25,7 +25,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; -import { CACHE_GET, CACHE_PUT, CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; +import { CACHE_GET, CACHE_PUT, CACHE_REMOVE, DB, DB_QUERY } from '@sentry/conventions/op'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; @@ -323,14 +323,16 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory await createTestRunner() .expect({ span: container => { - // The connect span opens its own segment, but shares the trace with the test span, - // so both segments arrive in the same container. - expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ - 'redis-connect', - segmentName, - ]); + const segments = container.items.filter(item => item.is_segment); + expect(segments.map(item => item.name)).toEqual([segmentName]); + const parent = segments[0]!; expect(childSpans(container)).toEqual([ + { + ...(span('redis-connect', DB, {}) as Record), + parent_span_id: parent.span_id, + trace_id: parent.trace_id, + }, span('SET localhost:6383', redisSpanOp, { [DB_OPERATION_NAME]: 'SET', [DB_QUERY_TEXT]: 'SET redis-test-key [1 other arguments]', @@ -427,12 +429,16 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory await createTestRunner() .expect({ span: container => { - expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ - 'redis-connect', - segmentName, - ]); + const segments = container.items.filter(item => item.is_segment); + expect(segments.map(item => item.name)).toEqual([segmentName]); + const parent = segments[0]!; expect(childSpans(container)).toEqual([ + { + ...(span('redis-connect', DB, {}) as Record), + parent_span_id: parent.span_id, + trace_id: parent.trace_id, + }, span('SET localhost:6383', redisSpanOp, { [DB_OPERATION_NAME]: 'SET', [DB_QUERY_TEXT]: 'SET redis-5-test-key [1 other arguments]', diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-dc/scenario-redis-5-tracing.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-dc/scenario-redis-5-tracing.mjs index 661b37ef9c6d..612793899ff2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-dc/scenario-redis-5-tracing.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-dc/scenario-redis-5-tracing.mjs @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node'; async function run() { const { createClient } = await import('redis-5-tracing'); - const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect(); + const redisClient = createClient({ socket: { host: '127.0.0.1', port: 6381 } }); await Sentry.startSpan( { @@ -10,6 +10,8 @@ async function run() { op: 'test-span-redis-5-dc', }, async () => { + await redisClient.connect(); + try { await redisClient.set('dc-test-key', 'test-value'); await redisClient.set('dc-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts index a9f10789dc48..d5fef4490755 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts @@ -23,7 +23,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; -import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; +import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB, DB_QUERY } from '@sentry/conventions/op'; import { afterAll, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -126,18 +126,20 @@ describeWithDockerCompose( await createTestRunner() .expect({ span: container => { - // The connect span opens its own segment but shares the trace with the test span, - // so both segments arrive in the same container. - expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ - 'redis-connect', - SEGMENT_NAME, - ]); + const segments = container.items.filter(item => item.is_segment); + expect(segments.map(item => item.name)).toEqual([SEGMENT_NAME]); + const parent = segments[0]!; const spans = container.items.filter( item => !item.is_segment && item.attributes[SENTRY_SEGMENT_NAME]?.value === SEGMENT_NAME, ); expect(spans).toEqual([ + { + ...(streamedSpan('redis-connect', DB, {}) as Record), + parent_span_id: parent.span_id, + trace_id: parent.trace_id, + }, streamedSpan(`SET ${HOST}:${PORT}`, DB_QUERY, { [DB_OPERATION_NAME]: 'SET', [DB_QUERY_TEXT]: 'SET dc-test-key ?', From 7441a6fc2e4bd4a735b412f515baf2f6e02040de Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 15:10:28 +0100 Subject: [PATCH 3/7] test(node): Remove unnecessary ioredis handshake assertions Co-Authored-By: GPT-6 --- .../node-integration-tests/suites/tracing/ioredis-dc/test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts index a35f47f322d4..82d3b29b5023 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts @@ -111,11 +111,6 @@ describeWithDockerCompose( trace_id: parent.trace_id, }); - const handshakes = container.items.filter(item => item.parent_span_id === connection.span_id); - expect(handshakes.map(item => item.attributes[DB_OPERATION_NAME]?.value)).toEqual( - label === 'v6' ? ['hello', 'client', 'client', 'info'] : ['client', 'client', 'info'], - ); - const spans = container.items.filter( item => !item.is_segment && From 8e811acf8e21c6febf05941c619ec54f344b5367 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 15:13:53 +0100 Subject: [PATCH 4/7] ref: Minimize the DB parent alignment diff Co-Authored-By: GPT-6 --- .../suites/tracing/ioredis-dc/test.ts | 16 ++++++---------- .../src/instrumentations/instrumentSqlStorage.ts | 6 +++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts index 82d3b29b5023..7250567151a8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts @@ -104,21 +104,17 @@ describeWithDockerCompose( const segments = container.items.filter(item => item.is_segment); expect(segments.map(item => item.name)).toEqual([SEGMENT_NAME]); const parent = segments[0]!; - const connection = container.items.find(item => item.name === 'redis-connect')!; - expect(connection).toEqual({ - ...(streamedSpan('redis-connect', DB, {}) as Record), - parent_span_id: parent.span_id, - trace_id: parent.trace_id, - }); const spans = container.items.filter( - item => - !item.is_segment && - item.span_id !== connection.span_id && - item.parent_span_id !== connection.span_id, + item => !item.is_segment && item.parent_span_id === parent.span_id, ); expect(spans).toEqual([ + { + ...(streamedSpan('redis-connect', DB, {}) as Record), + parent_span_id: parent.span_id, + trace_id: parent.trace_id, + }, streamedSpan(`set ${HOST}:${PORT}`, DB_QUERY, { [DB_OPERATION_NAME]: 'set', [DB_QUERY_TEXT]: 'set dc-test-key ?', diff --git a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts index 38d8e477e7be..c11653bf970d 100644 --- a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts @@ -72,8 +72,8 @@ export function instrumentSqlStorage(sql: SqlStorage): SqlStorage { } /** - * Returns `true` when `startSpan` creates a non-recording span: spans are disabled, no parent exists, - * or the active span is not sampled. Must agree with `createChildOrRootSpan` and `_startChildSpan` in `@sentry/core`. + * Returns `true` when `startSpan` creates a non-recording span: spans are disabled, or the active span + * is not sampled. Must agree with `createChildOrRootSpan` and `_startChildSpan` in `@sentry/core`. */ function childSpanWillNotBeRecorded(): boolean { if (!hasSpansEnabled()) { @@ -81,5 +81,5 @@ function childSpanWillNotBeRecorded(): boolean { } const activeSpan = getActiveSpan(); - return !activeSpan || !spanIsSampled(activeSpan); + return !!activeSpan && !spanIsSampled(activeSpan); } From c18462ad9533ed39d6b353dc9bfcd3683838e541 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 15:23:09 +0100 Subject: [PATCH 5/7] ref: Exclude DataLoader from DB parent alignment Co-Authored-By: GPT-6 --- packages/server-utils/src/integrations/dataloader.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/server-utils/src/integrations/dataloader.ts b/packages/server-utils/src/integrations/dataloader.ts index 63983e9b32b3..5fb50a577325 100644 --- a/packages/server-utils/src/integrations/dataloader.ts +++ b/packages/server-utils/src/integrations/dataloader.ts @@ -179,7 +179,9 @@ function subscribeConstruct(): void { function subscribeLoad(): void { const channel = diagnosticsChannel.tracingChannel(CHANNELS.DATALOADER_LOAD); - bindTracingChannelToSpan(channel, data => startInactiveSpanFor(data.self, 'load', data.arguments[0])); + bindTracingChannelToSpan(channel, data => startInactiveSpanFor(data.self, 'load', data.arguments[0]), { + requiresParentSpan: true, + }); channel.end.subscribe(message => { const data = message as TracingChannelPayloadWithSpan; @@ -192,8 +194,10 @@ function subscribeLoad(): void { } function subscribeSimpleOperation(channelName: ChannelName, operation: Operation): void { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => - startInactiveSpanFor(data.self, operation, data.arguments[0]), + bindTracingChannelToSpan( + diagnosticsChannel.tracingChannel(channelName), + data => startInactiveSpanFor(data.self, operation, data.arguments[0]), + { requiresParentSpan: true }, ); } From ff76e314abf7d64ecc01a04499c383f0f7d07f92 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 15:43:56 +0100 Subject: [PATCH 6/7] test: Align unit expectations with DB parent requirements Co-Authored-By: GPT-6 --- .../test/instrumentDurableObjectStorage.test.ts | 12 ++++++++++++ .../instrumentDurableObjectSyncKvStorage.test.ts | 4 ++++ .../cloudflare/test/instrumentSqlStorage.test.ts | 4 ++++ .../instrumentations/worker/instrumentD1.test.ts | 9 +++++++++ .../instrumentations/worker/instrumentKV.test.ts | 6 ++++++ .../mysql2/mysql2-dc-subscriber.test.ts | 4 ++-- .../server-utils/test/integrations/neon.test.ts | 4 ++-- .../test/integrations/postgres/postgres.test.ts | 4 ++-- .../test/integrations/prisma-orchestrion.test.ts | 4 ++-- .../test/integrations/prisma.test.ts | 16 +++++++++++++--- 10 files changed, 56 insertions(+), 11 deletions(-) diff --git a/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts b/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts index a2295ceff624..0ca6a6c7dd20 100644 --- a/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts +++ b/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts @@ -29,6 +29,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_get', attributes: { 'sentry.op': 'db', @@ -50,6 +51,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_get', attributes: { 'sentry.op': 'db', @@ -73,6 +75,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_put', attributes: { 'sentry.op': 'db', @@ -94,6 +97,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_put', attributes: { 'sentry.op': 'db', @@ -117,6 +121,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_delete', attributes: { 'sentry.op': 'db', @@ -138,6 +143,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_delete', attributes: { 'sentry.op': 'db', @@ -161,6 +167,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_list', attributes: { 'sentry.op': 'db', @@ -184,6 +191,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_setAlarm', attributes: { 'sentry.op': 'db', @@ -268,6 +276,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_getAlarm', attributes: { 'sentry.op': 'db', @@ -289,6 +298,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_deleteAlarm', attributes: { 'sentry.op': 'db', @@ -312,6 +322,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'SELECT', attributes: { 'sentry.op': 'db.query', @@ -436,6 +447,7 @@ describe('instrumentDurableObjectStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_kv_get', attributes: { 'sentry.op': 'db', diff --git a/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts b/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts index b75cc0e0e245..8c6a5f8b59e1 100644 --- a/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts +++ b/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts @@ -18,6 +18,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_kv_get', attributes: { 'sentry.op': 'db', @@ -61,6 +62,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_kv_put', attributes: { 'sentry.op': 'db', @@ -93,6 +95,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_kv_delete', attributes: { 'sentry.op': 'db', @@ -126,6 +129,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'durable_object_storage_kv_list', attributes: { 'sentry.op': 'db', diff --git a/packages/cloudflare/test/instrumentSqlStorage.test.ts b/packages/cloudflare/test/instrumentSqlStorage.test.ts index af9cff36cfec..77ee3ad1a51a 100644 --- a/packages/cloudflare/test/instrumentSqlStorage.test.ts +++ b/packages/cloudflare/test/instrumentSqlStorage.test.ts @@ -24,6 +24,7 @@ describe('instrumentSqlStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'SELECT users', attributes: { 'sentry.op': 'db.query', @@ -48,6 +49,7 @@ describe('instrumentSqlStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'SELECT users', attributes: { 'sentry.op': 'db.query', @@ -81,6 +83,7 @@ describe('instrumentSqlStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'INSERT users', attributes: { 'sentry.op': 'db.query', @@ -301,6 +304,7 @@ describe('instrumentSqlStorage', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: 'exec', attributes: { 'sentry.op': 'db.query', diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts index 7c6a6478853e..059c20fc6fd0 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts @@ -74,6 +74,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', @@ -176,6 +177,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', @@ -230,6 +232,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', @@ -284,6 +287,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', @@ -335,6 +339,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', @@ -374,6 +379,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', @@ -403,6 +409,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', @@ -441,6 +448,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', @@ -478,6 +486,7 @@ describe('instrumentD1', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenLastCalledWith( { + onlyIfParent: true, attributes: { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentKV.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentKV.test.ts index 50e08dc1b69b..a5203a01e9e9 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentKV.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentKV.test.ts @@ -48,6 +48,7 @@ describe('instrumentKV', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: CACHE_GET, attributes: { [SENTRY_OP]: CACHE_GET, @@ -81,6 +82,7 @@ describe('instrumentKV', () => { expect(get).toHaveBeenCalledWith(keys, 'text'); expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: CACHE_GET, attributes: { [SENTRY_OP]: CACHE_GET, @@ -105,6 +107,7 @@ describe('instrumentKV', () => { expect(kv.getWithMetadata).toHaveBeenCalledWith('test-key', 'json'); expect(startSpanSpy).toHaveBeenCalledWith( { + onlyIfParent: true, name: CACHE_GET, attributes: { [SENTRY_OP]: CACHE_GET, @@ -132,6 +135,7 @@ describe('instrumentKV', () => { expect(startSpanSpy).toHaveBeenNthCalledWith( 1, { + onlyIfParent: true, name: CACHE_PUT, attributes: { [SENTRY_OP]: CACHE_PUT, @@ -148,6 +152,7 @@ describe('instrumentKV', () => { expect(startSpanSpy).toHaveBeenNthCalledWith( 2, { + onlyIfParent: true, name: CACHE_REMOVE, attributes: { [SENTRY_OP]: CACHE_REMOVE, @@ -164,6 +169,7 @@ describe('instrumentKV', () => { expect(startSpanSpy).toHaveBeenNthCalledWith( 3, { + onlyIfParent: true, name: CACHE_GET, attributes: { [SENTRY_OP]: CACHE_GET, diff --git a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts index f414f8ee1bf0..df6f688de355 100644 --- a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts +++ b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts @@ -168,7 +168,7 @@ describe('subscribeMysql2DiagnosticChannels', () => { vi.clearAllMocks(); }); - it('does not create a span when there is no enclosing active span', async () => { + it('activates a non-recording span when there is no enclosing active span', async () => { const channel = tracingChannel(MYSQL2_DC_CHANNEL_QUERY); let span: Span | undefined; @@ -179,7 +179,7 @@ describe('subscribeMysql2DiagnosticChannels', () => { { query: 'SELECT 1' }, ); - expect(span).toBeUndefined(); + expect(span?.isRecording()).toBe(false); }); describe('query channel', () => { diff --git a/packages/server-utils/test/integrations/neon.test.ts b/packages/server-utils/test/integrations/neon.test.ts index 661a65cc7389..ebb71b8af08d 100644 --- a/packages/server-utils/test/integrations/neon.test.ts +++ b/packages/server-utils/test/integrations/neon.test.ts @@ -154,13 +154,13 @@ describe('neonIntegration', () => { expect(span.end).toHaveBeenCalledTimes(1); }); - it('does not start a span without a parent span', async () => { + it('requires a parent when starting a query span', async () => { getActiveSpanSpy.mockReturnValue(undefined); const ctx: ChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.NEON_QUERY).tracePromise(async () => ({ rows: [] }), ctx); - expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + expect(startInactiveSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ onlyIfParent: true })); }); }); diff --git a/packages/server-utils/test/integrations/postgres/postgres.test.ts b/packages/server-utils/test/integrations/postgres/postgres.test.ts index db64ef9e137c..4533e8a11722 100644 --- a/packages/server-utils/test/integrations/postgres/postgres.test.ts +++ b/packages/server-utils/test/integrations/postgres/postgres.test.ts @@ -251,12 +251,12 @@ describe('postgresIntegration', () => { ); }); - it('requireParentSpan: does not create a span when there is no active span', async () => { + it('requires a parent when starting a query span', async () => { getActiveSpanSpy.mockReturnValue(undefined); const ctx: OrchestrionChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.PG_QUERY).tracePromise(async () => ({ rows: [] }), ctx); - expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + expect(startInactiveSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ onlyIfParent: true })); }); }); diff --git a/packages/server-utils/test/integrations/prisma-orchestrion.test.ts b/packages/server-utils/test/integrations/prisma-orchestrion.test.ts index 78a1e3bf1988..e59bc609fa54 100644 --- a/packages/server-utils/test/integrations/prisma-orchestrion.test.ts +++ b/packages/server-utils/test/integrations/prisma-orchestrion.test.ts @@ -206,13 +206,13 @@ describe('instrumentPrismaV8', () => { expect(span.end).toHaveBeenCalledTimes(1); }); - it('does not open a span without a parent', async () => { + it('requires a parent when starting an operation span', async () => { await tracingChannel(CHANNELS.PRISMA_CREATE).tracePromise(async () => ({ id: 1 }), { arguments: [], self: { modelName: 'User' }, }); - expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + expect(startInactiveSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ onlyIfParent: true })); }); it('async terminal: marks the span as errored when the promise rejects', async () => { diff --git a/packages/server-utils/test/integrations/prisma.test.ts b/packages/server-utils/test/integrations/prisma.test.ts index 2d02718b6d1e..00af81b1f7d4 100644 --- a/packages/server-utils/test/integrations/prisma.test.ts +++ b/packages/server-utils/test/integrations/prisma.test.ts @@ -1,5 +1,13 @@ import type { Span } from '@sentry/core'; -import { Client, createTransport, getCurrentScope, initAndBind, resolvedSyncPromise, spanToJSON } from '@sentry/core'; +import { + Client, + createTransport, + getCurrentScope, + initAndBind, + resolvedSyncPromise, + spanToJSON, + startSpan, +} from '@sentry/core'; import { afterEach, describe, expect, it } from 'vitest'; import { instrumentPrisma } from '../../src/integrations/prisma'; import type { TracingHelper } from '../../src/integrations/prisma/types'; @@ -37,8 +45,10 @@ function initTestClient(): void { /** Runs a `db_query` span through the installed helper and returns the span it created. */ function runDbQuerySpan(attributes: Record): Span { let span: Span | undefined; - getHelper()?.runInChildSpan({ name: 'db_query', attributes }, createdSpan => { - span = createdSpan; + startSpan({ name: 'parent' }, () => { + getHelper()?.runInChildSpan({ name: 'db_query', attributes }, createdSpan => { + span = createdSpan; + }); }); return span!; } From 56e6673c7fed941b13d584cdd4314ab6bda50945 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 17:03:15 +0100 Subject: [PATCH 7/7] fix(db): Allow connection spans without a parent Co-Authored-By: GPT-6 --- .../ioredis-dc/scenario-ioredis-5-11.mjs | 4 +- .../tracing/ioredis-dc/scenario-ioredis-6.mjs | 4 +- .../suites/tracing/ioredis-dc/test.ts | 21 ++++----- .../tracing/redis-cache/scenario-redis-4.mjs | 6 +-- .../tracing/redis-cache/scenario-redis-5.mjs | 4 +- .../suites/tracing/redis-cache/test.ts | 44 ++++++++++--------- .../redis-dc/scenario-redis-5-tracing.mjs | 4 +- .../suites/tracing/redis-dc/test.ts | 16 +++---- .../suites/tracing/redis/test.ts | 12 +++-- .../src/integrations/generic-pool.ts | 1 - .../mysql2/mysql2-dc-subscriber.ts | 1 - .../server-utils/src/integrations/postgres.ts | 2 +- .../src/integrations/prisma/tracing-helper.ts | 4 +- .../src/integrations/redis/index.ts | 1 - .../redis/ioredis-channel-subscriber.ts | 1 - .../integrations/redis/redis-dc-subscriber.ts | 1 - .../mysql2/mysql2-dc-subscriber.test.ts | 22 +++++++--- .../integrations/postgres/postgres.test.ts | 4 ++ 18 files changed, 82 insertions(+), 70 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-5-11.mjs b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-5-11.mjs index 8d975717f0c1..402adb6f364f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-5-11.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-5-11.mjs @@ -4,14 +4,14 @@ async function run() { const { default: Redis } = await import('ioredis-5'); const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true }); + await redisClient.connect(); + await Sentry.startSpan( { name: 'Test Span IORedis DC', op: 'test-span-ioredis-dc', }, async () => { - await redisClient.connect(); - try { await redisClient.set('dc-test-key', 'test-value'); await redisClient.set('dc-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-6.mjs b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-6.mjs index c281afde049d..994c679d9d3a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-6.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/scenario-ioredis-6.mjs @@ -4,14 +4,14 @@ async function run() { const { default: Redis } = await import('ioredis-6'); const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true }); + await redisClient.connect(); + await Sentry.startSpan( { name: 'Test Span IORedis DC', op: 'test-span-ioredis-dc', }, async () => { - await redisClient.connect(); - try { await redisClient.set('dc-test-key', 'test-value'); await redisClient.set('dc-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts index 7250567151a8..fe1cc5bb7b09 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts @@ -21,7 +21,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; -import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB, DB_QUERY } from '@sentry/conventions/op'; +import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; import { afterAll, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -101,20 +101,21 @@ describeWithDockerCompose( await createTestRunner() .expect({ span: container => { - const segments = container.items.filter(item => item.is_segment); - expect(segments.map(item => item.name)).toEqual([SEGMENT_NAME]); - const parent = segments[0]!; + // The connect span opens its own segment but shares the trace with the test span, + // so both segments arrive in the same container. + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'redis-connect', + SEGMENT_NAME, + ]); + // ioredis' own handshake commands (`client SETINFO`, `info`) are emitted on the + // channel too, but belong to the connect segment — the test span's children are the + // commands the scenario issues. const spans = container.items.filter( - item => !item.is_segment && item.parent_span_id === parent.span_id, + item => !item.is_segment && item.attributes[SENTRY_SEGMENT_NAME]?.value === SEGMENT_NAME, ); expect(spans).toEqual([ - { - ...(streamedSpan('redis-connect', DB, {}) as Record), - parent_span_id: parent.span_id, - trace_id: parent.trace_id, - }, streamedSpan(`set ${HOST}:${PORT}`, DB_QUERY, { [DB_OPERATION_NAME]: 'set', [DB_QUERY_TEXT]: 'set dc-test-key ?', diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.mjs index d148751d6773..592602d7222e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.mjs @@ -2,11 +2,11 @@ import * as Sentry from '@sentry/node'; import { createClient } from 'redis-4'; async function run() { - const redisClient = createClient({ + const redisClient = await createClient({ socket: { port: 6383, }, - }); + }).connect(); await Sentry.startSpan( { @@ -14,8 +14,6 @@ async function run() { op: 'test-span-redis-4', }, async () => { - await redisClient.connect(); - try { await redisClient.set('redis-test-key', 'test-value'); await redisClient.set('redis-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-5.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-5.mjs index 8a4b5ec1d9af..a32aee1c3af2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-5.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-5.mjs @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node'; import { createClient } from 'redis-5'; async function run() { - const redisClient = createClient({ socket: { port: 6383 } }); + const redisClient = await createClient({ socket: { port: 6383 } }).connect(); await Sentry.startSpan( { @@ -10,8 +10,6 @@ async function run() { op: 'test-span-redis-5', }, async () => { - await redisClient.connect(); - try { await redisClient.set('redis-5-test-key', 'test-value'); await redisClient.set('redis-5-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index 22d3be2ed943..4861255f59ab 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts @@ -25,7 +25,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; -import { CACHE_GET, CACHE_PUT, CACHE_REMOVE, DB, DB_QUERY } from '@sentry/conventions/op'; +import { CACHE_GET, CACHE_PUT, CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; @@ -153,7 +153,12 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory createEsmAndCjsTests(__dirname, 'scenario-ioredis.mjs', 'instrument-ioredis.mjs', (createTestRunner, test) => { test('should create cache spans for prefixed keys (ioredis)', { timeout: 60_000 }, async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + await createTestRunner() + .unordered() + .expect({ transaction: { transaction: 'connect' } }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); }); }); }); @@ -238,9 +243,14 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory .withEnv({ STREAMED: 'true' }) .expect({ span: container => { - expect(container.items.find(item => item.is_segment)?.name).toBe(segmentName); + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'connect', + segmentName, + ]); - expect(childSpans(container)).toEqual([ + expect( + childSpans(container).filter(item => item.attributes[SENTRY_SEGMENT_NAME]?.value === segmentName), + ).toEqual([ span('set localhost:6383', redisSpanOp, { [DB_OPERATION_NAME]: 'set', [DB_QUERY_TEXT]: 'set test-key [1 other arguments]', @@ -323,16 +333,14 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory await createTestRunner() .expect({ span: container => { - const segments = container.items.filter(item => item.is_segment); - expect(segments.map(item => item.name)).toEqual([segmentName]); - const parent = segments[0]!; + // The connect span opens its own segment, but shares the trace with the test span, + // so both segments arrive in the same container. + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'redis-connect', + segmentName, + ]); expect(childSpans(container)).toEqual([ - { - ...(span('redis-connect', DB, {}) as Record), - parent_span_id: parent.span_id, - trace_id: parent.trace_id, - }, span('SET localhost:6383', redisSpanOp, { [DB_OPERATION_NAME]: 'SET', [DB_QUERY_TEXT]: 'SET redis-test-key [1 other arguments]', @@ -429,16 +437,12 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory await createTestRunner() .expect({ span: container => { - const segments = container.items.filter(item => item.is_segment); - expect(segments.map(item => item.name)).toEqual([segmentName]); - const parent = segments[0]!; + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'redis-connect', + segmentName, + ]); expect(childSpans(container)).toEqual([ - { - ...(span('redis-connect', DB, {}) as Record), - parent_span_id: parent.span_id, - trace_id: parent.trace_id, - }, span('SET localhost:6383', redisSpanOp, { [DB_OPERATION_NAME]: 'SET', [DB_QUERY_TEXT]: 'SET redis-5-test-key [1 other arguments]', diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-dc/scenario-redis-5-tracing.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-dc/scenario-redis-5-tracing.mjs index 612793899ff2..661b37ef9c6d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-dc/scenario-redis-5-tracing.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-dc/scenario-redis-5-tracing.mjs @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node'; async function run() { const { createClient } = await import('redis-5-tracing'); - const redisClient = createClient({ socket: { host: '127.0.0.1', port: 6381 } }); + const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect(); await Sentry.startSpan( { @@ -10,8 +10,6 @@ async function run() { op: 'test-span-redis-5-dc', }, async () => { - await redisClient.connect(); - try { await redisClient.set('dc-test-key', 'test-value'); await redisClient.set('dc-cache:test-key', 'test-value'); diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts index d5fef4490755..a9f10789dc48 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts @@ -23,7 +23,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; -import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB, DB_QUERY } from '@sentry/conventions/op'; +import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; import { afterAll, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -126,20 +126,18 @@ describeWithDockerCompose( await createTestRunner() .expect({ span: container => { - const segments = container.items.filter(item => item.is_segment); - expect(segments.map(item => item.name)).toEqual([SEGMENT_NAME]); - const parent = segments[0]!; + // The connect span opens its own segment but shares the trace with the test span, + // so both segments arrive in the same container. + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'redis-connect', + SEGMENT_NAME, + ]); const spans = container.items.filter( item => !item.is_segment && item.attributes[SENTRY_SEGMENT_NAME]?.value === SEGMENT_NAME, ); expect(spans).toEqual([ - { - ...(streamedSpan('redis-connect', DB, {}) as Record), - parent_span_id: parent.span_id, - trace_id: parent.trace_id, - }, streamedSpan(`SET ${HOST}:${PORT}`, DB_QUERY, { [DB_OPERATION_NAME]: 'SET', [DB_QUERY_TEXT]: 'SET dc-test-key ?', diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts index 36decf4b6001..22111b91a006 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts @@ -92,10 +92,16 @@ describeWithDockerCompose('redis auto instrumentation', { workingDirectory: [__d await createTestRunner() .expect({ span: container => { - const segmentSpan = container.items.find(item => item.is_segment); - expect(segmentSpan?.name).toBe('Test Span'); + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'connect', + 'Test Span', + ]); - const dbSpans = container.items.filter(item => item.attributes[SENTRY_OP]?.value === redisSpanOp); + const dbSpans = container.items.filter( + item => + item.attributes[SENTRY_OP]?.value === redisSpanOp && + item.attributes[SENTRY_SEGMENT_NAME]?.value === 'Test Span', + ); expect(dbSpans).toEqual([ expectedDbSpan({ operation: 'set', statement: 'set test-key [1 other arguments]' }), diff --git a/packages/server-utils/src/integrations/generic-pool.ts b/packages/server-utils/src/integrations/generic-pool.ts index da8a853d2c51..ca27d9bd29ff 100644 --- a/packages/server-utils/src/integrations/generic-pool.ts +++ b/packages/server-utils/src/integrations/generic-pool.ts @@ -35,7 +35,6 @@ function instrumentGenericPool(): void { diagnosticsChannel.tracingChannel(CHANNELS.GENERIC_POOL_ACQUIRE), () => startInactiveSpan({ - onlyIfParent: true, name: 'generic-pool.acquire', attributes: { [SENTRY_OP]: DB, diff --git a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts index 19216e3c3115..225c1ebdd470 100644 --- a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts @@ -126,7 +126,6 @@ function setupQueryChannel(tracingChannel: MySQL2TracingChannelFactory, channelN function setupConnectChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string, spanName: string): void { bindTracingChannelToSpan(tracingChannel(channelName), data => { return startInactiveSpan({ - onlyIfParent: true, name: spanName, attributes: { [SENTRY_ORIGIN]: ORIGIN, diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 1715ce88ef8b..a95ff7899981 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -142,7 +142,7 @@ function subscribeQueryLikeChannel( // `sentry.kind` matches across the OTel and diagnostics-channel paths. const spanOptions = getSpanOptions(data); return startInactiveSpan({ - onlyIfParent: true, + onlyIfParent: channelName !== CHANNELS.PG_CONNECT && channelName !== CHANNELS.PGPOOL_CONNECT, ...spanOptions, attributes: { ...spanOptions.attributes, [SENTRY_KIND]: 'client' }, }); diff --git a/packages/server-utils/src/integrations/prisma/tracing-helper.ts b/packages/server-utils/src/integrations/prisma/tracing-helper.ts index 140747a4b992..ba2b083e23c5 100644 --- a/packages/server-utils/src/integrations/prisma/tracing-helper.ts +++ b/packages/server-utils/src/integrations/prisma/tracing-helper.ts @@ -296,7 +296,7 @@ export class ActiveTracingHelper implements TracingHelper { const attributes = buildSpanAttributes(name, options.attributes); const spanOptions = { name: buildSpanName(name, attributes), - onlyIfParent: true, + onlyIfParent: name !== 'prisma:client:connect', attributes, kind: options.kind, links: options.links, @@ -334,7 +334,7 @@ function dispatchEngineSpan( startSpanManual( { name: buildSpanName(engineSpan.name, attributes), - onlyIfParent: true, + onlyIfParent: engineSpan.name !== 'prisma:engine:connect', attributes: { ...attributes, [SENTRY_KIND]: engineSpan.kind === 'client' ? 'client' : undefined, diff --git a/packages/server-utils/src/integrations/redis/index.ts b/packages/server-utils/src/integrations/redis/index.ts index 701a260286b8..2c5e86dc5bdb 100644 --- a/packages/server-utils/src/integrations/redis/index.ts +++ b/packages/server-utils/src/integrations/redis/index.ts @@ -245,7 +245,6 @@ function bindNodeRedisConnectChannel(): void { bindTracingChannelToSpan(channel, data => { const options = (data.self as NodeRedisClient | undefined)?.options; return startInactiveSpan({ - onlyIfParent: true, name: 'redis-connect', attributes: { [SENTRY_KIND]: 'client', diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index 2eb19c7b2820..6684ba3cc066 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -123,7 +123,6 @@ export function instrumentIoredis(options: RedisCacheOptions): void { bindTracingChannelToSpan(connectChannel, data => { const { host, port } = getConnectionOptions(data.self); return startInactiveSpan({ - onlyIfParent: true, name: 'connect', attributes: { [SENTRY_KIND]: 'client', diff --git a/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts b/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts index 88d7ad0846ab..7e3887d82b75 100644 --- a/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts @@ -202,7 +202,6 @@ function setupBatchChannel( function setupConnectChannel(tracingChannel: RedisTracingChannelFactory, channelName: string): void { bindTracingChannelToSpan(tracingChannel(channelName), data => { return startInactiveSpan({ - onlyIfParent: true, name: 'redis-connect', attributes: { [SENTRY_ORIGIN]: ORIGIN, diff --git a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts index df6f688de355..3b93097eae57 100644 --- a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts +++ b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts @@ -349,12 +349,17 @@ describe('subscribeMysql2DiagnosticChannels', () => { describe('connect channels', () => { it('creates a connect span without db.query.text', async () => { - const { span } = await traceOperation( - MYSQL2_DC_CHANNEL_CONNECT, + let span: Span | undefined; + await tracingChannel(MYSQL2_DC_CHANNEL_CONNECT).tracePromise( + async () => { + span = getActiveSpan(); + expect(span?.isRecording()).toBe(true); + }, { database: 'test', serverAddress: '127.0.0.1', serverPort: 3306, user: 'root' }, - { result: undefined }, ); + expect(spanToJSON(span!).parent_span_id).toBeUndefined(); + const json = spanToJSON(span!); expect(json.name).toBe('mysql2.connect'); expect(json.attributes['sentry.op']).toBe('db'); @@ -367,12 +372,17 @@ describe('subscribeMysql2DiagnosticChannels', () => { }); it('names the pool connect span distinctly', async () => { - const { span } = await traceOperation( - MYSQL2_DC_CHANNEL_POOL_CONNECT, + let span: Span | undefined; + await tracingChannel(MYSQL2_DC_CHANNEL_POOL_CONNECT).tracePromise( + async () => { + span = getActiveSpan(); + expect(span?.isRecording()).toBe(true); + }, { database: 'test', serverAddress: '127.0.0.1', serverPort: 3306 }, - { result: undefined }, ); + expect(spanToJSON(span!).parent_span_id).toBeUndefined(); + expect(spanToJSON(span!).name).toBe('mysql2.pool.connect'); }); diff --git a/packages/server-utils/test/integrations/postgres/postgres.test.ts b/packages/server-utils/test/integrations/postgres/postgres.test.ts index 4533e8a11722..e6c190932cb1 100644 --- a/packages/server-utils/test/integrations/postgres/postgres.test.ts +++ b/packages/server-utils/test/integrations/postgres/postgres.test.ts @@ -171,6 +171,7 @@ describe('postgresIntegration', () => { }); it('connect: builds a `pg.connect` span with no origin (defaults to manual)', async () => { + getActiveSpanSpy.mockReturnValue(undefined); const ctx: OrchestrionChannelContext = { arguments: [], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.PG_CONNECT).tracePromise(async () => undefined, ctx); @@ -178,6 +179,7 @@ describe('postgresIntegration', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'pg.connect', + onlyIfParent: false, attributes: expect.objectContaining({ 'sentry.op': 'db', 'db.system.name': 'postgresql', @@ -192,6 +194,7 @@ describe('postgresIntegration', () => { }); it('pool connect: builds a `pg-pool.connect` span with masked connection string + pool attributes', async () => { + getActiveSpanSpy.mockReturnValue(undefined); const ctx: OrchestrionChannelContext = { arguments: [], self: { @@ -209,6 +212,7 @@ describe('postgresIntegration', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'pg-pool.connect', + onlyIfParent: false, attributes: expect.objectContaining({ 'sentry.op': 'db', 'db.system.name': 'postgresql',