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..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 @@ -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]', 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/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..c11653bf970d 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, 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/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/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/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..225c1ebdd470 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,49 @@ 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({ + 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..a95ff7899981 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: channelName !== CHANNELS.PG_CONNECT && channelName !== CHANNELS.PGPOOL_CONNECT, ...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..ba2b083e23c5 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: name !== 'prisma:client:connect', attributes, kind: options.kind, links: options.links, @@ -332,6 +334,7 @@ function dispatchEngineSpan( startSpanManual( { name: buildSpanName(engineSpan.name, attributes), + 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 7176a14b57ec..2c5e86dc5bdb 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', @@ -268,6 +269,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..6684ba3cc066 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,15 @@ 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({ + 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..7e3887d82b75 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, 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..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 @@ -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', () => { @@ -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/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..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', @@ -251,12 +255,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!; }