Skip to content

Commit e36268b

Browse files
authored
feat(mongoose): implement orchestrion mongoose integration (#22202)
Port the OTel mongoose intstrumentation to Orchestrion. Add Deno integration, and node integration tests for mongoose versions 5, 6, 7, 8, and 9. Native diagnostics channel used on Mongoose versions supporting them (ie, 9.7+). Fix: JS-2412 Fix: #20761
1 parent 2014e5d commit e36268b

21 files changed

Lines changed: 614 additions & 96 deletions

File tree

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ module.exports = [
400400
import: createImport('init', 'experimentalUseDiagnosticsChannelInjection'),
401401
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
402402
gzip: true,
403-
limit: '148 KB',
403+
limit: '150 KB',
404404
disablePlugins: ['@size-limit/esbuild'],
405405
},
406406
{
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as Sentry from '@sentry/node';
2+
import mongoose from 'mongoose';
3+
4+
async function run() {
5+
await mongoose.connect(process.env.MONGO_URL || '');
6+
7+
const BlogPostSchema = new mongoose.Schema({
8+
title: String,
9+
body: String,
10+
date: Date,
11+
});
12+
13+
const BlogPost = mongoose.model('BlogPost', BlogPostSchema);
14+
15+
await Sentry.startSpan(
16+
{
17+
name: 'Test Transaction',
18+
op: 'transaction',
19+
},
20+
async () => {
21+
const post = new BlogPost({ title: 'Test', body: 'Test body', date: new Date() });
22+
23+
await post.save();
24+
25+
await BlogPost.findOne({});
26+
27+
await BlogPost.aggregate([{ $match: {} }]);
28+
29+
await BlogPost.insertMany([{ title: 'Insert', body: 'Insert body', date: new Date() }]);
30+
31+
await BlogPost.bulkWrite([{ insertOne: { document: { title: 'Bulk', body: 'Bulk body', date: new Date() } } }]);
32+
},
33+
);
34+
}
35+
36+
run();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { MongoMemoryServer } from 'mongodb-memory-server-global';
2+
import { afterAll, beforeAll, describe, expect } from 'vitest';
3+
import { isOrchestrionEnabled } from '../../../utils';
4+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
5+
6+
// Pins mongoose 5.9.7
7+
// the bottom of the IITM patcher's `>=5.9.7 <9.7.0` range, so the oldest
8+
// supported major is exercised against a real mongoose.
9+
describe('Mongoose v5 Test', () => {
10+
const origin = isOrchestrionEnabled() ? 'auto.db.orchestrion.mongoose' : 'auto.db.otel.mongoose';
11+
let mongoServer: MongoMemoryServer;
12+
13+
beforeAll(async () => {
14+
mongoServer = await MongoMemoryServer.create();
15+
process.env.MONGO_URL = mongoServer.getUri();
16+
}, 30000);
17+
18+
afterAll(async () => {
19+
if (mongoServer) {
20+
await mongoServer.stop();
21+
}
22+
cleanupChildProcesses();
23+
});
24+
25+
const expectedSpan = (operation: string) =>
26+
expect.objectContaining({
27+
data: expect.objectContaining({
28+
'db.mongodb.collection': 'blogposts',
29+
'db.operation': operation,
30+
'db.system': 'mongoose',
31+
}),
32+
description: `mongoose.BlogPost.${operation}`,
33+
op: 'db',
34+
origin,
35+
});
36+
37+
const EXPECTED_TRANSACTION = {
38+
transaction: 'Test Transaction',
39+
spans: expect.arrayContaining([
40+
expectedSpan('save'),
41+
expectedSpan('findOne'),
42+
expectedSpan('aggregate'),
43+
expectedSpan('insertMany'),
44+
expectedSpan('bulkWrite'),
45+
]),
46+
};
47+
48+
createEsmAndCjsTests(
49+
__dirname,
50+
'scenario.mjs',
51+
'instrument.mjs',
52+
(createTestRunner, test) => {
53+
test('auto-instruments `mongoose` v5.', async () => {
54+
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
55+
});
56+
},
57+
{ additionalDependencies: { mongoose: '^5.9.7' } },
58+
);
59+
});

dev-packages/node-integration-tests/suites/tracing/mongoose-v7/test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { MongoMemoryServer } from 'mongodb-memory-server-global';
22
import { afterAll, beforeAll, describe, expect } from 'vitest';
3+
import { isOrchestrionEnabled } from '../../../utils';
34
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
45

56
// Pins mongoose 7 so the `contextCaptureFunctions7` version branch is exercised against a real mongoose.
67
describe('Mongoose v7 Test', () => {
8+
const origin = isOrchestrionEnabled() ? 'auto.db.orchestrion.mongoose' : 'auto.db.otel.mongoose';
79
let mongoServer: MongoMemoryServer;
810

911
beforeAll(async () => {
@@ -27,7 +29,7 @@ describe('Mongoose v7 Test', () => {
2729
}),
2830
description: `mongoose.BlogPost.${operation}`,
2931
op: 'db',
30-
origin: 'auto.db.otel.mongoose',
32+
origin,
3133
});
3234

3335
const EXPECTED_TRANSACTION = {

dev-packages/node-integration-tests/suites/tracing/mongoose-v8/test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { MongoMemoryServer } from 'mongodb-memory-server-global';
22
import { afterAll, beforeAll, describe, expect } from 'vitest';
3+
import { isOrchestrionEnabled } from '../../../utils';
34
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
45

56
// Pins mongoose 8 (>= 8.21) so the document `updateOne`/`deleteOne` lazy-Query path is exercised
67
// against a real mongoose, guarding the thenable trap that mongoose 6 (the workspace version) can't hit.
78
describe('Mongoose v8 Test', () => {
9+
const origin = isOrchestrionEnabled() ? 'auto.db.orchestrion.mongoose' : 'auto.db.otel.mongoose';
810
let mongoServer: MongoMemoryServer;
911

1012
beforeAll(async () => {
@@ -30,7 +32,7 @@ describe('Mongoose v8 Test', () => {
3032
}),
3133
description: 'mongoose.BlogPost.save',
3234
op: 'db',
33-
origin: 'auto.db.otel.mongoose',
35+
origin,
3436
}),
3537
expect.objectContaining({
3638
data: expect.objectContaining({
@@ -40,7 +42,7 @@ describe('Mongoose v8 Test', () => {
4042
}),
4143
description: 'mongoose.BlogPost.updateOne',
4244
op: 'db',
43-
origin: 'auto.db.otel.mongoose',
45+
origin,
4446
}),
4547
expect.objectContaining({
4648
data: expect.objectContaining({
@@ -50,7 +52,7 @@ describe('Mongoose v8 Test', () => {
5052
}),
5153
description: 'mongoose.BlogPost.deleteOne',
5254
op: 'db',
53-
origin: 'auto.db.otel.mongoose',
55+
origin,
5456
}),
5557
]),
5658
};

dev-packages/node-integration-tests/suites/tracing/mongoose-v9/test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { MongoMemoryServer } from 'mongodb-memory-server-global';
22
import { afterAll, beforeAll, expect } from 'vitest';
3-
import { conditionalTest } from '../../../utils';
3+
import { conditionalTest, isOrchestrionEnabled } from '../../../utils';
44
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
55

66
// Pins the highest mongoose 9 below 9.7, the top of the IITM patcher's `>=5.9.7 <9.7.0` range, so the
77
// monkey-patch path is exercised against a real mongoose 9. mongoose >= 9.7 publishes via
88
// diagnostics_channel and is covered by the `mongoose-tracing-channel` suite instead.
99
// mongoose 9 requires Node >=20.19, so this suite is skipped on older Node.
1010
conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
11+
const origin = isOrchestrionEnabled() ? 'auto.db.orchestrion.mongoose' : 'auto.db.otel.mongoose';
1112
let mongoServer: MongoMemoryServer;
1213

1314
beforeAll(async () => {
@@ -31,7 +32,7 @@ conditionalTest({ min: 20 })('Mongoose v9 Test', () => {
3132
}),
3233
description: `mongoose.BlogPost.${operation}`,
3334
op: 'db',
34-
origin: 'auto.db.otel.mongoose',
35+
origin,
3536
});
3637

3738
const EXPECTED_TRANSACTION = {

dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { MongoMemoryServer } from 'mongodb-memory-server-global';
22
import { afterAll, beforeAll, describe, expect } from 'vitest';
3+
import { isOrchestrionEnabled } from '../../../utils';
34
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
45

56
describe('Mongoose experimental Test', () => {
7+
const origin = isOrchestrionEnabled() ? 'auto.db.orchestrion.mongoose' : 'auto.db.otel.mongoose';
68
let mongoServer: MongoMemoryServer;
79

810
beforeAll(async () => {
@@ -29,7 +31,7 @@ describe('Mongoose experimental Test', () => {
2931
}),
3032
description: 'mongoose.BlogPost.save',
3133
op: 'db',
32-
origin: 'auto.db.otel.mongoose',
34+
origin,
3335
}),
3436
expect.objectContaining({
3537
data: expect.objectContaining({
@@ -40,7 +42,7 @@ describe('Mongoose experimental Test', () => {
4042
}),
4143
description: 'mongoose.BlogPost.findOne',
4244
op: 'db',
43-
origin: 'auto.db.otel.mongoose',
45+
origin,
4446
}),
4547
expect.objectContaining({
4648
data: expect.objectContaining({
@@ -51,7 +53,7 @@ describe('Mongoose experimental Test', () => {
5153
}),
5254
description: 'mongoose.BlogPost.aggregate',
5355
op: 'db',
54-
origin: 'auto.db.otel.mongoose',
56+
origin,
5557
}),
5658
expect.objectContaining({
5759
data: expect.objectContaining({
@@ -62,7 +64,7 @@ describe('Mongoose experimental Test', () => {
6264
}),
6365
description: 'mongoose.BlogPost.insertMany',
6466
op: 'db',
65-
origin: 'auto.db.otel.mongoose',
67+
origin,
6668
}),
6769
expect.objectContaining({
6870
data: expect.objectContaining({
@@ -73,7 +75,7 @@ describe('Mongoose experimental Test', () => {
7375
}),
7476
description: 'mongoose.BlogPost.bulkWrite',
7577
op: 'db',
76-
origin: 'auto.db.otel.mongoose',
78+
origin,
7779
}),
7880
// `remove` is patched only on mongoose 5/6.
7981
expect.objectContaining({
@@ -85,7 +87,7 @@ describe('Mongoose experimental Test', () => {
8587
}),
8688
description: 'mongoose.BlogPost.remove',
8789
op: 'db',
88-
origin: 'auto.db.otel.mongoose',
90+
origin,
8991
}),
9092
// A failing operation still produces a span, marked with an error status.
9193
expect.objectContaining({
@@ -95,7 +97,7 @@ describe('Mongoose experimental Test', () => {
9597
}),
9698
description: 'mongoose.RequiredDoc.save',
9799
op: 'db',
98-
origin: 'auto.db.otel.mongoose',
100+
origin,
99101
status: 'internal_error',
100102
}),
101103
]),

packages/deno/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export { denoAmqplibIntegration } from './integrations/amqplib';
117117
export { denoDataloaderIntegration } from './integrations/dataloader';
118118
export { denoKnexIntegration } from './integrations/knex';
119119
export { denoKoaIntegration } from './integrations/koa';
120+
export { denoMongooseIntegration } from './integrations/mongoose';
120121
export { denoContextIntegration } from './integrations/context';
121122
export { globalHandlersIntegration } from './integrations/globalhandlers';
122123
export { normalizePathsIntegration } from './integrations/normalizepaths';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { mongooseChannelIntegration } from '@sentry/server-utils/orchestrion';
2+
import type { Integration, IntegrationFn } from '@sentry/core';
3+
import { defineIntegration, extendIntegration } from '@sentry/core';
4+
import { setAsyncLocalStorageAsyncContextStrategy } from '../async';
5+
6+
const INTEGRATION_NAME = 'DenoMongoose' as const;
7+
8+
/**
9+
* Create spans for `mongoose` queries under Deno.
10+
*
11+
* `mongoose` channels are injected by the orchestrion runtime hook at load
12+
* time. The `@sentry/deno/import` loader must be active for this integration
13+
* to record anything.
14+
*
15+
* The channel-subscription logic is shared with the other server runtimes in
16+
* `@sentry/server-utils`. This just installs Deno's `AsyncLocalStorage`
17+
* context strategy (so spans nest under the active span and survive mongoose's
18+
* internal callback dispatch) before delegating.
19+
*/
20+
const _denoMongooseIntegration = (() => {
21+
const inner = mongooseChannelIntegration();
22+
23+
return extendIntegration(inner, {
24+
name: INTEGRATION_NAME,
25+
setupOnce() {
26+
setAsyncLocalStorageAsyncContextStrategy();
27+
},
28+
});
29+
}) satisfies IntegrationFn;
30+
31+
export const denoMongooseIntegration = defineIntegration(_denoMongooseIntegration) as () => Integration & {
32+
name: 'DenoMongoose';
33+
setupOnce: () => void;
34+
};

0 commit comments

Comments
 (0)