Skip to content
Open
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
34 changes: 29 additions & 5 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,34 @@ Affected SDKs: `@sentry/cloudflare`.

Calls to rate limiter bindings (`env.MY_RATE_LIMITER.limit()`) no longer create a span. The removed span had the op `rpc`, the origin `auto.faas.cloudflare.rate_limit`, and the attribute `rpc.service: cloudflare.rate_limit`. Remove any dashboard, alert, or `ignoreSpans` entry that references it.

### `@sentry/nuxt`: the server config is bundled, `--import` is no longer needed

The SDK now bundles `sentry.server.config.ts` into the Nitro server build, where it initializes itself when the server starts. Instrumentation happens at build time, so preloading the config file is no longer necessary.

Remove the `--import` flag from your production start command:

```bash
# before
node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs

# after
node .output/server/index.mjs
```

Old start commands keep working: the SDK still emits a file at the old path, but it only prints a reminder that the flag can be removed. If you preload a file that calls `Sentry.init` yourself, that init wins and the bundled one is skipped.

The same applies in development. Remove the `NODE_OPTIONS` preload:

```bash
# before
NODE_OPTIONS='--import ./.nuxt/dev/sentry.server.config.mjs' nuxt dev

# after
nuxt dev
```

Since no preload is needed anymore, the `autoInjectServerSentry` option (`'top-level-import'` and `'experimental_dynamic-import'`) and `experimental_entrypointWrappedFunctions` are deprecated. Remove them from your `sentry` module options as the default behavior replaces both. They will be deleted in the next major version.

### `@sentry/ember` is now a v2 addon with manual setup

Affected SDKs: `@sentry/ember`.
Expand Down Expand Up @@ -1698,11 +1726,7 @@ public/instrument.server.ts
sentry.server.config.ts
```

After the rename, the SDK also emits `.output/server/sentry.server.config.mjs` for you to preload:

```bash
node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs
```
After the rename, the SDK bundles the file into the Nitro server build and initializes itself at server startup. See ["the server config is bundled"](#sentrynuxt-the-server-config-is-bundled---import-is-no-longer-needed) above: the `--import` preload is no longer needed.

The deprecated `sourceMapsUploadOptions` module option was removed. Move its fields to the root level of the `sentry` module options. Note that `url` was renamed to `sentryUrl`, and `enabled` was replaced by `sourcemaps.disable` (inverted: `enabled: false` becomes `sourcemaps: { disable: true }`).

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Simulates a v10-style `node --import` preload that fully initializes the SDK
// before the config bundled into the server build runs its own `Sentry.init`.
import * as Sentry from '@sentry/nuxt';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0,
tunnel: 'http://localhost:3031/',
});

This file was deleted.

10 changes: 8 additions & 2 deletions dev-packages/e2e-tests/test-applications/nuxt-4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
"clean": "npx nuxi cleanup",
"test": "playwright test",
"test:prod": "TEST_ENV=production playwright test",
"test:dev": "bash ./nuxt-start-dev-server.bash && TEST_ENV=development playwright test environment",
"test:dev": "TEST_ENV=development playwright test environment",
"test:build": "pnpm install && pnpm build",
"test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitropack@npm:nitropack-nightly@latest && pnpm install --force && pnpm build",
"test:assert": "pnpm test:prod && pnpm test:dev"
"test:assert": "pnpm test:prod && pnpm test:dev",
"test:prod:import": "TEST_ENV=production-import playwright test"
},
"dependencies": {
"@pinia/nuxt": "^0.5.5",
Expand All @@ -38,6 +39,11 @@
"build-command": "E2E_TEST_OTEL_SETUP=true pnpm test:build",
"assert-command": "E2E_TEST_OTEL_SETUP=true pnpm test:assert",
"label": "nuxt-4 (tracer provider)"
},
{
"build-command": "pnpm test:build",
"assert-command": "pnpm test:prod:import",
"label": "nuxt-4 (--import compat)"
}
],
"optionalVariants": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ if (!testEnv) {

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

if (testEnv === 'production') {
return 'pnpm start';
}

// Runs the suite with the compat shim preloaded, like existing `--import` deploy commands do.
if (testEnv === 'production-import') {
return 'pnpm start:import';
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineNitroPlugin } from 'nitropack/runtime';

// Throws during module evaluation, before any plugin function runs.
// The `aa-` prefix makes this the first scanned plugin.
if (process.env.SENTRY_TEST_EVAL_CRASH) {
throw new Error('eval-crash-test');
}

export default defineNitroPlugin(() => {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineNitroPlugin } from 'nitropack/runtime';

// Throws while nitro runs its plugins, before `listen`.
// The `zz-` prefix makes this the last scanned plugin (`aa-eval-crash.ts` covers the earliest point).
export default defineNitroPlugin(() => {
if (process.env.SENTRY_TEST_STARTUP_CRASH) {
throw new Error('startup-crash-test');
}
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readFileSync, readdirSync } from 'node:fs';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { expect, test } from '@playwright/test';

Expand Down Expand Up @@ -43,3 +43,37 @@ test.describe('Orchestrion build-time injection', () => {
expect(clientBundle).not.toMatch(/orchestrion:/);
});
});

test.describe('Sentry server config injection', () => {
test('evaluates Sentry.init before nitro runs its plugins', () => {
const nitroChunk = readFileSync(path.join(process.cwd(), '.output/server/chunks/nitro/nitro.mjs'), 'utf8');

// The app DSN only appears in the transpiled `Sentry.init` options object, so it marks where
// init evaluates inside the chunk.
const initIndex = nitroChunk.indexOf('https://public@dsn.ingest.sentry.io/1337');
const runPluginsIndex = nitroChunk.indexOf('runNitroPlugins');

expect(initIndex).toBeGreaterThan(-1);
expect(runPluginsIndex).toBeGreaterThan(-1);
expect(initIndex).toBeLessThan(runPluginsIndex);
});

test('emits the `--import` compatibility shim at the former config path', () => {
const shimPath = path.join(process.cwd(), '.output/server/sentry.server.config.mjs');

expect(existsSync(shimPath)).toBe(true);
expect(readFileSync(shimPath, 'utf8')).toContain('no longer needed');
});

test('does not bake tracing meta tags into prerendered pages', () => {
// Prerendering executes the server bundle at build time; init is skipped there, so no Sentry
// client may leak trace meta tags into the static HTML.
const prerenderedPage = readFileSync(
path.join(process.cwd(), '.output/public/rendering-modes/pre-rendered-page/index.html'),
'utf8',
);

expect(prerenderedPage).not.toContain('sentry-trace');
expect(prerenderedPage).not.toContain('baggage');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prerender test does not prove skip

Low Severity

The prerender test treats missing sentry-trace and baggage tags as proof that bundled Sentry.init is skipped at build time. Those tags are already omitted for responses with the x-nitro-prerender header, so the assertion can pass even when init still runs and send events during CI builds.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 830a18a. Configure here.

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process';
import { expect, test } from '@playwright/test';
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

// `node --import` start commands must keep working now that the config is bundled: the emitted
// config file is a shim that only prints a removal hint, and a preload that really initializes
// the SDK must not cause a second init. Each test spawns its own server on a dedicated port.

interface PreloadedServer {
child: ChildProcess;
output: () => string;
}

async function startServerWithPreload(preloadPath: string, port: string): Promise<PreloadedServer> {
const child = spawn('node', ['--import', preloadPath, '.output/server/index.mjs'], {
env: { ...process.env, PORT: port },
});

let output = '';
child.stdout?.on('data', chunk => (output += chunk));
child.stderr?.on('data', chunk => (output += chunk));

for (let attempt = 0; attempt < 100; attempt++) {
try {
await fetch(`http://localhost:${port}/`);
break;
} catch {
await new Promise(resolve => setTimeout(resolve, 200));
}
}

