Skip to content

Commit b193268

Browse files
committed
feat(mongoose): implement orchestrion mongoose integration
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 abacdbf commit b193268

20 files changed

Lines changed: 613 additions & 95 deletions

File tree

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+
};

packages/deno/src/sdk.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { denoServeIntegration } from './integrations/deno-serve';
2525
import { denoHttpIntegration } from './integrations/http';
2626
import { denoAmqplibIntegration } from './integrations/amqplib';
2727
import { denoKoaIntegration } from './integrations/koa';
28+
import { denoMongooseIntegration } from './integrations/mongoose';
2829
import { denoMysqlIntegration } from './integrations/mysql';
2930
import { denoPostgresIntegration } from './integrations/postgres';
3031
import { denoRedisIntegration } from './integrations/redis';
@@ -63,7 +64,13 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
6364
// (or in parallel to) loading the SDK, so we only gate on whether the
6465
// feature is possible. If they're never loaded, it'll just be a no-op.
6566
...(MODULE_REGISTER_HOOKS_SUPPORTED
66-
? [denoMysqlIntegration(), denoPostgresIntegration(), denoAmqplibIntegration(), denoKoaIntegration()]
67+
? [
68+
denoAmqplibIntegration(),
69+
denoKoaIntegration(),
70+
denoMongooseIntegration(),
71+
denoMysqlIntegration(),
72+
denoPostgresIntegration(),
73+
]
6774
: []),
6875
contextLinesIntegration(),
6976
normalizePathsIntegration(),

0 commit comments

Comments
 (0)