Skip to content

Commit d177054

Browse files
chargomeclaude
andauthored
test(nextjs): Add MongoDB and SQL driver orchestrion instrumentations (#22530)
Adds e2e coverage for `mongodb`, `mongoose`, `mysql2` and `postgres.js` orchestrion instrumentations in the `nextjs-16-orchestrion` app. `mongodb`/`mongoose` use a new mongo container; `mysql2` and `postgres.js` reuse the existing MySQL and Postgres containers. `mysql2` is pinned below 3.20.0 (like `ioredis`) so the orchestrion path is exercised rather than the driver's native diagnostics channels. Also splits the app's e2e tests into one file per instrumented library for readability. Closes #22505 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 97d3d71 commit d177054

20 files changed

Lines changed: 617 additions & 320 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { MongoClient } from 'mongodb';
2+
import { NextResponse } from 'next/server';
3+
4+
export const dynamic = 'force-dynamic';
5+
6+
export async function GET() {
7+
const client = new MongoClient('mongodb://localhost:27017');
8+
9+
try {
10+
await client.connect();
11+
const collection = client.db('admin').collection('movies');
12+
13+
await collection.insertOne({ title: 'Rear Window' });
14+
await collection.findOne({ title: 'Rear Window' });
15+
16+
return NextResponse.json({ status: 'ok' });
17+
} finally {
18+
await client.close();
19+
}
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import mongoose, { Schema } from 'mongoose';
2+
import { NextResponse } from 'next/server';
3+
4+
export const dynamic = 'force-dynamic';
5+
6+
export async function GET() {
7+
// The `test` database matches the mongoose scenario in node-integration-tests.
8+
await mongoose.connect('mongodb://localhost:27017/test');
9+
10+
// Guard against model recompilation across requests in the same worker.
11+
const BlogPost = mongoose.models.BlogPost || mongoose.model('BlogPost', new Schema({ title: String }));
12+
13+
try {
14+
const post = new BlogPost({ title: 'Rear Window' });
15+
await post.save();
16+
await BlogPost.findOne({ title: 'Rear Window' });
17+
18+
return NextResponse.json({ status: 'ok' });
19+
} finally {
20+
await mongoose.disconnect();
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import mysql from 'mysql2/promise';
2+
import { NextResponse } from 'next/server';
3+
4+
export const dynamic = 'force-dynamic';
5+
6+
// `mysql2` reuses the same MySQL container as the legacy `mysql` driver — it supports both auth plugins.
7+
export async function GET() {
8+
const connection = await mysql.createConnection({
9+
host: 'localhost',
10+
port: 3306,
11+
user: 'root',
12+
password: 'docker',
13+
});
14+
15+
try {
16+
await connection.query('SELECT 1 + 1 AS solution');
17+
await connection.execute('SELECT 42 AS answer');
18+
return NextResponse.json({ status: 'ok' });
19+
} finally {
20+
await connection.end();
21+
}
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { NextResponse } from 'next/server';
2+
import postgres from 'postgres';
3+
4+
export const dynamic = 'force-dynamic';
5+
6+
// postgres.js reuses the same Postgres container as the `pg` driver.
7+
export async function GET() {
8+
const sql = postgres({
9+
host: 'localhost',
10+
port: 5432,
11+
user: 'postgres',
12+
password: 'docker',
13+
database: 'postgres',
14+
});
15+
16+
try {
17+
await sql`SELECT 1 + 1 AS solution`;
18+
await sql`SELECT * from generate_series(1, 3) as x`;
19+
return NextResponse.json({ status: 'ok' });
20+
} finally {
21+
await sql.end();
22+
}
23+
}

dev-packages/e2e-tests/test-applications/nextjs-16-orchestrion/docker-compose.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ services:
3232
restart: always
3333
container_name: e2e-tests-nextjs-16-orchestrion-mysql
3434
# The `mysql` 2.x driver doesn't speak MySQL 8's default
35-
# `caching_sha2_password` auth, so force the legacy plugin.
35+
# `caching_sha2_password` auth, so force the legacy plugin. `mysql2` supports
36+
# both, so it connects to this same container unchanged.
3637
command: ['--default-authentication-plugin=mysql_native_password']
3738
ports:
3839
- '3306:3306'
@@ -44,3 +45,16 @@ services:
4445
timeout: 3s
4546
retries: 30
4647
start_period: 10s
48+
49+
mongo:
50+
image: mongo:7
51+
restart: always
52+
container_name: e2e-tests-nextjs-16-orchestrion-mongo
53+
ports:
54+
- '27017:27017'
55+
healthcheck:
56+
test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"]
57+
interval: 2s
58+
timeout: 3s
59+
retries: 30
60+
start_period: 10s

dev-packages/e2e-tests/test-applications/nextjs-16-orchestrion/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"test:build-webpack": "pnpm install && pnpm build-webpack",
1515
"test:assert": "pnpm test:prod"
1616
},
17-
"//": "Pin `ioredis` to 5.10.1 because that's the last version before it publishes its own `ioredis:*` diagnostics channels; orchestrion's ioredis config covers `<5.11.0`.",
17+
"//": "Pin `ioredis` to 5.10.1 and `mysql2` to 3.19.1: both are the last versions before the driver publishes its own native diagnostics channels; orchestrion's configs cover `ioredis <5.11.0` and `mysql2 <3.20.0`.",
1818
"dependencies": {
1919
"@sentry/core": "file:../../packed/sentry-core-packed.tgz",
2020
"@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
@@ -23,9 +23,13 @@
2323
"ioredis": "5.10.1",
2424
"knex": "^2.5.1",
2525
"lru-memoizer": "2.3.0",
26+
"mongodb": "^6.4.0",
27+
"mongoose": "^7.8.11",
2628
"mysql": "^2.18.1",
29+
"mysql2": "3.19.1",
2730
"next": "16.2.10",
2831
"pg": "^8.13.1",
32+
"postgres": "^3.4.7",
2933
"react": "19.1.0",
3034
"react-dom": "19.1.0"
3135
},
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('Instruments dataloader automatically via orchestrion', async ({ baseURL }) => {
5+
const transactionEventPromise = waitForTransaction('nextjs-16-orchestrion', transactionEvent => {
6+
return (
7+
transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /api/dataloader'
8+
);
9+
});
10+
11+
await fetch(`${baseURL}/api/dataloader`);
12+
13+
const transactionEvent = await transactionEventPromise;
14+
15+
const spans = transactionEvent.spans || [];
16+
17+
const loadSpan = spans.find(span => span.description === 'dataloader.load usersLoader');
18+
expect(loadSpan).toBeDefined();
19+
expect(loadSpan?.op).toBe('cache.get');
20+
expect(loadSpan?.origin).toBe('auto.db.orchestrion.dataloader');
21+
expect(loadSpan?.status).toBe('ok');
22+
expect(loadSpan?.data?.['cache.key']).toEqual(['user-1']);
23+
24+
// The batch span opens on the deferred dispatch tick and links back to the load span.
25+
const batchSpan = spans.find(span => span.description === 'dataloader.batch usersLoader');
26+
expect(batchSpan).toBeDefined();
27+
expect(batchSpan?.op).toBe('cache.get');
28+
expect(batchSpan?.origin).toBe('auto.db.orchestrion.dataloader');
29+
expect(batchSpan?.status).toBe('ok');
30+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('Instruments DB calls made during server-side rendering of a page', async ({ page }) => {
5+
const transactionEventPromise = waitForTransaction('nextjs-16-orchestrion', transactionEvent => {
6+
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /db-page';
7+
});
8+
9+
await page.goto('/db-page');
10+
await expect(page.locator('#answer')).toHaveText('answer: 42');
11+
await expect(page.locator('#cached')).toHaveText('cached: 42');
12+
13+
const transactionEvent = await transactionEventPromise;
14+
15+
const spans = transactionEvent.spans || [];
16+
17+
// One page render produces spans from both injection paths: pg (externalized → runtime module
18+
// hook) and ioredis (bundle-safe allowlisted → build-time loader).
19+
expect(spans).toContainEqual(
20+
expect.objectContaining({
21+
op: 'db',
22+
origin: 'auto.db.orchestrion.postgres',
23+
description: 'SELECT 40 + 2 AS answer',
24+
status: 'ok',
25+
data: expect.objectContaining({
26+
'db.system': 'postgresql',
27+
'db.statement': 'SELECT 40 + 2 AS answer',
28+
}),
29+
}),
30+
);
31+
expect(spans).toContainEqual(
32+
expect.objectContaining({
33+
op: 'db',
34+
origin: 'auto.db.orchestrion.redis',
35+
description: 'set page-key [1 other arguments]',
36+
status: 'ok',
37+
data: expect.objectContaining({
38+
'db.system': 'redis',
39+
'db.statement': 'set page-key [1 other arguments]',
40+
}),
41+
}),
42+
);
43+
expect(spans).toContainEqual(
44+
expect.objectContaining({
45+
op: 'db',
46+
origin: 'auto.db.orchestrion.redis',
47+
description: 'get page-key',
48+
status: 'ok',
49+
}),
50+
);
51+
});

0 commit comments

Comments
 (0)