Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/node';
import mongoose from 'mongoose';

async function run() {
await mongoose.connect(process.env.MONGO_URL || '');

const BlogPostSchema = new mongoose.Schema({
title: String,
body: String,
date: Date,
});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
await post.save();

// Filter with a real value, to assert it is redacted out of `db.query.text`.
await BlogPost.findOne({ title: 'Test' });

await BlogPost.aggregate([{ $match: { title: 'Test' } }]);

await BlogPost.insertMany([
{ title: 'Insert1', body: 'b', date: new Date() },
{ title: 'Insert2', body: 'b', date: new Date() },
]);

await BlogPost.bulkWrite([
{ insertOne: { document: { title: 'Bulk1', body: 'b', date: new Date() } } },
{ insertOne: { document: { title: 'Bulk2', body: 'b', date: new Date() } } },
]);

// Drive a cursor to exercise the `mongoose:cursor:next` channel.
const cursor = BlogPost.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
// iterate
}
},
);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
import { MongoMemoryServer } from 'mongodb-memory-server-global';
import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// mongoose >= 9.7.0 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes
// to those channels (`subscribeMongooseDiagnosticChannels`) instead of monkey-patching. This suite
// pins `^9.7` and asserts the diagnostics-channel path: stable OTel DB semconv attributes, redacted
// query text, span relationships, and that the legacy IITM patcher does NOT also fire (no double
// instrumentation). mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose tracing channel Test', () => {
let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URL = mongoServer.getUri();
}, 30000);

afterAll(async () => {
if (mongoServer) {
await mongoServer.stop();
}
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
origin: 'auto.db.mongoose.diagnostic_channel',
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});

test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
// The monkey-patch path (origin `auto.db.otel.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.otel.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
},
})
.start()
.completed();
});

test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
}
},
})
.start()
.completed();
});

test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === 'auto.db.otel.mongo',
);
expect(driverChild).toBeDefined();
},
})
.start()
.completed();
});
},
{ additionalDependencies: { mongoose: '^9.7' } },
);
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,10 @@ import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Pins mongoose 9 (top of our supported `>=5.9.7 <10` range) so the latest major is exercised
// against a real mongoose. mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
let mongoServer: MongoMemoryServer;

Expand DownExpand Up@@ -55,6 +57,6 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
},
{ additionalDependencies: { mongoose: '^9' } },
{ additionalDependencies: { mongoose: '>=9 <9.7' } },
);
});
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
import { MongooseInstrumentation } from './vendored/mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { mongooseIntegration as mongooseChannelIntegration } from '@sentry/server-utils';

const INTEGRATION_NAME = 'Mongoose' as const;

export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation());

const _mongooseIntegration = (() => {
return {
// The diagnostics_channel subscription (mongoose >= 9.7) lives in server-utils so it is shared
// across server runtimes; we extend it here to also run the IITM-based patcher for mongoose < 9.7.
return extendIntegration(mongooseChannelIntegration(), {
name: INTEGRATION_NAME,
setupOnce() {
instrumentMongoose();
},
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload skips mongoose channel subscribe

Medium Severity

For mongoose >=9.7, diagnostics-channel subscription runs only in the server-utils integration setupOnce, while preloadOpenTelemetry invokes instrumentMongoose alone. The IITM patcher no longer covers 9.7+, so mongoose work between preload and Sentry.init() (or any preload-only usage) is not traced.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9cbf9df. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is ok.

}) satisfies IntegrationFn;

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,10 @@ export class MongooseInstrumentation extends InstrumentationBase<Instrumentation
protected init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'mongoose',
['>=5.9.7 <10'],
// mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by
// `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not
// overlap it — otherwise every operation would emit two mongoose spans.
['>=5.9.7 <9.7.0'],
this.patch.bind(this),
this.unpatch.bind(this),
);
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
* @module
*/

export { mongooseIntegration } from './mongoose';
export {
IOREDIS_DC_CHANNEL_COMMAND,
IOREDIS_DC_CHANNEL_CONNECT,
Expand Down
30 changes: 30 additions & 0 deletions packages/server-utils/src/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core';
import * as dc from 'node:diagnostics_channel';
import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber';

const _mongooseIntegration = (() => {
return {
name: 'Mongoose',
setupOnce() {
// Bail on Node <= 18.18.0, where `tracingChannel` does not exist.
if (!dc.tracingChannel) {
return;
}

// Subscribe to mongoose's native tracing channels (mongoose >= 9.7).
// This is a no-op on versions that don't publish to the channels, so it is always safe to call.
waitForTracingChannelBinding(() => {
subscribeMongooseDiagnosticChannels(dc.tracingChannel);
});
},
};
}) satisfies IntegrationFn;

/**
* Auto-instrument the [mongoose](https://www.npmjs.com/package/mongoose) library via its native
* `node:diagnostics_channel` tracing channels (mongoose >= 9.7).
*
* On older mongoose versions the channels are never published to, so this integration is inert and
* the IITM-based patcher (gated to `< 9.7.0`) handles instrumentation instead.
*/
export const mongooseIntegration = defineIntegration(_mongooseIntegration);
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/node';
import mongoose from 'mongoose';

async function run() {
await mongoose.connect(process.env.MONGO_URL || '');

const BlogPostSchema = new mongoose.Schema({
title: String,
body: String,
date: Date,
});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
await post.save();

// Filter with a real value, to assert it is redacted out of `db.query.text`.
await BlogPost.findOne({ title: 'Test' });

await BlogPost.aggregate([{ $match: { title: 'Test' } }]);

await BlogPost.insertMany([
{ title: 'Insert1', body: 'b', date: new Date() },
{ title: 'Insert2', body: 'b', date: new Date() },
]);

await BlogPost.bulkWrite([
{ insertOne: { document: { title: 'Bulk1', body: 'b', date: new Date() } } },
{ insertOne: { document: { title: 'Bulk2', body: 'b', date: new Date() } } },
]);

// Drive a cursor to exercise the `mongoose:cursor:next` channel.
const cursor = BlogPost.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
// iterate
}
},
);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
import { MongoMemoryServer } from 'mongodb-memory-server-global';
import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// mongoose >= 9.7.0 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes
// to those channels (`subscribeMongooseDiagnosticChannels`) instead of monkey-patching. This suite
// pins `^9.7` and asserts the diagnostics-channel path: stable OTel DB semconv attributes, redacted
// query text, span relationships, and that the legacy IITM patcher does NOT also fire (no double
// instrumentation). mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose tracing channel Test', () => {
let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URL = mongoServer.getUri();
}, 30000);

