Skip to content

Commit c133bb8

Browse files
committed
feat(nuxt)!: Bundle server config into Nitro build
1 parent 89729c3 commit c133bb8

12 files changed

Lines changed: 432 additions & 271 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Simulates a v10-style `node --import` preload that fully initializes the SDK
2+
// before the config bundled into the server build runs its own `Sentry.init`.
3+
import * as Sentry from '@sentry/nuxt';
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
tracesSampleRate: 1.0,
8+
tunnel: 'http://localhost:3031/',
9+
});

‎dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash‎

Lines changed: 0 additions & 55 deletions
This file was deleted.

‎dev-packages/e2e-tests/test-applications/nuxt-4/package.json‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
"clean": "npx nuxi cleanup",
1313
"test": "playwright test",
1414
"test:prod": "TEST_ENV=production playwright test",
15-
"test:dev": "bash ./nuxt-start-dev-server.bash && TEST_ENV=development playwright test environment",
15+
"test:dev": "TEST_ENV=development playwright test environment",
1616
"test:build": "pnpm install && pnpm build",
1717
"test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitropack@npm:nitropack-nightly@latest && pnpm install --force && pnpm build",
18-
"test:assert": "pnpm test:prod && pnpm test:dev"
18+
"test:assert": "pnpm test:prod && pnpm test:dev",
19+
"test:prod:import": "TEST_ENV=production-import playwright test"
1920
},
2021
"dependencies": {
2122
"@pinia/nuxt": "^0.5.5",
@@ -38,6 +39,11 @@
3839
"build-command": "E2E_TEST_OTEL_SETUP=true pnpm test:build",
3940
"assert-command": "E2E_TEST_OTEL_SETUP=true pnpm test:assert",
4041
"label": "nuxt-4 (tracer provider)"
42+
},
43+
{
44+
"build-command": "pnpm test:build",
45+
"assert-command": "pnpm test:prod:import",
46+
"label": "nuxt-4 (--import compat)"
4147
}
4248
],
4349
"optionalVariants": [

‎dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@ if (!testEnv) {
88

99
const getStartCommand = () => {
1010
if (testEnv === 'development') {
11-
return "NODE_OPTIONS='--import ./.nuxt/dev/sentry.server.config.mjs' nuxt dev -p 3030";
11+
// The Sentry server config is bundled into the dev server via a nitro plugin, so no preload is needed.
12+
return 'nuxt dev -p 3030';
1213
}
1314

1415
if (testEnv === 'production') {
16+
return 'pnpm start';
17+
}
18+
19+
// Runs the suite with the compat shim preloaded, like existing `--import` deploy commands do.
20+
if (testEnv === 'production-import') {
1521
return 'pnpm start:import';
1622
}
1723

‎dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readFileSync, readdirSync } from 'node:fs';
1+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
22
import path from 'node:path';
33
import { expect, test } from '@playwright/test';
44

@@ -43,3 +43,37 @@ test.describe('Orchestrion build-time injection', () => {
4343
expect(clientBundle).not.toMatch(/orchestrion:/);
4444
});
4545
});
46+
47+
test.describe('Sentry server config injection', () => {
48+
test('evaluates Sentry.init before nitro runs its plugins', () => {
49+
const nitroChunk = readFileSync(path.join(process.cwd(), '.output/server/chunks/nitro/nitro.mjs'), 'utf8');
50+
51+
// The app DSN only appears in the transpiled `Sentry.init` options object, so it marks where
52+
// init evaluates inside the chunk.
53+
const initIndex = nitroChunk.indexOf('https://public@dsn.ingest.sentry.io/1337');
54+
const runPluginsIndex = nitroChunk.indexOf('runNitroPlugins');
55+
56+
expect(initIndex).toBeGreaterThan(-1);
57+
expect(runPluginsIndex).toBeGreaterThan(-1);
58+
expect(initIndex).toBeLessThan(runPluginsIndex);
59+
});
60+
61+
test('emits the `--import` compatibility shim at the former config path', () => {
62+
const shimPath = path.join(process.cwd(), '.output/server/sentry.server.config.mjs');
63+
64+
expect(existsSync(shimPath)).toBe(true);
65+
expect(readFileSync(shimPath, 'utf8')).toContain('no longer needed');
66+
});
67+
68+
test('does not bake tracing meta tags into prerendered pages', () => {
69+
// Prerendering executes the server bundle at build time; init is skipped there, so no Sentry
70+
// client may leak trace meta tags into the static HTML.
71+
const prerenderedPage = readFileSync(
72+
path.join(process.cwd(), '.output/public/rendering-modes/pre-rendered-page/index.html'),
73+
'utf8',
74+
);
75+
76+
expect(prerenderedPage).not.toContain('sentry-trace');
77+
expect(prerenderedPage).not.toContain('baggage');
78+
});
79+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { ChildProcess } from 'node:child_process';
2+
import { spawn } from 'node:child_process';
3+
import { expect, test } from '@playwright/test';
4+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
5+
6+
// `node --import` start commands must keep working now that the config is bundled: the emitted
7+
// config file is a shim that only prints a removal hint, and a preload that really initializes
8+
// the SDK must not cause a second init. Each test spawns its own server on a dedicated port.
9+
10+
interface PreloadedServer {
11+
child: ChildProcess;
12+
output: () => string;
13+
}
14+
15+
async function startServerWithPreload(preloadPath: string, port: string): Promise<PreloadedServer> {
16+
const child = spawn('node', ['--import', preloadPath, '.output/server/index.mjs'], {
17+
env: { ...process.env, PORT: port },
18+
});
19+
20+
let output = '';
21+
child.stdout?.on('data', chunk => (output += chunk));
22+
child.stderr?.on('data', chunk => (output += chunk));
23+
24+
for (let attempt = 0; attempt < 100; attempt++) {
25+
try {
26+
await fetch(`http://localhost:${port}/`);
27+
break;
28+
} catch {
29+
await new Promise(resolve => setTimeout(resolve, 200));
30+
}
31+
}
32+
33+
return { child, output: () => output };
34+
}
35+
36+
test('serves traced requests with the shim preloaded and prints the removal hint', async () => {
37+
const server = await startServerWithPreload('./.output/server/sentry.server.config.mjs', '3081');
38+
39+
try {
40+
const spanPromise = waitForStreamedSpan(
41+
'nuxt-4',
42+
span => span.is_segment === true && span.attributes?.['url.path']?.value === '/test-param/8281',
43+
);
44+
45+
const response = await fetch('http://localhost:3081/test-param/8281');
46+
expect(response.status).toBe(200);
47+
48+
const span = await spanPromise;
49+
expect(getSpanOp(span)).toBe('http.server');
50+
expect(server.output()).toContain('no longer needed');
51+
} finally {
52+
server.child.kill();
53+
}
54+
});
55+
56+
test('skips the second init when a preload already initialized the SDK', async () => {
57+
const server = await startServerWithPreload('./instrument-preload.mjs', '3082');
58+
59+
try {
60+
const spanPromise = waitForStreamedSpan(
61+
'nuxt-4',
62+
span => span.is_segment === true && span.attributes?.['url.path']?.value === '/test-param/8282',
63+
);
64+
65+
const response = await fetch('http://localhost:3082/test-param/8282');
66+
expect(response.status).toBe(200);
67+
68+
// The preload-created client stays active and still delivers events.
69+
const span = await spanPromise;
70+
expect(getSpanOp(span)).toBe('http.server');
71+
expect(server.output()).toContain('already initialized');
72+
} finally {
73+
server.child.kill();
74+
}
75+
});

‎dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash‎

Lines changed: 0 additions & 55 deletions
This file was deleted.

‎dev-packages/e2e-tests/test-applications/nuxt-5/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"clean": "npx nuxi cleanup",
1313
"test": "playwright test",
1414
"test:prod": "TEST_ENV=production playwright test",
15-
"test:dev": "bash ./nuxt-start-dev-server.bash && TEST_ENV=development playwright test environment",
15+
"test:dev": "TEST_ENV=development playwright test environment",
1616
"test:build": "pnpm install && pnpm build",
1717
"test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitro@npm:nitro-nightly@latest && pnpm install --force && pnpm build",
1818
"test:assert": "pnpm test:prod && pnpm test:dev"

‎dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ if (!testEnv) {
88

99
const getStartCommand = () => {
1010
if (testEnv === 'development') {
11-
return "NODE_OPTIONS='--import ./.nuxt/dev/sentry.server.config.mjs' nuxt dev -p 3030";
11+
// The Sentry server config is bundled into the dev server via a nitro plugin, so no preload is needed.
12+
return 'nuxt dev -p 3030';
1213
}
1314

1415
if (testEnv === 'production') {
15-
return 'pnpm start:import';
16+
return 'pnpm start';
1617
}
1718

1819
throw new Error(`Unknown test env: ${testEnv}`);

0 commit comments

Comments
 (0)