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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MongoClient } from 'mongodb';
import { NextResponse } from 'next/server';

export const dynamic = 'force-dynamic';

export async function GET() {
const client = new MongoClient('mongodb://localhost:27017');

try {
await client.connect();
const collection = client.db('admin').collection('movies');

await collection.insertOne({ title: 'Rear Window' });
await collection.findOne({ title: 'Rear Window' });

return NextResponse.json({ status: 'ok' });
} finally {
await client.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import mongoose, { Schema } from 'mongoose';
import { NextResponse } from 'next/server';

export const dynamic = 'force-dynamic';

export async function GET() {
// The `test` database matches the mongoose scenario in node-integration-tests.
await mongoose.connect('mongodb://localhost:27017/test');

// Guard against model recompilation across requests in the same worker.
const BlogPost = mongoose.models.BlogPost || mongoose.model('BlogPost', new Schema({ title: String }));

try {
const post = new BlogPost({ title: 'Rear Window' });
await post.save();
await BlogPost.findOne({ title: 'Rear Window' });

return NextResponse.json({ status: 'ok' });
} finally {
await mongoose.disconnect();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import mysql from 'mysql2/promise';
import { NextResponse } from 'next/server';

export const dynamic = 'force-dynamic';

// `mysql2` reuses the same MySQL container as the legacy `mysql` driver — it supports both auth plugins.
export async function GET() {
const connection = await mysql.createConnection({
host: 'localhost',
port: 3306,
user: 'root',
password: 'docker',
});

try {
await connection.query('SELECT 1 + 1 AS solution');
await connection.execute('SELECT 42 AS answer');
return NextResponse.json({ status: 'ok' });
} finally {
await connection.end();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextResponse } from 'next/server';
import postgres from 'postgres';

export const dynamic = 'force-dynamic';

// postgres.js reuses the same Postgres container as the `pg` driver.
export async function GET() {
const sql = postgres({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'docker',
database: 'postgres',
});

try {
await sql`SELECT 1 + 1 AS solution`;
await sql`SELECT * from generate_series(1, 3) as x`;
return NextResponse.json({ status: 'ok' });
} finally {
await sql.end();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ services:
restart: always
container_name: e2e-tests-nextjs-16-orchestrion-mysql
# The `mysql` 2.x driver doesn't speak MySQL 8's default
# `caching_sha2_password` auth, so force the legacy plugin.
# `caching_sha2_password` auth, so force the legacy plugin. `mysql2` supports
# both, so it connects to this same container unchanged.
command: ['--default-authentication-plugin=mysql_native_password']
ports:
- '3306:3306'
Expand All @@ -44,3 +45,16 @@ services:
timeout: 3s
retries: 30
start_period: 10s

mongo:
image: mongo:7
restart: always
container_name: e2e-tests-nextjs-16-orchestrion-mongo
ports:
- '27017:27017'
healthcheck:
test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"]
interval: 2s
timeout: 3s
retries: 30
start_period: 10s
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"test:build-webpack": "pnpm install && pnpm build-webpack",
"test:assert": "pnpm test:prod"
},
"//": "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`.",
"//": "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`.",
"dependencies": {
"@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
Expand All @@ -23,9 +23,13 @@
"ioredis": "5.10.1",
"knex": "^2.5.1",
"lru-memoizer": "2.3.0",
"mongodb": "^6.4.0",
"mongoose": "^7.8.11",
"mysql": "^2.18.1",
"mysql2": "3.19.1",
"next": "16.2.10",
"pg": "^8.13.1",
"postgres": "^3.4.7",
"react": "19.1.0",
"react-dom": "19.1.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('Instruments dataloader automatically via orchestrion', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('nextjs-16-orchestrion', transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /api/dataloader'
);
});

await fetch(`${baseURL}/api/dataloader`);

const transactionEvent = await transactionEventPromise;

const spans = transactionEvent.spans || [];

const loadSpan = spans.find(span => span.description === 'dataloader.load usersLoader');
expect(loadSpan).toBeDefined();
expect(loadSpan?.op).toBe('cache.get');
expect(loadSpan?.origin).toBe('auto.db.orchestrion.dataloader');
expect(loadSpan?.status).toBe('ok');
expect(loadSpan?.data?.['cache.key']).toEqual(['user-1']);

// The batch span opens on the deferred dispatch tick and links back to the load span.
const batchSpan = spans.find(span => span.description === 'dataloader.batch usersLoader');
expect(batchSpan).toBeDefined();
expect(batchSpan?.op).toBe('cache.get');
expect(batchSpan?.origin).toBe('auto.db.orchestrion.dataloader');
expect(batchSpan?.status).toBe('ok');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('Instruments DB calls made during server-side rendering of a page', async ({ page }) => {
const transactionEventPromise = waitForTransaction('nextjs-16-orchestrion', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /db-page';
});

await page.goto('/db-page');
await expect(page.locator('#answer')).toHaveText('answer: 42');
await expect(page.locator('#cached')).toHaveText('cached: 42');

const transactionEvent = await transactionEventPromise;

const spans = transactionEvent.spans || [];

// One page render produces spans from both injection paths: pg (externalized → runtime module
// hook) and ioredis (bundle-safe allowlisted → build-time loader).
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.postgres',
description: 'SELECT 40 + 2 AS answer',
status: 'ok',
data: expect.objectContaining({
'db.system': 'postgresql',
'db.statement': 'SELECT 40 + 2 AS answer',
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.redis',
description: 'set page-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system': 'redis',
'db.statement': 'set page-key [1 other arguments]',
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.redis',
description: 'get page-key',
status: 'ok',
}),
);
});
Loading
Loading