afterAll(async () => {
if (mongoServer) {
await mongoServer.stop();
}
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
origin: 'auto.db.mongoose.diagnostic_channel',
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});

test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
// The monkey-patch path (origin `auto.db.otel.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.otel.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
},
})
.start()
.completed();
});

test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
}
},
})
.start()
.completed();
});

test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === 'auto.db.otel.mongo',
);
expect(driverChild).toBeDefined();
},
})
.start()
.completed();
});
},
{ additionalDependencies: { mongoose: '^9.7' } },
);
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,10 @@ import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Pins mongoose 9 (top of our supported `>=5.9.7 <10` range) so the latest major is exercised
// against a real mongoose. mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
let mongoServer: MongoMemoryServer;

Expand DownExpand Up@@ -55,6 +57,6 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
},
{ additionalDependencies: { mongoose: '^9' } },
{ additionalDependencies: { mongoose: '>=9 <9.7' } },
);
});
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
import { MongooseInstrumentation } from './vendored/mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { mongooseIntegration as mongooseChannelIntegration } from '@sentry/server-utils';

const INTEGRATION_NAME = 'Mongoose' as const;

export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation());

const _mongooseIntegration = (() => {
return {
// The diagnostics_channel subscription (mongoose >= 9.7) lives in server-utils so it is shared
// across server runtimes; we extend it here to also run the IITM-based patcher for mongoose < 9.7.
return extendIntegration(mongooseChannelIntegration(), {
name: INTEGRATION_NAME,
setupOnce() {
instrumentMongoose();
},
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload skips mongoose channel subscribe

Medium Severity

For mongoose >=9.7, diagnostics-channel subscription runs only in the server-utils integration setupOnce, while preloadOpenTelemetry invokes instrumentMongoose alone. The IITM patcher no longer covers 9.7+, so mongoose work between preload and Sentry.init() (or any preload-only usage) is not traced.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9cbf9df. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is ok.

}) satisfies IntegrationFn;

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,10 @@ export class MongooseInstrumentation extends InstrumentationBase<Instrumentation
protected init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'mongoose',
['>=5.9.7 <10'],
// mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by
// `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not
// overlap it — otherwise every operation would emit two mongoose spans.
['>=5.9.7 <9.7.0'],
this.patch.bind(this),
this.unpatch.bind(this),
);
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
* @module
*/

export { mongooseIntegration } from './mongoose';
export {
IOREDIS_DC_CHANNEL_COMMAND,
IOREDIS_DC_CHANNEL_CONNECT,
Expand Down
30 changes: 30 additions & 0 deletions packages/server-utils/src/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core';
import * as dc from 'node:diagnostics_channel';
import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber';

const _mongooseIntegration = (() => {
return {
name: 'Mongoose',
setupOnce() {
// Bail on Node <= 18.18.0, where `tracingChannel` does not exist.
if (!dc.tracingChannel) {
return;
}

// Subscribe to mongoose's native tracing channels (mongoose >= 9.7).
// This is a no-op on versions that don't publish to the channels, so it is always safe to call.
waitForTracingChannelBinding(() => {
subscribeMongooseDiagnosticChannels(dc.tracingChannel);
});
},
};
}) satisfies IntegrationFn;

/**
* Auto-instrument the [mongoose](https://www.npmjs.com/package/mongoose) library via its native
* `node:diagnostics_channel` tracing channels (mongoose >= 9.7).
*
* On older mongoose versions the channels are never published to, so this integration is inert and
* the IITM-based patcher (gated to `< 9.7.0`) handles instrumentation instead.
*/
export const mongooseIntegration = defineIntegration(_mongooseIntegration);
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/node';
import mongoose from 'mongoose';

async function run() {
await mongoose.connect(process.env.MONGO_URL || '');

const BlogPostSchema = new mongoose.Schema({
title: String,
body: String,
date: Date,
});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
await post.save();

// Filter with a real value, to assert it is redacted out of `db.query.text`.
await BlogPost.findOne({ title: 'Test' });

await BlogPost.aggregate([{ $match: { title: 'Test' } }]);

await BlogPost.insertMany([
{ title: 'Insert1', body: 'b', date: new Date() },
{ title: 'Insert2', body: 'b', date: new Date() },
]);

await BlogPost.bulkWrite([
{ insertOne: { document: { title: 'Bulk1', body: 'b', date: new Date() } } },
{ insertOne: { document: { title: 'Bulk2', body: 'b', date: new Date() } } },
]);

// Drive a cursor to exercise the `mongoose:cursor:next` channel.
const cursor = BlogPost.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
// iterate
}
},
);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
import { MongoMemoryServer } from 'mongodb-memory-server-global';
import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// mongoose >= 9.7.0 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes
// to those channels (`subscribeMongooseDiagnosticChannels`) instead of monkey-patching. This suite
// pins `^9.7` and asserts the diagnostics-channel path: stable OTel DB semconv attributes, redacted
// query text, span relationships, and that the legacy IITM patcher does NOT also fire (no double
// instrumentation). mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose tracing channel Test', () => {
let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URL = mongoServer.getUri();
}, 30000);

