Skip to content

Commit 9af2819

Browse files
authored
test(nuxt): Test mysql instrumentation with orchestrion bundler plugin (#21782)
Adds a Nuxt 4 E2E test app that shows how orchestrion (build-time `diagnostics_channel` injection) can be wired into a meta framework, so DB instrumentation works on bundled server code without `--import`. - Rollup plugin instead of Vite. Nuxt's server is built by Nitro (Rollup), so the orchestrion code transform is wired in as a Nitro Rollup plugin, mirroring the existing Vite plugin. - Local module to inject `init` to "server entry". Nitro does not expose an explicit server entry to place the Sentry init in. A small local Nuxt module (mirroring `@sentry/nuxt`'s `addSentryTopImport`) injects a top-level `import './sentry.server.config.mjs'` into Nitro's built entry, so `node .output/server/index.mjs` initializes Sentry first - Both instrumentation paths coexist. orchestrion handles the bundled `mysql` (auto.db.orchestrion.mysql), while OTel keeps instrumenting everything left external (`ioredis`, `http`, …)
1 parent b4831c4 commit 9af2819

18 files changed

Lines changed: 394 additions & 0 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Nuxt dev/build outputs
2+
.output
3+
.data
4+
.nuxt
5+
.nitro
6+
.cache
7+
dist
8+
9+
# Node dependencies
10+
node_modules
11+
12+
# Logs
13+
logs
14+
*.log
15+
16+
# Misc
17+
.DS_Store
18+
.fleet
19+
.idea
20+
21+
# Local env files
22+
.env
23+
.env.*
24+
!.env.example
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<template>
2+
<div>
3+
<NuxtRouteAnnouncer />
4+
<NuxtWelcome />
5+
</div>
6+
</template>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
services:
2+
db:
3+
image: mysql:8.0
4+
restart: always
5+
container_name: e2e-tests-nuxt-4-orchestrion-mysql
6+
# The `mysql` 2.x driver doesn't speak MySQL 8's default
7+
# `caching_sha2_password` auth, so force the legacy plugin.
8+
command: ['--default-authentication-plugin=mysql_native_password']
9+
ports:
10+
- '3306:3306'
11+
environment:
12+
MYSQL_ROOT_PASSWORD: docker
13+
healthcheck:
14+
test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pdocker']
15+
interval: 2s
16+
timeout: 3s
17+
retries: 30
18+
start_period: 10s
19+
20+
redis:
21+
image: redis:7
22+
restart: always
23+
container_name: e2e-tests-nuxt-4-orchestrion-redis
24+
ports:
25+
- '6379:6379'
26+
healthcheck:
27+
test: ['CMD', 'redis-cli', 'ping']
28+
interval: 2s
29+
timeout: 3s
30+
retries: 30
31+
start_period: 5s
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { execSync } from 'child_process';
2+
import { dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
export default async function globalSetup() {
8+
// Start MySQL via Docker Compose. `--wait` blocks until the healthcheck
9+
// in docker-compose.yml passes, so the app can connect immediately.
10+
execSync('docker compose up -d --wait', {
11+
cwd: __dirname,
12+
stdio: 'inherit',
13+
});
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { execSync } from 'child_process';
2+
import { dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
export default async function globalTeardown() {
8+
execSync('docker compose down --volumes', {
9+
cwd: __dirname,
10+
stdio: 'inherit',
11+
});
12+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2+
import { createResolver, defineNuxtModule } from '@nuxt/kit';
3+
4+
const SERVER_CONFIG_FILENAME = 'sentry.server.config';
5+
6+
/**
7+
* Local demo module — NOT part of `@sentry/nuxt`.
8+
*
9+
* The orchestrion bundler approach needs the Sentry init to run inside the
10+
* server build (so the diagnostics-channel subscribers are registered before
11+
* the first request) WITHOUT relying on `node --import`.
12+
*/
13+
export default defineNuxtModule({
14+
meta: { name: 'sentry-server-init' },
15+
setup(_options, nuxt) {
16+
nuxt.hooks.hook('nitro:init', nitro => {
17+
nitro.hooks.hook('close', () => {
18+
const entryFilePath = createResolver(nitro.options.output.serverDir).resolve('index.mjs');
19+
20+
if (!existsSync(entryFilePath)) {
21+
return;
22+
}
23+
24+
const topImport = `import './${SERVER_CONFIG_FILENAME}.mjs';\n`;
25+
const data = readFileSync(entryFilePath, 'utf8');
26+
27+
if (data.startsWith(topImport)) {
28+
return;
29+
}
30+
31+
writeFileSync(entryFilePath, topImport + data, 'utf8');
32+
});
33+
});
34+
},
35+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import codeTransformerRollup from '@apm-js-collab/code-transformer-bundler-plugins/rollup';
2+
import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config';
3+
4+
// Mirrors the marker that `sentryOrchestrionPlugin()` (the Vite plugin) prepends to entry chunks.
5+
const orchestrionBundlerMarker = {
6+
name: 'sentry-orchestrion-marker',
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
renderChunk(code: string, chunk: any): { code: string; map: null } | null {
9+
if (!chunk.isEntry) {
10+
return null;
11+
}
12+
const banner =
13+
'globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{});globalThis.__SENTRY_ORCHESTRION__.bundler=true;\n';
14+
return { code: banner + code, map: null };
15+
},
16+
};
17+
18+
// https://nuxt.com/docs/api/configuration/nuxt-config
19+
export default defineNuxtConfig({
20+
compatibilityDate: '2025-07-15',
21+
devtools: { enabled: true },
22+
23+
modules: ['@sentry/nuxt/module', './modules/sentry-server-init'],
24+
25+
runtimeConfig: {
26+
public: {
27+
sentry: {
28+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
29+
},
30+
},
31+
},
32+
33+
nitro: {
34+
// Nuxt's server is built by Nitro (Rollup), not Vite — so the orchestrion
35+
// code transform has to run as a Nitro Rollup plugin to reach `server/api/*`
36+
// routes. Force-bundle ONLY the instrumented deps (`mysql`) via
37+
// `externals.inline`; externalized deps are `require()`d from `node_modules`
38+
// at runtime and never pass through the transform.
39+
externals: {
40+
inline: INSTRUMENTED_MODULE_NAMES,
41+
},
42+
rollupConfig: {
43+
plugins: [
44+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45+
codeTransformerRollup({ instrumentations: SENTRY_INSTRUMENTATIONS }) as any,
46+
orchestrionBundlerMarker,
47+
],
48+
},
49+
},
50+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "nuxt-4-orchestrion",
3+
"type": "module",
4+
"private": true,
5+
"scripts": {
6+
"build": "nuxt build",
7+
"dev": "nuxt dev",
8+
"generate": "nuxt generate",
9+
"preview": "nuxt preview",
10+
"start": "node .output/server/index.mjs",
11+
"start:import": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs",
12+
"clean": "npx nuxi cleanup",
13+
"test": "playwright test",
14+
"test:prod": "TEST_ENV=production playwright test",
15+
"test:build": "pnpm install && pnpm build",
16+
"test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitropack@npm:nitropack-nightly@latest && pnpm install --force && pnpm build",
17+
"test:assert": "pnpm test:prod"
18+
},
19+
"//": "Need to use ioredis 5.10.1 because that's the last version before they support tracing channels",
20+
"dependencies": {
21+
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
22+
"@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz",
23+
"ioredis": "5.10.1",
24+
"mysql": "^2.18.1",
25+
"nuxt": "^4.4.8",
26+
"vue": "^3.5.38",
27+
"vue-router": "^5.1.0"
28+
},
29+
"devDependencies": {
30+
"@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0",
31+
"@playwright/test": "~1.56.0",
32+
"@sentry-internal/test-utils": "link:../../../test-utils"
33+
},
34+
"volta": {
35+
"extends": "../../package.json",
36+
"node": "22.20.0"
37+
}
38+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: 'pnpm start',
5+
});
6+
7+
export default {
8+
...config,
9+
globalSetup: './global-setup.mjs',
10+
globalTeardown: './global-teardown.mjs',
11+
};
Binary file not shown.

0 commit comments

Comments
 (0)