From 95a80a3394ab87983772ba33ffb21a964b88c4c3 Mon Sep 17 00:00:00 2001 From: lazerg Date: Thu, 29 Oct 2026 11:56:42 +0500 Subject: [PATCH] fix(server-utils): Require a parent span for Mongoose operations Co-Authored-By: Claude Opus 5.5 --- .../scenario-no-parent.mjs | 26 +++++++ .../tracing/mongoose-tracing-channel/test.ts | 43 ++++++++++++ .../tracing/mongoose/scenario-no-parent.mjs | 30 ++++++++ .../suites/tracing/mongoose/test.ts | 37 ++++++++++ .../src/integrations/mongoose/index.ts | 7 +- .../mongoose/mongoose-dc-subscriber.ts | 68 ++++++++++--------- 6 files changed, 177 insertions(+), 34 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/scenario-no-parent.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mongoose/scenario-no-parent.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/scenario-no-parent.mjs b/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/scenario-no-parent.mjs new file mode 100644 index 000000000000..84a0c090d8b0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/scenario-no-parent.mjs @@ -0,0 +1,26 @@ +import * as Sentry from '@sentry/node'; +import mongoose from 'mongoose'; + +async function run() { + await mongoose.connect(process.env.MONGO_URL || ''); + + const BlogPost = mongoose.model('BlogPost', new mongoose.Schema({ title: String })); + + await new BlogPost({ title: 'Test' }).save(); + await BlogPost.find({}); + + const parent = Sentry.startInactiveSpan({ name: 'parent operation' }); + await Sentry.withActiveSpan(parent, async () => { + await BlogPost.findOne({}); + }); + + await BlogPost.find({}); + await mongoose.disconnect(); + + // Flush every query before the parent signals that span collection is complete. + await Sentry.flush(); + parent.end(); + await Sentry.flush(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/test.ts index 56efaad69243..de41087f0d26 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpan } from '@sentry/core'; import { DB_COLLECTION_NAME, DB_NAMESPACE, @@ -196,4 +197,46 @@ describe('Mongoose tracing channel Test', () => { }, { additionalDependencies: { mongoose: '^9.7' } }, ); + + createEsmAndCjsTests( + __dirname, + 'scenario-no-parent.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('only instruments operations with an active parent span', async () => { + const runner = createTestRunner(); + const dbSpans = new Map(); + const spansPromise = runner.collectStreamedSpans(spans => { + for (const span of spans) { + if (span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mongoose.native_channel') { + dbSpans.set(span.span_id, span); + } + } + return spans.some(span => span.is_segment && span.name === 'parent operation'); + }); + + await runner.unordered().start().completed(); + + const spans = await spansPromise; + const parent = spans.find(span => span.is_segment && span.name === 'parent operation'); + expect(parent).toBeDefined(); + expect( + Array.from(dbSpans.values(), span => ({ + operation: span.attributes[DB_OPERATION_NAME]?.value, + parentSpanId: span.parent_span_id, + traceId: span.trace_id, + isSegment: span.is_segment, + })), + ).toEqual([ + { + operation: 'findOne', + parentSpanId: parent?.span_id, + traceId: parent?.trace_id, + isSegment: false, + }, + ]); + }); + }, + { additionalDependencies: { mongoose: '^9.7' } }, + ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose/scenario-no-parent.mjs b/dev-packages/node-integration-tests/suites/tracing/mongoose/scenario-no-parent.mjs new file mode 100644 index 000000000000..461a80c50a71 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose/scenario-no-parent.mjs @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node'; +import mongoose from 'mongoose'; + +async function run() { + await mongoose.connect(process.env.MONGO_URL || ''); + + const BlogPost = mongoose.model('BlogPost', new mongoose.Schema({ title: String })); + + await new BlogPost({ title: 'Test' }).save(); + await BlogPost.find({}); + + let pendingQuery; + const parent = Sentry.startInactiveSpan({ name: 'parent operation' }); + await Sentry.withActiveSpan(parent, async () => { + await BlogPost.findOne({}); + pendingQuery = BlogPost.countDocuments({}); + }); + + // Built inside the parent, so it still parents to it when executed outside. + await pendingQuery; + await BlogPost.find({}); + await mongoose.disconnect(); + + // Flush every query before the parent signals that span collection is complete. + await Sentry.flush(); + parent.end(); + await Sentry.flush(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts index beeb52aa100d..800c4c2ff7b6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpan } from '@sentry/core'; import { DB_COLLECTION_NAME, DB_NAMESPACE, @@ -107,4 +108,40 @@ describe('Mongoose experimental Test', () => { .completed(); }); }); + + createEsmAndCjsTests(__dirname, 'scenario-no-parent.mjs', 'instrument.mjs', (createTestRunner, test) => { + test('only instruments operations with an active or build-time parent span', async () => { + const runner = createTestRunner(); + const dbSpans = new Map(); + const spansPromise = runner.collectStreamedSpans(spans => { + for (const span of spans) { + if (span.attributes[SENTRY_ORIGIN]?.value === origin) { + dbSpans.set(span.span_id, span); + } + } + return spans.some(span => span.is_segment && span.name === 'parent operation'); + }); + + await runner.unordered().start().completed(); + + const spans = await spansPromise; + const parent = spans.find(span => span.is_segment && span.name === 'parent operation'); + expect(parent).toBeDefined(); + expect( + Array.from(dbSpans.values(), span => ({ + operation: span.attributes[DB_OPERATION_NAME]?.value, + parentSpanId: span.parent_span_id, + traceId: span.trace_id, + isSegment: span.is_segment, + })), + ).toEqual( + ['findOne', 'countDocuments'].map(operation => ({ + operation, + parentSpanId: parent?.span_id, + traceId: parent?.trace_id, + isSegment: false, + })), + ); + }); + }); }); diff --git a/packages/server-utils/src/integrations/mongoose/index.ts b/packages/server-utils/src/integrations/mongoose/index.ts index 62b7ead8b720..39a738266b6a 100644 --- a/packages/server-utils/src/integrations/mongoose/index.ts +++ b/packages/server-utils/src/integrations/mongoose/index.ts @@ -146,7 +146,10 @@ function startSpan( modelName: string | undefined, operation: string, parentSpan?: Span, -): Span { +): Span | undefined { + if (!parentSpan && !getActiveSpan()) { + return undefined; + } return startMongooseLegacySpan({ collection, modelName, operation, origin: ORIGIN, parentSpan }); } @@ -157,7 +160,7 @@ function channel(channelName: string): SentryTracingChannel(channelName); } -function bindExecSpan(channelName: string, getSpan: (self: object) => Span): void { +function bindExecSpan(channelName: string, getSpan: (self: object) => Span | undefined): void { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(channelName), data => { 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..14401f551c07 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts @@ -113,38 +113,42 @@ export function subscribeMongooseDiagnosticChannels(tracingChannel: MongooseTrac } function setupChannel(tracingChannel: MongooseTracingChannelFactory, channelName: string): void { - bindTracingChannelToSpan(tracingChannel(channelName), data => { - const collection = data.collection; - const queryText = redactMongoQuery(data.args?.pipeline ?? data.args?.filter); - const batchSize = getBatchSize(data); - - const client = getClient(); - const target = collection || data.database; - const name = - client && hasSpanStreamingEnabled(client) - ? target - ? `${data.operation} ${target}` - : DB_SYSTEM_NAME_VALUE_MONGODB - : collection - ? `mongoose.${collection}.${data.operation}` - : `mongoose.${data.operation}`; - - return startInactiveSpan({ - name, - attributes: { - [SENTRY_ORIGIN]: ORIGIN, - [SENTRY_OP]: DB, - [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB, - [DB_OPERATION_NAME]: data.operation, - [DB_COLLECTION_NAME]: collection ?? undefined, - [DB_NAMESPACE]: data.database ?? undefined, - [DB_QUERY_TEXT]: queryText ?? undefined, - [DB_OPERATION_BATCH_SIZE]: batchSize ?? undefined, - [SERVER_ADDRESS]: data.serverAddress ?? undefined, - [SERVER_PORT]: data.serverPort ?? undefined, - }, - }); - }); + bindTracingChannelToSpan( + tracingChannel(channelName), + data => { + const collection = data.collection; + const queryText = redactMongoQuery(data.args?.pipeline ?? data.args?.filter); + const batchSize = getBatchSize(data); + + const client = getClient(); + const target = collection || data.database; + const name = + client && hasSpanStreamingEnabled(client) + ? target + ? `${data.operation} ${target}` + : DB_SYSTEM_NAME_VALUE_MONGODB + : collection + ? `mongoose.${collection}.${data.operation}` + : `mongoose.${data.operation}`; + + return startInactiveSpan({ + name, + attributes: { + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: DB, + [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB, + [DB_OPERATION_NAME]: data.operation, + [DB_COLLECTION_NAME]: collection ?? undefined, + [DB_NAMESPACE]: data.database ?? undefined, + [DB_QUERY_TEXT]: queryText ?? undefined, + [DB_OPERATION_BATCH_SIZE]: batchSize ?? undefined, + [SERVER_ADDRESS]: data.serverAddress ?? undefined, + [SERVER_PORT]: data.serverPort ?? undefined, + }, + }); + }, + { requiresParentSpan: true }, + ); } /**