afterAll(async () => {
if (mongoServer) {
await mongoServer.stop();
}
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
origin: 'auto.db.mongoose.diagnostic_channel',
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});

test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
// The monkey-patch path (origin `auto.db.otel.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.otel.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
},
})
.start()
.completed();
});

test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
}
},
})
.start()
.completed();
});

test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === 'auto.db.otel.mongo',
);
expect(driverChild).toBeDefined();
},
})
.start()
.completed();
});
},
{ additionalDependencies: { mongoose: '^9.7' } },
);
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,10 @@ import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Pins mongoose 9 (top of our supported `>=5.9.7 <10` range) so the latest major is exercised
// against a real mongoose. mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
let mongoServer: MongoMemoryServer;

Expand DownExpand Up@@ -55,6 +57,6 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
},
{ additionalDependencies: { mongoose: '^9' } },
{ additionalDependencies: { mongoose: '>=9 <9.7' } },
);
});
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
import { MongooseInstrumentation } from './vendored/mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { mongooseIntegration as mongooseChannelIntegration } from '@sentry/server-utils';

const INTEGRATION_NAME = 'Mongoose' as const;

export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation());

const _mongooseIntegration = (() => {
return {
// The diagnostics_channel subscription (mongoose >= 9.7) lives in server-utils so it is shared
// across server runtimes; we extend it here to also run the IITM-based patcher for mongoose < 9.7.
return extendIntegration(mongooseChannelIntegration(), {
name: INTEGRATION_NAME,
setupOnce() {
instrumentMongoose();
},
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload skips mongoose channel subscribe

Medium Severity

For mongoose >=9.7, diagnostics-channel subscription runs only in the server-utils integration setupOnce, while preloadOpenTelemetry invokes instrumentMongoose alone. The IITM patcher no longer covers 9.7+, so mongoose work between preload and Sentry.init() (or any preload-only usage) is not traced.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9cbf9df. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is ok.

}) satisfies IntegrationFn;

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,10 @@ export class MongooseInstrumentation extends InstrumentationBase<Instrumentation
protected init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'mongoose',
['>=5.9.7 <10'],
// mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by
// `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not
// overlap it — otherwise every operation would emit two mongoose spans.
['>=5.9.7 <9.7.0'],
this.patch.bind(this),
this.unpatch.bind(this),
);
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
* @module
*/

export { mongooseIntegration } from './mongoose';
export {
IOREDIS_DC_CHANNEL_COMMAND,
IOREDIS_DC_CHANNEL_CONNECT,
Expand Down
30 changes: 30 additions & 0 deletions packages/server-utils/src/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core';
import * as dc from 'node:diagnostics_channel';
import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber';

const _mongooseIntegration = (() => {
return {
name: 'Mongoose',
setupOnce() {
// Bail on Node <= 18.18.0, where `tracingChannel` does not exist.
if (!dc.tracingChannel) {
return;
}

// Subscribe to mongoose's native tracing channels (mongoose >= 9.7).
// This is a no-op on versions that don't publish to the channels, so it is always safe to call.
waitForTracingChannelBinding(() => {
subscribeMongooseDiagnosticChannels(dc.tracingChannel);
});
},
};
}) satisfies IntegrationFn;

/**
* Auto-instrument the [mongoose](https://www.npmjs.com/package/mongoose) library via its native
* `node:diagnostics_channel` tracing channels (mongoose >= 9.7).
*
* On older mongoose versions the channels are never published to, so this integration is inert and
* the IITM-based patcher (gated to `< 9.7.0`) handles instrumentation instead.
*/
export const mongooseIntegration = defineIntegration(_mongooseIntegration);
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/node';
import mongoose from 'mongoose';

async function run() {
await mongoose.connect(process.env.MONGO_URL || '');

const BlogPostSchema = new mongoose.Schema({
title: String,
body: String,
date: Date,
});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
await post.save();

// Filter with a real value, to assert it is redacted out of `db.query.text`.
await BlogPost.findOne({ title: 'Test' });

await BlogPost.aggregate([{ $match: { title: 'Test' } }]);

await BlogPost.insertMany([
{ title: 'Insert1', body: 'b', date: new Date() },
{ title: 'Insert2', body: 'b', date: new Date() },
]);

await BlogPost.bulkWrite([
{ insertOne: { document: { title: 'Bulk1', body: 'b', date: new Date() } } },
{ insertOne: { document: { title: 'Bulk2', body: 'b', date: new Date() } } },
]);

// Drive a cursor to exercise the `mongoose:cursor:next` channel.
const cursor = BlogPost.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
// iterate
}
},
);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
import { MongoMemoryServer } from 'mongodb-memory-server-global';
import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// mongoose >= 9.7.0 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes
// to those channels (`subscribeMongooseDiagnosticChannels`) instead of monkey-patching. This suite
// pins `^9.7` and asserts the diagnostics-channel path: stable OTel DB semconv attributes, redacted
// query text, span relationships, and that the legacy IITM patcher does NOT also fire (no double
// instrumentation). mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose tracing channel Test', () => {
let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URL = mongoServer.getUri();
}, 30000);

