Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpan } from '@sentry/core';
import {
DB_COLLECTION_NAME,
DB_NAMESPACE,
Expand Down Expand Up @@ -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<string, SerializedStreamedSpan>();
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' } },
);
});
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpan } from '@sentry/core';
import {
DB_COLLECTION_NAME,
DB_NAMESPACE,
Expand Down Expand Up @@ -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<string, SerializedStreamedSpan>();
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,
})),
);
});
});
});
7 changes: 5 additions & 2 deletions packages/server-utils/src/integrations/mongoose/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

Expand All @@ -157,7 +160,7 @@ function channel(channelName: string): SentryTracingChannel<MongooseChannelConte
return diagnosticsChannel.tracingChannel<MongooseChannelContext>(channelName);
}

function bindExecSpan(channelName: string, getSpan: (self: object) => Span): void {
function bindExecSpan(channelName: string, getSpan: (self: object) => Span | undefined): void {
bindTracingChannelToSpan<MongooseChannelContext>(
diagnosticsChannel.tracingChannel<MongooseChannelContext>(channelName),
data => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,38 +113,42 @@ export function subscribeMongooseDiagnosticChannels(tracingChannel: MongooseTrac
}

function setupChannel(tracingChannel: MongooseTracingChannelFactory, channelName: string): void {
bindTracingChannelToSpan(tracingChannel<MongooseTracingData>(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<MongooseTracingData>(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 },
);
}

/**
Expand Down