return { child, output: () => output };
}

test('serves traced requests with the shim preloaded and prints the removal hint', async () => {
const server = await startServerWithPreload('./.output/server/sentry.server.config.mjs', '3081');

try {
const spanPromise = waitForStreamedSpan(
'nuxt-4',
span => span.is_segment === true && span.attributes?.['url.path']?.value === '/test-param/8281',
);

const response = await fetch('http://localhost:3081/test-param/8281');
expect(response.status).toBe(200);

const span = await spanPromise;
expect(getSpanOp(span)).toBe('http.server');
expect(server.output()).toContain('no longer needed');
} finally {
server.child.kill();
}
});

test('skips the second init when a preload already initialized the SDK', async () => {
const server = await startServerWithPreload('./instrument-preload.mjs', '3082');

try {
const spanPromise = waitForStreamedSpan(
'nuxt-4',
span => span.is_segment === true && span.attributes?.['url.path']?.value === '/test-param/8282',
);

const response = await fetch('http://localhost:3082/test-param/8282');
expect(response.status).toBe(200);

// The preload-created client stays active and still delivers events.
const span = await spanPromise;
expect(getSpanOp(span)).toBe('http.server');
expect(server.output()).toContain('already initialized');
} finally {
server.child.kill();
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { spawn } from 'node:child_process';
import { expect, test } from '@playwright/test';
import { waitForError, waitForSession } from '@sentry-internal/test-utils';

// Errors thrown between server start and `listen` (module evaluation, nitro plugin runs) must be
// captured and flushed before the process exits. Each test spawns its own server process because
// the crash kills it; events still reach the shared event proxy via the tunnel.

/** Starts the built server and resolves with its exit code. */
function spawnCrashingServer(env: Record<string, string>): Promise<number | null> {
return new Promise((resolve, reject) => {
const child = spawn('node', ['.output/server/index.mjs'], {
// Session tracking needs a release
env: { ...process.env, SENTRY_RELEASE: 'startup-error-test', ...env },
});
child.on('error', reject);
child.on('exit', code => resolve(code));
});
}

test('captures error and crashed session when a nitro plugin throws during startup', async () => {
const errorPromise = waitForError('nuxt-4', event => event.exception?.values?.[0]?.value === 'startup-crash-test');
const sessionPromise = waitForSession('nuxt-4', session => session.status === 'crashed');

const exitCode = await spawnCrashingServer({ SENTRY_TEST_STARTUP_CRASH: '1', PORT: '3077' });

const [errorEvent, session] = await Promise.all([errorPromise, sessionPromise]);

expect(exitCode).toBe(1);
expect(errorEvent.exception?.values?.[0]?.value).toBe('startup-crash-test');
expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false);
expect(session.status).toBe('crashed');
expect(session.errors).toBe(1);
});

test('captures error when a server module throws during bundle evaluation', async () => {
const errorPromise = waitForError('nuxt-4', event => event.exception?.values?.[0]?.value === 'eval-crash-test');

const exitCode = await spawnCrashingServer({ SENTRY_TEST_EVAL_CRASH: '1', PORT: '3078' });

const errorEvent = await errorPromise;

expect(exitCode).toBe(1);
expect(errorEvent.exception?.values?.[0]?.value).toBe('eval-crash-test');
expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false);
});
Loading
Loading