afterAll(async () => {
if (mongoServer) {
await mongoServer.stop();
}
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
origin: 'auto.db.mongoose.diagnostic_channel',
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});

test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
// The monkey-patch path (origin `auto.db.otel.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.otel.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
},
})
.start()
.completed();
});

test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
}
},
})
.start()
.completed();
});

test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === 'auto.db.otel.mongo',
);
expect(driverChild).toBeDefined();
},
})
.start()
.completed();
});
},
{ additionalDependencies: { mongoose: '^9.7' } },
);
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,10 @@ import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Pins mongoose 9 (top of our supported `>=5.9.7 <10` range) so the latest major is exercised
// against a real mongoose. mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
let mongoServer: MongoMemoryServer;

Expand DownExpand Up@@ -55,6 +57,6 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
},
{ additionalDependencies: { mongoose: '^9' } },
{ additionalDependencies: { mongoose: '>=9 <9.7' } },
);
});
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
import { MongooseInstrumentation } from './vendored/mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { mongooseIntegration as mongooseChannelIntegration } from '@sentry/server-utils';

const INTEGRATION_NAME = 'Mongoose' as const;

export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation());

const _mongooseIntegration = (() => {
return {
// The diagnostics_channel subscription (mongoose >= 9.7) lives in server-utils so it is shared
// across server runtimes; we extend it here to also run the IITM-based patcher for mongoose < 9.7.
return extendIntegration(mongooseChannelIntegration(), {
name: INTEGRATION_NAME,
setupOnce() {
instrumentMongoose();
},
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload skips mongoose channel subscribe

Medium Severity

For mongoose >=9.7, diagnostics-channel subscription runs only in the server-utils integration setupOnce, while preloadOpenTelemetry invokes instrumentMongoose alone. The IITM patcher no longer covers 9.7+, so mongoose work between preload and Sentry.init() (or any preload-only usage) is not traced.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9cbf9df. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is ok.

}) satisfies IntegrationFn;

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,10 @@ export class MongooseInstrumentation extends InstrumentationBase<Instrumentation
protected init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'mongoose',
['>=5.9.7 <10'],
// mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by
// `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not
// overlap it — otherwise every operation would emit two mongoose spans.
['>=5.9.7 <9.7.0'],
this.patch.bind(this),
this.unpatch.bind(this),
);
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
* @module
*/

export { mongooseIntegration } from './mongoose';
export {
IOREDIS_DC_CHANNEL_COMMAND,
IOREDIS_DC_CHANNEL_CONNECT,
Expand Down
30 changes: 30 additions & 0 deletions packages/server-utils/src/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core';
import * as dc from 'node:diagnostics_channel';
import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber';

const _mongooseIntegration = (() => {
return {
name: 'Mongoose',
setupOnce() {
// Bail on Node <= 18.18.0, where `tracingChannel` does not exist.
if (!dc.tracingChannel) {
return;
}

// Subscribe to mongoose's native tracing channels (mongoose >= 9.7).
// This is a no-op on versions that don't publish to the channels, so it is always safe to call.
waitForTracingChannelBinding(() => {
subscribeMongooseDiagnosticChannels(dc.tracingChannel);
});
},
};
}) satisfies IntegrationFn;

/**
* Auto-instrument the [mongoose](https://www.npmjs.com/package/mongoose) library via its native
* `node:diagnostics_channel` tracing channels (mongoose >= 9.7).
*
* On older mongoose versions the channels are never published to, so this integration is inert and
* the IITM-based patcher (gated to `< 9.7.0`) handles instrumentation instead.
*/
export const mongooseIntegration = defineIntegration(_mongooseIntegration);
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/node';
import mongoose from 'mongoose';

async function run() {
await mongoose.connect(process.env.MONGO_URL || '');

const BlogPostSchema = new mongoose.Schema({
title: String,
body: String,
date: Date,
});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
await post.save();

// Filter with a real value, to assert it is redacted out of `db.query.text`.
await BlogPost.findOne({ title: 'Test' });

await BlogPost.aggregate([{ $match: { title: 'Test' } }]);

await BlogPost.insertMany([
{ title: 'Insert1', body: 'b', date: new Date() },
{ title: 'Insert2', body: 'b', date: new Date() },
]);

await BlogPost.bulkWrite([
{ insertOne: { document: { title: 'Bulk1', body: 'b', date: new Date() } } },
{ insertOne: { document: { title: 'Bulk2', body: 'b', date: new Date() } } },
]);

// Drive a cursor to exercise the `mongoose:cursor:next` channel.
const cursor = BlogPost.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
// iterate
}
},
);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
import { MongoMemoryServer } from 'mongodb-memory-server-global';
import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// mongoose >= 9.7.0 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes
// to those channels (`subscribeMongooseDiagnosticChannels`) instead of monkey-patching. This suite
// pins `^9.7` and asserts the diagnostics-channel path: stable OTel DB semconv attributes, redacted
// query text, span relationships, and that the legacy IITM patcher does NOT also fire (no double
// instrumentation). mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose tracing channel Test', () => {
let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URL = mongoServer.getUri();
}, 30000);

afterAll(async () => {
if (mongoServer) {
await mongoServer.stop();
}
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
origin: 'auto.db.mongoose.diagnostic_channel',
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});

test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
// The monkey-patch path (origin `auto.db.otel.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.otel.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
},
})
.start()
.completed();
});

test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
}
},
})
.start()
.completed();
});

test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === 'auto.db.otel.mongo',
);
expect(driverChild).toBeDefined();
},
})
.start()
.completed();
});
},
{ additionalDependencies: { mongoose: '^9.7' } },
);
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,10 @@ import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Pins mongoose 9 (top of our supported `>=5.9.7 <10` range) so the latest major is exercised
// against a real mongoose. mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
let mongoServer: MongoMemoryServer;

Expand DownExpand Up@@ -55,6 +57,6 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
},
{ additionalDependencies: { mongoose: '^9' } },
{ additionalDependencies: { mongoose: '>=9 <9.7' } },
);
});
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
import { MongooseInstrumentation } from './vendored/mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { mongooseIntegration as mongooseChannelIntegration } from '@sentry/server-utils';

const INTEGRATION_NAME = 'Mongoose' as const;

export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation());

const _mongooseIntegration = (() => {
return {
// The diagnostics_channel subscription (mongoose >= 9.7) lives in server-utils so it is shared
// across server runtimes; we extend it here to also run the IITM-based patcher for mongoose < 9.7.
return extendIntegration(mongooseChannelIntegration(), {
name: INTEGRATION_NAME,
setupOnce() {
instrumentMongoose();
},
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload skips mongoose channel subscribe

Medium Severity

For mongoose >=9.7, diagnostics-channel subscription runs only in the server-utils integration setupOnce, while preloadOpenTelemetry invokes instrumentMongoose alone. The IITM patcher no longer covers 9.7+, so mongoose work between preload and Sentry.init() (or any preload-only usage) is not traced.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9cbf9df. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is ok.

}) satisfies IntegrationFn;

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,10 @@ export class MongooseInstrumentation extends InstrumentationBase<Instrumentation
protected init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'mongoose',
['>=5.9.7 <10'],
// mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by
// `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not
// overlap it — otherwise every operation would emit two mongoose spans.
['>=5.9.7 <9.7.0'],
this.patch.bind(this),
this.unpatch.bind(this),
);
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
* @module
*/

export { mongooseIntegration } from './mongoose';
export {
IOREDIS_DC_CHANNEL_COMMAND,
IOREDIS_DC_CHANNEL_CONNECT,
Expand Down
30 changes: 30 additions & 0 deletions packages/server-utils/src/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core';
import * as dc from 'node:diagnostics_channel';
import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber';

const _mongooseIntegration = (() => {
return {
name: 'Mongoose',
setupOnce() {
// Bail on Node <= 18.18.0, where `tracingChannel` does not exist.
if (!dc.tracingChannel) {
return;
}

// Subscribe to mongoose's native tracing channels (mongoose >= 9.7).
// This is a no-op on versions that don't publish to the channels, so it is always safe to call.
waitForTracingChannelBinding(() => {
subscribeMongooseDiagnosticChannels(dc.tracingChannel);
});
},
};
}) satisfies IntegrationFn;

/**
* Auto-instrument the [mongoose](https://www.npmjs.com/package/mongoose) library via its native
* `node:diagnostics_channel` tracing channels (mongoose >= 9.7).
*
* On older mongoose versions the channels are never published to, so this integration is inert and
* the IITM-based patcher (gated to `< 9.7.0`) handles instrumentation instead.
*/
export const mongooseIntegration = defineIntegration(_mongooseIntegration);
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/node';
import mongoose from 'mongoose';

async function run() {
await mongoose.connect(process.env.MONGO_URL || '');

const BlogPostSchema = new mongoose.Schema({
title: String,
body: String,
date: Date,
});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
await post.save();

// Filter with a real value, to assert it is redacted out of `db.query.text`.
await BlogPost.findOne({ title: 'Test' });

await BlogPost.aggregate([{ $match: { title: 'Test' } }]);

await BlogPost.insertMany([
{ title: 'Insert1', body: 'b', date: new Date() },
{ title: 'Insert2', body: 'b', date: new Date() },
]);

await BlogPost.bulkWrite([
{ insertOne: { document: { title: 'Bulk1', body: 'b', date: new Date() } } },
{ insertOne: { document: { title: 'Bulk2', body: 'b', date: new Date() } } },
]);

// Drive a cursor to exercise the `mongoose:cursor:next` channel.
const cursor = BlogPost.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
// iterate
}
},
);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
import { MongoMemoryServer } from 'mongodb-memory-server-global';
import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// mongoose >= 9.7.0 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes
// to those channels (`subscribeMongooseDiagnosticChannels`) instead of monkey-patching. This suite
// pins `^9.7` and asserts the diagnostics-channel path: stable OTel DB semconv attributes, redacted
// query text, span relationships, and that the legacy IITM patcher does NOT also fire (no double
// instrumentation). mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose tracing channel Test', () => {
let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URL = mongoServer.getUri();
}, 30000);

afterAll(async () => {
if (mongoServer) {
await mongoServer.stop();
}
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
origin: 'auto.db.mongoose.diagnostic_channel',
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});

test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
// The monkey-patch path (origin `auto.db.otel.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.otel.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
},
})
.start()
.completed();
});

test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
}
},
})
.start()
.completed();
});

test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === 'auto.db.otel.mongo',
);
expect(driverChild).toBeDefined();
},
})
.start()
.completed();
});
},
{ additionalDependencies: { mongoose: '^9.7' } },
);
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,10 @@ import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Pins mongoose 9 (top of our supported `>=5.9.7 <10` range) so the latest major is exercised
// against a real mongoose. mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
let mongoServer: MongoMemoryServer;

Expand DownExpand Up@@ -55,6 +57,6 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
},
{ additionalDependencies: { mongoose: '^9' } },
{ additionalDependencies: { mongoose: '>=9 <9.7' } },
);
});
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
import { MongooseInstrumentation } from './vendored/mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { mongooseIntegration as mongooseChannelIntegration } from '@sentry/server-utils';

const INTEGRATION_NAME = 'Mongoose' as const;

export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation());

const _mongooseIntegration = (() => {
return {
// The diagnostics_channel subscription (mongoose >= 9.7) lives in server-utils so it is shared
// across server runtimes; we extend it here to also run the IITM-based patcher for mongoose < 9.7.
return extendIntegration(mongooseChannelIntegration(), {
name: INTEGRATION_NAME,
setupOnce() {
instrumentMongoose();
},
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload skips mongoose channel subscribe

Medium Severity

For mongoose >=9.7, diagnostics-channel subscription runs only in the server-utils integration setupOnce, while preloadOpenTelemetry invokes instrumentMongoose alone. The IITM patcher no longer covers 9.7+, so mongoose work between preload and Sentry.init() (or any preload-only usage) is not traced.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9cbf9df. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is ok.

}) satisfies IntegrationFn;

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,10 @@ export class MongooseInstrumentation extends InstrumentationBase<Instrumentation
protected init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'mongoose',
['>=5.9.7 <10'],
// mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by
// `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not
// overlap it — otherwise every operation would emit two mongoose spans.
['>=5.9.7 <9.7.0'],
this.patch.bind(this),
this.unpatch.bind(this),
);
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
* @module
*/

export { mongooseIntegration } from './mongoose';
export {
IOREDIS_DC_CHANNEL_COMMAND,
IOREDIS_DC_CHANNEL_CONNECT,
Expand Down
30 changes: 30 additions & 0 deletions packages/server-utils/src/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core';
import * as dc from 'node:diagnostics_channel';
import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber';

const _mongooseIntegration = (() => {
return {
name: 'Mongoose',
setupOnce() {
// Bail on Node <= 18.18.0, where `tracingChannel` does not exist.
if (!dc.tracingChannel) {
return;
}

// Subscribe to mongoose's native tracing channels (mongoose >= 9.7).
// This is a no-op on versions that don't publish to the channels, so it is always safe to call.
waitForTracingChannelBinding(() => {
subscribeMongooseDiagnosticChannels(dc.tracingChannel);
});
},
};
}) satisfies IntegrationFn;

/**
* Auto-instrument the [mongoose](https://www.npmjs.com/package/mongoose) library via its native
* `node:diagnostics_channel` tracing channels (mongoose >= 9.7).
*
* On older mongoose versions the channels are never published to, so this integration is inert and
* the IITM-based patcher (gated to `< 9.7.0`) handles instrumentation instead.
*/
export const mongooseIntegration = defineIntegration(_mongooseIntegration);
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/node';
import mongoose from 'mongoose';

async function run() {
await mongoose.connect(process.env.MONGO_URL || '');

const BlogPostSchema = new mongoose.Schema({
title: String,
body: String,
date: Date,
});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
await post.save();

// Filter with a real value, to assert it is redacted out of `db.query.text`.
await BlogPost.findOne({ title: 'Test' });

await BlogPost.aggregate([{ $match: { title: 'Test' } }]);

await BlogPost.insertMany([
{ title: 'Insert1', body: 'b', date: new Date() },
{ title: 'Insert2', body: 'b', date: new Date() },
]);

await BlogPost.bulkWrite([
{ insertOne: { document: { title: 'Bulk1', body: 'b', date: new Date() } } },
{ insertOne: { document: { title: 'Bulk2', body: 'b', date: new Date() } } },
]);

// Drive a cursor to exercise the `mongoose:cursor:next` channel.
const cursor = BlogPost.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
// iterate
}
},
);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
import { MongoMemoryServer } from 'mongodb-memory-server-global';
import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// mongoose >= 9.7.0 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes
// to those channels (`subscribeMongooseDiagnosticChannels`) instead of monkey-patching. This suite
// pins `^9.7` and asserts the diagnostics-channel path: stable OTel DB semconv attributes, redacted
// query text, span relationships, and that the legacy IITM patcher does NOT also fire (no double
// instrumentation). mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose tracing channel Test', () => {
let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URL = mongoServer.getUri();
}, 30000);

afterAll(async () => {
if (mongoServer) {
await mongoServer.stop();
}
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
origin: 'auto.db.mongoose.diagnostic_channel',
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});

test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
// The monkey-patch path (origin `auto.db.otel.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.otel.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
},
})
.start()
.completed();
});

test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
}
},
})
.start()
.completed();
});

test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === 'auto.db.otel.mongo',
);
expect(driverChild).toBeDefined();
},
})
.start()
.completed();
});
},
{ additionalDependencies: { mongoose: '^9.7' } },
);
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,10 @@ import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Pins mongoose 9 (top of our supported `>=5.9.7 <10` range) so the latest major is exercised
// against a real mongoose. mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
let mongoServer: MongoMemoryServer;

Expand DownExpand Up@@ -55,6 +57,6 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
},
{ additionalDependencies: { mongoose: '^9' } },
{ additionalDependencies: { mongoose: '>=9 <9.7' } },
);
});
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
import { MongooseInstrumentation } from './vendored/mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { mongooseIntegration as mongooseChannelIntegration } from '@sentry/server-utils';

const INTEGRATION_NAME = 'Mongoose' as const;

export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation());

const _mongooseIntegration = (() => {
return {
// The diagnostics_channel subscription (mongoose >= 9.7) lives in server-utils so it is shared
// across server runtimes; we extend it here to also run the IITM-based patcher for mongoose < 9.7.
return extendIntegration(mongooseChannelIntegration(), {
name: INTEGRATION_NAME,
setupOnce() {
instrumentMongoose();
},
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload skips mongoose channel subscribe

Medium Severity

For mongoose >=9.7, diagnostics-channel subscription runs only in the server-utils integration setupOnce, while preloadOpenTelemetry invokes instrumentMongoose alone. The IITM patcher no longer covers 9.7+, so mongoose work between preload and Sentry.init() (or any preload-only usage) is not traced.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9cbf9df. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is ok.

}) satisfies IntegrationFn;

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,10 @@ export class MongooseInstrumentation extends InstrumentationBase<Instrumentation
protected init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'mongoose',
['>=5.9.7 <10'],
// mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by
// `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not
// overlap it — otherwise every operation would emit two mongoose spans.
['>=5.9.7 <9.7.0'],
this.patch.bind(this),
this.unpatch.bind(this),
);
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
* @module
*/

export { mongooseIntegration } from './mongoose';
export {
IOREDIS_DC_CHANNEL_COMMAND,
IOREDIS_DC_CHANNEL_CONNECT,
Expand Down
30 changes: 30 additions & 0 deletions packages/server-utils/src/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core';
import * as dc from 'node:diagnostics_channel';
import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber';

const _mongooseIntegration = (() => {
return {
name: 'Mongoose',
setupOnce() {
// Bail on Node <= 18.18.0, where `tracingChannel` does not exist.
if (!dc.tracingChannel) {
return;
}

// Subscribe to mongoose's native tracing channels (mongoose >= 9.7).
// This is a no-op on versions that don't publish to the channels, so it is always safe to call.
waitForTracingChannelBinding(() => {
subscribeMongooseDiagnosticChannels(dc.tracingChannel);
});
},
};
}) satisfies IntegrationFn;

/**
* Auto-instrument the [mongoose](https://www.npmjs.com/package/mongoose) library via its native
* `node:diagnostics_channel` tracing channels (mongoose >= 9.7).
*
* On older mongoose versions the channels are never published to, so this integration is inert and
* the IITM-based patcher (gated to `< 9.7.0`) handles instrumentation instead.
*/
export const mongooseIntegration = defineIntegration(_mongooseIntegration);
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/node';
import mongoose from 'mongoose';

async function run() {
await mongoose.connect(process.env.MONGO_URL || '');

const BlogPostSchema = new mongoose.Schema({
title: String,
body: String,
date: Date,
});

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
await post.save();

// Filter with a real value, to assert it is redacted out of `db.query.text`.
await BlogPost.findOne({ title: 'Test' });

await BlogPost.aggregate([{ $match: { title: 'Test' } }]);

await BlogPost.insertMany([
{ title: 'Insert1', body: 'b', date: new Date() },
{ title: 'Insert2', body: 'b', date: new Date() },
]);

await BlogPost.bulkWrite([
{ insertOne: { document: { title: 'Bulk1', body: 'b', date: new Date() } } },
{ insertOne: { document: { title: 'Bulk2', body: 'b', date: new Date() } } },
]);

// Drive a cursor to exercise the `mongoose:cursor:next` channel.
const cursor = BlogPost.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
// iterate
}
},
);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
import { MongoMemoryServer } from 'mongodb-memory-server-global';
import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// mongoose >= 9.7.0 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes
// to those channels (`subscribeMongooseDiagnosticChannels`) instead of monkey-patching. This suite
// pins `^9.7` and asserts the diagnostics-channel path: stable OTel DB semconv attributes, redacted
// query text, span relationships, and that the legacy IITM patcher does NOT also fire (no double
// instrumentation). mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose tracing channel Test', () => {
let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URL = mongoServer.getUri();
}, 30000);

afterAll(async () => {
if (mongoServer) {
await mongoServer.stop();
}
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
origin: 'auto.db.mongoose.diagnostic_channel',
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});

test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
// The monkey-patch path (origin `auto.db.otel.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.otel.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
},
})
.start()
.completed();
});

test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
}
},
})
.start()
.completed();
});

test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === 'auto.db.otel.mongo',
);
expect(driverChild).toBeDefined();
},
})
.start()
.completed();
});
},
{ additionalDependencies: { mongoose: '^9.7' } },
);
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,10 @@ import { afterAll, beforeAll, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Pins mongoose 9 (top of our supported `>=5.9.7 <10` range) so the latest major is exercised
// against a real mongoose. mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
let mongoServer: MongoMemoryServer;

Expand DownExpand Up@@ -55,6 +57,6 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
},
{ additionalDependencies: { mongoose: '^9' } },
{ additionalDependencies: { mongoose: '>=9 <9.7' } },
);
});
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
import { MongooseInstrumentation } from './vendored/mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { mongooseIntegration as mongooseChannelIntegration } from '@sentry/server-utils';

const INTEGRATION_NAME = 'Mongoose' as const;

export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation());

const _mongooseIntegration = (() => {
return {
// The diagnostics_channel subscription (mongoose >= 9.7) lives in server-utils so it is shared
// across server runtimes; we extend it here to also run the IITM-based patcher for mongoose < 9.7.
return extendIntegration(mongooseChannelIntegration(), {
name: INTEGRATION_NAME,
setupOnce() {
instrumentMongoose();
},
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload skips mongoose channel subscribe

Medium Severity

For mongoose >=9.7, diagnostics-channel subscription runs only in the server-utils integration setupOnce, while preloadOpenTelemetry invokes instrumentMongoose alone. The IITM patcher no longer covers 9.7+, so mongoose work between preload and Sentry.init() (or any preload-only usage) is not traced.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9cbf9df. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is ok.

}) satisfies IntegrationFn;

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,7 +107,10 @@ export class MongooseInstrumentation extends InstrumentationBase<Instrumentation
protected init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'mongoose',
['>=5.9.7 <10'],
// mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by
// `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not
// overlap it — otherwise every operation would emit two mongoose spans.
['>=5.9.7 <9.7.0'],
this.patch.bind(this),
this.unpatch.bind(this),
);
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
* @module
*/

export { mongooseIntegration } from './mongoose';
export {
IOREDIS_DC_CHANNEL_COMMAND,
IOREDIS_DC_CHANNEL_CONNECT,
Expand Down
30 changes: 30 additions & 0 deletions packages/server-utils/src/mongoose/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core';
import * as dc from 'node:diagnostics_channel';
import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber';

const _mongooseIntegration = (() => {
return {
name: 'Mongoose',
setupOnce() {
// Bail on Node <= 18.18.0, where `tracingChannel` does not exist.
if (!dc.tracingChannel) {
return;
}

// Subscribe to mongoose's native tracing channels (mongoose >= 9.7).
// This is a no-op on versions that don't publish to the channels, so it is always safe to call.
waitForTracingChannelBinding(() => {
subscribeMongooseDiagnosticChannels(dc.tracingChannel);
});
},
};
}) satisfies IntegrationFn;

/**
* Auto-instrument the [mongoose](https://www.npmjs.com/package/mongoose) library via its native
* `node:diagnostics_channel` tracing channels (mongoose >= 9.7).
*
* On older mongoose versions the channels are never published to, so this integration is inert and
* the IITM-based patcher (gated to `< 9.7.0`) handles instrumentation instead.
*/
export const mongooseIntegration = defineIntegration(_mongooseIntegration);
Loading
Loading