From 8ca69625c0d85c921305d1b8d5c755671de83176 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:59:40 +0200 Subject: [PATCH 1/8] cloudflare util --- packages/nuxt/src/vite/orchestrion.ts | 4 ++-- packages/nuxt/src/vite/utils.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/vite/orchestrion.ts b/packages/nuxt/src/vite/orchestrion.ts index 79938d010b4f..1a7f74ee62df 100644 --- a/packages/nuxt/src/vite/orchestrion.ts +++ b/packages/nuxt/src/vite/orchestrion.ts @@ -2,6 +2,7 @@ import type { Nuxt } from '@nuxt/schema'; import { INSTRUMENTED_MODULE_NAMES } from '@sentry/server-utils/orchestrion/config'; import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/rollup'; import type { NitroConfig } from 'nitropack'; +import { isCloudflarePreset } from './utils'; // ioredis requires this CommonJS helper to be bundled with it. Leaving it // external makes Nitro resolve the default export as a namespace object. @@ -34,8 +35,7 @@ export function setupOrchestrion(nuxt: Nuxt, hasServerConfig: boolean, buildTime // On Cloudflare (workerd) the SDK is initialized through `sentryCloudflareNitroPlugin` (no // server config file), so the transform must still run there — detected via the Nitro preset. - // Nitro normalizes preset names, so match any `cloudflare*` spelling. - const isCloudflare = !!nitroConfig.preset?.replace(/-/g, '_').startsWith('cloudflare'); + const isCloudflare = isCloudflarePreset(nitroConfig.preset); if (!hasServerConfig && !isCloudflare) { return; diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 81f4ed984720..158ff71a8df7 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -77,6 +77,11 @@ export async function findDefaultSdkInitFile( export const SERVER_CONFIG_FILENAME = 'sentry.server.config'; +/** Whether a resolved Nitro preset targets Cloudflare (workerd). Nitro normalizes preset names, so any `cloudflare*` spelling matches. */ +export function isCloudflarePreset(preset: string | undefined): boolean { + return !!preset?.replace(/-/g, '_').startsWith('cloudflare'); +} + /** Builds the value for `node --import`. Node reads it as a URL, so it needs forward slashes on Windows too. */ export function toImportSpecifier(fromDir: string, filePath: string): string { return `./${path.relative(fromDir, filePath).split(/[\\/]/).join('/')}`; From bece8466f5a48a1aeccf83643711c658af80efbe Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:00:39 +0200 Subject: [PATCH 2/8] fix package detection --- packages/nuxt/src/vite/utils.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 158ff71a8df7..4c95658e48b3 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -20,17 +20,22 @@ export async function getNitroMajorVersion(rootDir: string): Promise { try { const { getPackageInfo } = await import('local-pkg'); + // `paths` entries must point at a file: for a bare directory, resolution starts at the + // directory's parent and skips the directory's own `node_modules`, so a hoisted copy higher + // up the tree (e.g. a monorepo root) wins over the app's actual dependency. + const fromPackage = (dir: string): { paths: string[] } => ({ paths: [path.join(dir, 'package.json')] }); + // The package that declares the Nitro dependency: `nuxt` itself, or `@nuxt/nitro-server` (Nuxt >= 3.21) when nuxt delegates to it - let provider = await getPackageInfo('nuxt', { paths: [rootDir] }); + let provider = await getPackageInfo('nuxt', fromPackage(rootDir)); if (provider?.packageJson.dependencies?.['@nuxt/nitro-server']) { - provider = (await getPackageInfo('@nuxt/nitro-server', { paths: [provider.rootPath] })) ?? provider; + provider = (await getPackageInfo('@nuxt/nitro-server', fromPackage(provider.rootPath))) ?? provider; } if (!provider?.packageJson.dependencies?.nitro) { return 2; } - const info = await getPackageInfo('nitro', { paths: [provider.rootPath] }); + const info = await getPackageInfo('nitro', fromPackage(provider.rootPath)); const major = parseInt(info?.version?.split('.')[0] ?? '', 10); // The provider imports `nitro` (not `nitropack`), so it is at least v3 even if the version is unreadable return isNaN(major) ? 3 : major; From a84ae4f2c95359c65eb0fd8377831a45d5dd27d4 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:09:05 +0200 Subject: [PATCH 3/8] add deprecations --- packages/nuxt/src/common/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index d623c10f1d55..28514172e9c2 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -56,6 +56,10 @@ export type SentryNuxtModuleOptions = BuildTimeOptionsBase & { * If `"experimental_dynamic-import"` is enabled, the Sentry SDK wraps the server entry file with `import()`. * * @default undefined + * + * @deprecated The Sentry server config is bundled into the Nitro server build by default now and + * initializes itself at server startup — no `node --import` preload and no inject mode needed. + * Remove this option to use the default behavior. It will be removed in a future major version. */ autoInjectServerSentry?: 'top-level-import' | 'experimental_dynamic-import'; @@ -87,6 +91,9 @@ export type SentryNuxtModuleOptions = BuildTimeOptionsBase & { * Any wrapped export is expected to be an async function. * * @default ['default', 'handler', 'server'] + * + * @deprecated Only used with the deprecated `autoInjectServerSentry: 'experimental_dynamic-import'` + * mode. It will be removed in a future major version together with that mode. */ experimental_entrypointWrappedFunctions?: string[]; }; From 03efd42f88e6fcfff108c1b3b928c982e60ab7cf Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:12:03 +0200 Subject: [PATCH 4/8] crash tests --- .../nuxt-4/server/plugins/aa-eval-crash.ts | 9 ++++ .../nuxt-4/server/plugins/zz-startup-crash.ts | 9 ++++ .../nuxt-4/tests/startup-error.test.ts | 46 ++++++++++++++++++ .../nuxt-5/server/plugins/aa-eval-crash.ts | 9 ++++ .../nuxt-5/server/plugins/zz-startup-crash.ts | 9 ++++ .../nuxt-5/tests/startup-error.test.ts | 47 +++++++++++++++++++ 6 files changed, 129 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/plugins/aa-eval-crash.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/plugins/zz-startup-crash.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/tests/startup-error.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/plugins/aa-eval-crash.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/plugins/zz-startup-crash.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/tests/startup-error.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/plugins/aa-eval-crash.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/plugins/aa-eval-crash.ts new file mode 100644 index 000000000000..1354b5e5e5a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/plugins/aa-eval-crash.ts @@ -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(() => {}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/plugins/zz-startup-crash.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/plugins/zz-startup-crash.ts new file mode 100644 index 000000000000..44dc868227a1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/plugins/zz-startup-crash.ts @@ -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'); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/startup-error.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/startup-error.test.ts new file mode 100644 index 000000000000..fd6a98a0f714 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/startup-error.test.ts @@ -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): Promise { + 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); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/plugins/aa-eval-crash.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/plugins/aa-eval-crash.ts new file mode 100644 index 000000000000..87f007969cac --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/plugins/aa-eval-crash.ts @@ -0,0 +1,9 @@ +import { definePlugin } from 'nitro'; + +// 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 definePlugin(() => {}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/plugins/zz-startup-crash.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/plugins/zz-startup-crash.ts new file mode 100644 index 000000000000..86dad0f2a094 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/plugins/zz-startup-crash.ts @@ -0,0 +1,9 @@ +import { definePlugin } from 'nitro'; + +// 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 definePlugin(() => { + if (process.env.SENTRY_TEST_STARTUP_CRASH) { + throw new Error('startup-crash-test'); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/startup-error.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/startup-error.test.ts new file mode 100644 index 000000000000..3ea755529149 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/startup-error.test.ts @@ -0,0 +1,47 @@ +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): Promise { + return new Promise((resolve, reject) => { + const child = spawn('node', ['.output/server/index.mjs'], { + // Session tracking needs a release; builds without git (e.g. the E2E runner's temp copy) + // have none injected, so pin one for the crashed-session assertion. + 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-5', event => event.exception?.values?.[0]?.value === 'startup-crash-test'); + const sessionPromise = waitForSession('nuxt-5', 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-5', 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); +}); From 24c03548c12ca1f7857f7467a0778a7fd05c807d Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:35:31 +0200 Subject: [PATCH 5/8] double init guard --- packages/nuxt/src/common/devMode.ts | 20 +++++++++++++-- packages/nuxt/src/server/sdk.ts | 35 ++++++++++++++++++++++--- packages/nuxt/test/server/sdk.test.ts | 37 ++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/common/devMode.ts b/packages/nuxt/src/common/devMode.ts index f7fb61ce953d..a550d87528f8 100644 --- a/packages/nuxt/src/common/devMode.ts +++ b/packages/nuxt/src/common/devMode.ts @@ -1,9 +1,25 @@ import { GLOBAL_OBJ } from '@sentry/core'; -/** Global flag set by the generated `/dev/sentry.server.config.mjs`. */ +/** Global flag set by the generated runtime-flags module before the Sentry server config evaluates. */ export const NUXT_DEV_MODE_FLAG = '__SENTRY_NUXT_DEV_MODE__'; -/** Whether the SDK was preloaded by the generated `nuxt dev` server config file. */ +/** Global flag set by the generated runtime-flags module during a prerender build. */ +export const NUXT_PRERENDER_FLAG = '__SENTRY_NUXT_PRERENDER__'; + +/** Global flag set by the Nuxt server SDK after a successful `init`, to guard against a second init. */ +export const NUXT_SERVER_INITIALIZED_FLAG = '__SENTRY_NUXT_SERVER_INITIALIZED__'; + +/** Whether the server runs in `nuxt dev`. */ export function isNuxtDevRuntime(): boolean { return NUXT_DEV_MODE_FLAG in GLOBAL_OBJ && GLOBAL_OBJ[NUXT_DEV_MODE_FLAG] === true; } + +/** Whether a Nuxt server SDK `init` already ran in this process. */ +export function isNuxtServerInitialized(): boolean { + return NUXT_SERVER_INITIALIZED_FLAG in GLOBAL_OBJ && GLOBAL_OBJ[NUXT_SERVER_INITIALIZED_FLAG] === true; +} + +/** Records that the Nuxt server SDK initialized in this process. */ +export function markNuxtServerInitialized(): void { + (GLOBAL_OBJ as typeof GLOBAL_OBJ & { [NUXT_SERVER_INITIALIZED_FLAG]?: boolean })[NUXT_SERVER_INITIALIZED_FLAG] = true; +} diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 2af4ffb5fa03..170cb24eb31a 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,9 +1,21 @@ import * as path from 'node:path'; import type { Client, Event, EventProcessor } from '@sentry/core'; -import { applySdkMetadata, debug, DEFAULT_ENVIRONMENT, DEV_ENVIRONMENT, getGlobalScope } from '@sentry/core'; +import { + applySdkMetadata, + consoleSandbox, + debug, + DEFAULT_ENVIRONMENT, + DEV_ENVIRONMENT, + getClient, + getGlobalScope, +} from '@sentry/core'; import { init as initNode } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; -import { isNuxtDevRuntime } from '../common/devMode'; +import { + isNuxtDevRuntime, + isNuxtServerInitialized, + markNuxtServerInitialized, +} from '../common/devMode'; import type { SentryNuxtServerOptions } from '../common/types'; /** @@ -12,12 +24,25 @@ import type { SentryNuxtServerOptions } from '../common/types'; * @param options Configuration options for the SDK. */ export function init(options: SentryNuxtServerOptions): Client | undefined { + // Since the server config is bundled into the Nitro build, a `node --import` preload of a config + // file initializes the SDK a second time. The first init wins so a preload keeps its semantics. + if (isNuxtServerInitialized()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + '[Sentry] The Sentry server SDK is already initialized, skipping a second initialization. The Sentry server config is bundled into the Nitro server build, so a `node --import` preload of the config file is no longer needed and can be removed.', + ); + }); + return getClient(); + } + let isDevBuild = false; /*! rollup-include-esm-only */ isDevBuild = !!import.meta.dev; /*! rollup-include-esm-only-end */ - // Nitro v3 does not bundle the Sentry server config file, so `import.meta.dev` stays undefined there + // `import.meta.dev` is only substituted when this file itself is bundled; the generated + // runtime-flags module sets the global flag for the (usual) externalized case. const envFallback = isDevBuild || isNuxtDevRuntime() ? DEV_ENVIRONMENT : DEFAULT_ENVIRONMENT; const sentryOptions = { @@ -29,6 +54,10 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { const client = initNode(sentryOptions); + if (client) { + markNuxtServerInitialized(); + } + getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options)); getGlobalScope().addEventProcessor(clientSourceMapErrorFilter(options)); diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index 8e6ed325e6cd..4cfc378cf307 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -1,8 +1,9 @@ import type { Event, EventProcessor } from '@sentry/core'; +import { originalConsoleMethods } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import { getGlobalScope, Scope, SDK_VERSION } from '@sentry/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { NUXT_DEV_MODE_FLAG } from '../../src/common/devMode'; +import { NUXT_DEV_MODE_FLAG, NUXT_PRERENDER_FLAG, NUXT_SERVER_INITIALIZED_FLAG } from '../../src/common/devMode'; import { init } from '../../src/server'; import { clientSourceMapErrorFilter, lowQualityTransactionsFilter } from '../../src/server/sdk'; @@ -12,6 +13,9 @@ describe('Nuxt Server SDK', () => { describe('init', () => { beforeEach(() => { vi.clearAllMocks(); + // Each test needs a fresh init; the double-init guard would otherwise skip every later call. + delete (globalThis as { __SENTRY_NUXT_SERVER_INITIALIZED__?: boolean }).__SENTRY_NUXT_SERVER_INITIALIZED__; + delete (globalThis as { __SENTRY_NUXT_PRERENDER__?: boolean }).__SENTRY_NUXT_PRERENDER__; }); it('Adds Nuxt metadata to the SDK options', () => { @@ -42,6 +46,37 @@ describe('Nuxt Server SDK', () => { expect(init({})).not.toBeUndefined(); }); + describe('initialization guards', () => { + it('skips a second initialization and notifies that the `--import` preload is removable', () => { + // A `node --import` preload of the config file initializes once before the bundled config does. + expect(NUXT_SERVER_INITIALIZED_FLAG).toBe('__SENTRY_NUXT_SERVER_INITIALIZED__'); + // `consoleSandbox` swaps in the method recorded in `originalConsoleMethods`, so a spy on + // `console.log` never sees the notice — intercept the sandboxed method instead. + const logMock = vi.fn(); + const originalLog = originalConsoleMethods.log; + originalConsoleMethods.log = logMock; + + try { + const firstClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + const secondClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(nodeInit).toHaveBeenCalledTimes(1); + expect(secondClient).toBe(firstClient); + expect(logMock).toHaveBeenCalledWith(expect.stringContaining('already initialized')); + } finally { + originalConsoleMethods.log = originalLog; + } + }); + + it('marks a successful initialization for the double-init guard', () => { + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect( + (globalThis as { __SENTRY_NUXT_SERVER_INITIALIZED__?: boolean }).__SENTRY_NUXT_SERVER_INITIALIZED__, + ).toBe(true); + }); + }); + it('delegates default integrations to initNode when not provided in options', () => { // Resolving them here would pin the selection to the raw options, before `initNode` // resolves `SENTRY_TRACES_SAMPLE_RATE`, and would drop the performance integrations From 89729c371ac10f269eeaaa389dda1a61a1a27e89 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:26:29 +0200 Subject: [PATCH 6/8] check for prerender runtime --- packages/nuxt/src/common/devMode.ts | 5 +++++ packages/nuxt/src/server/sdk.ts | 8 ++++++++ packages/nuxt/test/server/sdk.test.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/packages/nuxt/src/common/devMode.ts b/packages/nuxt/src/common/devMode.ts index a550d87528f8..f576a916c02a 100644 --- a/packages/nuxt/src/common/devMode.ts +++ b/packages/nuxt/src/common/devMode.ts @@ -14,6 +14,11 @@ export function isNuxtDevRuntime(): boolean { return NUXT_DEV_MODE_FLAG in GLOBAL_OBJ && GLOBAL_OBJ[NUXT_DEV_MODE_FLAG] === true; } +/** Whether the server bundle is executed by the Nitro prerenderer at build time. */ +export function isNuxtPrerenderRuntime(): boolean { + return NUXT_PRERENDER_FLAG in GLOBAL_OBJ && GLOBAL_OBJ[NUXT_PRERENDER_FLAG] === true; +} + /** Whether a Nuxt server SDK `init` already ran in this process. */ export function isNuxtServerInitialized(): boolean { return NUXT_SERVER_INITIALIZED_FLAG in GLOBAL_OBJ && GLOBAL_OBJ[NUXT_SERVER_INITIALIZED_FLAG] === true; diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 170cb24eb31a..ec309246f176 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -13,6 +13,7 @@ import { init as initNode } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; import { isNuxtDevRuntime, + isNuxtPrerenderRuntime, isNuxtServerInitialized, markNuxtServerInitialized, } from '../common/devMode'; @@ -24,6 +25,13 @@ import type { SentryNuxtServerOptions } from '../common/types'; * @param options Configuration options for the SDK. */ export function init(options: SentryNuxtServerOptions): Client | undefined { + // The prerenderer executes the server bundle (including nitro plugins) at build time (pollutes release health and adds build-time traces) + if (isNuxtPrerenderRuntime()) { + // potential follow-up: configurable with `capturePrerenderErrors` + DEBUG_BUILD && debug.log('Detected a Nitro prerender build. Skipping Sentry server initialization.'); + return undefined; + } + // Since the server config is bundled into the Nitro build, a `node --import` preload of a config // file initializes the SDK a second time. The first init wins so a preload keeps its semantics. if (isNuxtServerInitialized()) { diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index 4cfc378cf307..ba2c5f82818c 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -47,6 +47,20 @@ describe('Nuxt Server SDK', () => { }); describe('initialization guards', () => { + it('skips initialization during a prerender build', () => { + const globalWithFlag = globalThis as { __SENTRY_NUXT_PRERENDER__?: boolean }; + + // The generated runtime-flags module sets this by name, so a rename must break the test rather than the runtime. + expect(NUXT_PRERENDER_FLAG).toBe('__SENTRY_NUXT_PRERENDER__'); + + globalWithFlag.__SENTRY_NUXT_PRERENDER__ = true; + + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(client).toBeUndefined(); + expect(nodeInit).not.toHaveBeenCalled(); + }); + it('skips a second initialization and notifies that the `--import` preload is removable', () => { // A `node --import` preload of the config file initializes once before the bundled config does. expect(NUXT_SERVER_INITIALIZED_FLAG).toBe('__SENTRY_NUXT_SERVER_INITIALIZED__'); From c133bb8adbb35c2edb7f0050c34496feac9202ad Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:04:16 +0200 Subject: [PATCH 7/8] feat(nuxt)!: Bundle server config into Nitro build --- .../nuxt-4/instrument-preload.mjs | 9 + .../nuxt-4/nuxt-start-dev-server.bash | 55 ----- .../test-applications/nuxt-4/package.json | 10 +- .../nuxt-4/playwright.config.ts | 8 +- .../nuxt-4/tests/build-injection.test.ts | 36 +++- .../nuxt-4/tests/import-compat.test.ts | 75 +++++++ .../nuxt-5/nuxt-start-dev-server.bash | 55 ----- .../test-applications/nuxt-5/package.json | 2 +- .../nuxt-5/playwright.config.ts | 5 +- packages/nuxt/src/module.ts | 112 ++++------ packages/nuxt/src/vite/addServerConfig.ts | 144 +++++++++---- .../nuxt/test/vite/addServerConfig.test.ts | 192 ++++++++++++++---- 12 files changed, 432 insertions(+), 271 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/instrument-preload.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/tests/import-compat.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/instrument-preload.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4/instrument-preload.mjs new file mode 100644 index 000000000000..0d37fb5bffe2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/instrument-preload.mjs @@ -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/', +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash deleted file mode 100644 index 4affbb553b0e..000000000000 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -# To enable Sentry in Nuxt dev, it needs the sentry.server.config.mjs file from the .nuxt folder. -# First, we need to start 'nuxt dev' to generate the file, and then start 'nuxt dev' again with the NODE_OPTIONS to have Sentry enabled. - -# Using a different port to avoid playwright already starting with the tests for port 3030 -TEMP_PORT=3035 - -# 1. Start dev in background - this generates .nuxt folder -pnpm dev -p $TEMP_PORT & -DEV_PID=$! - -# 2. Wait for the sentry.server.config.mjs file to appear -echo "Waiting for .nuxt/dev/sentry.server.config.mjs file..." -COUNTER=0 -while [ ! -f ".nuxt/dev/sentry.server.config.mjs" ] && [ $COUNTER -lt 30 ]; do - sleep 1 - ((COUNTER++)) -done - -if [ ! -f ".nuxt/dev/sentry.server.config.mjs" ]; then - echo "ERROR: .nuxt/dev/sentry.server.config.mjs file never appeared!" - echo "This usually means the Nuxt dev server failed to start or generate the file. Try to rerun the test." - pkill -P $DEV_PID || kill $DEV_PID - exit 1 -fi - -# 3. Cleanup -# `pkill -P` only kills direct children, so the grandchild dev server holding the -# port survives; newer Nuxt's directory-scoped dev lock then blocks the real start. -echo "Found .nuxt/dev/sentry.server.config.mjs, stopping 'nuxt dev' process..." -pkill -P $DEV_PID 2>/dev/null -kill $DEV_PID 2>/dev/null - -# Wait for port to be released -echo "Waiting for port $TEMP_PORT to be released..." -COUNTER=0 -# Check if port is still in use -while lsof -i :$TEMP_PORT > /dev/null 2>&1 && [ $COUNTER -lt 10 ]; do - sleep 1 - ((COUNTER++)) -done - -if lsof -i :$TEMP_PORT > /dev/null 2>&1; then - echo "Port $TEMP_PORT still in use, killing remaining processes bound to it..." - lsof -t -i :$TEMP_PORT | xargs -r kill -9 2>/dev/null - sleep 1 -fi - -if lsof -i :$TEMP_PORT > /dev/null 2>&1; then - echo "WARNING: Port $TEMP_PORT still in use, proceeding anyway..." -else - echo "Port $TEMP_PORT released successfully" -fi - -echo "Nuxt dev server can now be started with '--import ./.nuxt/dev/sentry.server.config.mjs'" diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json index ce6c0aeb9c07..d915f94fa0a5 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -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", @@ -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": [ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts index 0b61db7cde11..bc80fed9ac7d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts @@ -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'; } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts index 796a7538c0ee..ae71967eebad 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts @@ -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'; @@ -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'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/import-compat.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/import-compat.test.ts new file mode 100644 index 000000000000..a2eac8bbbf54 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/import-compat.test.ts @@ -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 { + 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(); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash b/dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash deleted file mode 100644 index 4affbb553b0e..000000000000 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -# To enable Sentry in Nuxt dev, it needs the sentry.server.config.mjs file from the .nuxt folder. -# First, we need to start 'nuxt dev' to generate the file, and then start 'nuxt dev' again with the NODE_OPTIONS to have Sentry enabled. - -# Using a different port to avoid playwright already starting with the tests for port 3030 -TEMP_PORT=3035 - -# 1. Start dev in background - this generates .nuxt folder -pnpm dev -p $TEMP_PORT & -DEV_PID=$! - -# 2. Wait for the sentry.server.config.mjs file to appear -echo "Waiting for .nuxt/dev/sentry.server.config.mjs file..." -COUNTER=0 -while [ ! -f ".nuxt/dev/sentry.server.config.mjs" ] && [ $COUNTER -lt 30 ]; do - sleep 1 - ((COUNTER++)) -done - -if [ ! -f ".nuxt/dev/sentry.server.config.mjs" ]; then - echo "ERROR: .nuxt/dev/sentry.server.config.mjs file never appeared!" - echo "This usually means the Nuxt dev server failed to start or generate the file. Try to rerun the test." - pkill -P $DEV_PID || kill $DEV_PID - exit 1 -fi - -# 3. Cleanup -# `pkill -P` only kills direct children, so the grandchild dev server holding the -# port survives; newer Nuxt's directory-scoped dev lock then blocks the real start. -echo "Found .nuxt/dev/sentry.server.config.mjs, stopping 'nuxt dev' process..." -pkill -P $DEV_PID 2>/dev/null -kill $DEV_PID 2>/dev/null - -# Wait for port to be released -echo "Waiting for port $TEMP_PORT to be released..." -COUNTER=0 -# Check if port is still in use -while lsof -i :$TEMP_PORT > /dev/null 2>&1 && [ $COUNTER -lt 10 ]; do - sleep 1 - ((COUNTER++)) -done - -if lsof -i :$TEMP_PORT > /dev/null 2>&1; then - echo "Port $TEMP_PORT still in use, killing remaining processes bound to it..." - lsof -t -i :$TEMP_PORT | xargs -r kill -9 2>/dev/null - sleep 1 -fi - -if lsof -i :$TEMP_PORT > /dev/null 2>&1; then - echo "WARNING: Port $TEMP_PORT still in use, proceeding anyway..." -else - echo "Port $TEMP_PORT released successfully" -fi - -echo "Nuxt dev server can now be started with '--import ./.nuxt/dev/sentry.server.config.mjs'" diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/package.json b/dev-packages/e2e-tests/test-applications/nuxt-5/package.json index 619ed809b087..2279ff1980bc 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/package.json @@ -12,7 +12,7 @@ "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 nitro@npm:nitro-nightly@latest && pnpm install --force && pnpm build", "test:assert": "pnpm test:prod && pnpm test:dev" diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts index b86690ca086c..80df201381ce 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts @@ -8,11 +8,12 @@ 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:import'; + return 'pnpm start'; } throw new Error(`Unknown test env: ${testEnv}`); diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 646347db1cee..bc03deb58328 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -14,23 +14,18 @@ import { consoleSandbox } from '@sentry/core'; import * as path from 'path'; import type { SentryNuxtModuleOptions } from './common/types'; import { - addDevServerConfigFile, addDynamicImportEntryFileWrapper, + addServerConfigShimWithWarning, addSentryTopImport, + addServerConfigPlugin, addServerConfigToBuild, - DEV_SERVER_CONFIG_PATH, } from './vite/addServerConfig'; import { addDatabaseInstrumentation } from './vite/databaseConfig'; import { addMiddlewareImports, addMiddlewareInstrumentation } from './vite/middlewareConfig'; import { setupOrchestrion } from './vite/orchestrion'; import { setupSourceMaps } from './vite/sourceMaps'; import { addStorageInstrumentation } from './vite/storageConfig'; -import { - addOTelCommonJSImportAlias, - findDefaultSdkInitFile, - getNitroMajorVersion, - toImportSpecifier, -} from './vite/utils'; +import { addOTelCommonJSImportAlias, findDefaultSdkInitFile, getNitroMajorVersion } from './vite/utils'; export type ModuleOptions = SentryNuxtModuleOptions; type NuxtPageSubset = { file?: string; path: string }; @@ -51,7 +46,9 @@ export default defineNuxtModule({ const moduleOptions = { ...moduleOptionsParam, + // oxlint-disable-next-line typescript/no-deprecated -- supported until removal autoInjectServerSentry: moduleOptionsParam.autoInjectServerSentry, + // oxlint-disable-next-line typescript/no-deprecated -- supported until removal experimental_entrypointWrappedFunctions: moduleOptionsParam.experimental_entrypointWrappedFunctions || [ 'default', 'handler', @@ -103,7 +100,16 @@ export default defineNuxtModule({ // Cloudflare detection happens inside, keyed off the resolved Nitro preset. setupOrchestrion(nuxt, !!serverConfigFile, moduleOptions.buildTimeInstrumentation); + // The deprecated inject modes replace the default in-bundle initialization until their removal + const usesDeprecatedInjectMode = + moduleOptions.autoInjectServerSentry === 'top-level-import' || + moduleOptions.autoInjectServerSentry === 'experimental_dynamic-import'; + if (serverConfigFile) { + if (!usesDeprecatedInjectMode) { + addServerConfigPlugin(nuxt, serverConfigFile); + } + if (isNitroV3) { addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/handler.server')); addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/update-route-name.server')); @@ -124,11 +130,6 @@ export default defineNuxtModule({ addMiddlewareImports(); addStorageInstrumentation(nuxt, !isNitroV3); addDatabaseInstrumentation(nuxt.options.nitro, !isNitroV3, moduleOptions); - - // Outside `nitro:init` so that `nuxt prepare` writes the file before the first `nuxt dev`. - if (isNitroV3) { - addDevServerConfigFile(nuxt, serverConfigFile); - } } if (clientConfigFile || serverConfigFile) { @@ -194,77 +195,44 @@ export default defineNuxtModule({ if (serverConfigFile) { addMiddlewareInstrumentation(nitro); - consoleSandbox(() => { - const serverDir = nitro.options.output.serverDir; + if (!usesDeprecatedInjectMode) { + addServerConfigShimWithWarning(nitro); - // Netlify env: https://docs.netlify.com/configure-builds/environment-variables/#build-metadata - if (serverDir.includes('.netlify') || !!process.env.NETLIFY) { - // eslint-disable-next-line no-console - console.warn( - '[Sentry] Warning: The Sentry SDK detected a Netlify build. Server-side support for the Sentry Nuxt SDK on Netlify is currently unreliable due to technical limitations of serverless functions. Traces are not collected, and errors may occasionally not be reported. For more information on setting up Sentry on the Nuxt server-side, please refer to the documentation: https://docs.sentry.io/platforms/javascript/guides/nuxt/install/', - ); + if (moduleOptions.debug) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Bundled \`${serverConfigFile}\` into the Nitro server build. The SDK initializes itself at server startup — no \`node --import\` preload needed.`, + ); + }); } - - // Vercel env: https://vercel.com/docs/projects/environment-variables/system-environment-variables#VERCEL - if (serverDir.includes('.vercel') || !!process.env.VERCEL) { + } else { + consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( - '[Sentry] Warning: The Sentry SDK detected a Vercel build. The Sentry Nuxt SDK currently does not support tracing on Vercel. For more information on setting up Sentry on the Nuxt server-side, please refer to the documentation: https://docs.sentry.io/platforms/javascript/guides/nuxt/install/', + `[Sentry] \`autoInjectServerSentry: '${moduleOptions.autoInjectServerSentry}'\` is deprecated and will be removed in a future major version. The Sentry server config is bundled into the Nitro server build by default now. Remove the option to use the default behavior.`, ); - } - }); + }); - if (moduleOptions.autoInjectServerSentry !== 'experimental_dynamic-import') { - // Nitro 3 (in Nuxt 5) is not bundled in dev mode. See `addDevServerConfigFile` for how we add the file now. - if (!(isNitroV3 && nitro.options.dev)) { - addServerConfigToBuild(moduleOptions, nitro, serverConfigFile); + if (moduleOptions.autoInjectServerSentry === 'top-level-import') { + // Nitro 3 (in Nuxt 5) is not bundled in dev mode, so there is no build to emit into. + if (!(isNitroV3 && nitro.options.dev)) { + addServerConfigToBuild(moduleOptions, nitro, serverConfigFile); + } + addSentryTopImport(moduleOptions, nitro); } - if (moduleOptions.debug) { - const serverDirResolver = createResolver(nitro.options.output.serverDir); - const serverConfigPath = serverDirResolver.resolve('sentry.server.config.mjs'); - - // For the default nitro node-preset build output this relative path would be: ./.output/server/sentry.server.config.mjs - const serverConfigRelativePath = toImportSpecifier(nitro.options.rootDir, serverConfigPath); - const devConfigRelativePath = isNitroV3 - ? toImportSpecifier(nuxt.options.rootDir, path.join(nuxt.options.buildDir, DEV_SERVER_CONFIG_PATH)) - : serverConfigRelativePath; + if (moduleOptions.autoInjectServerSentry === 'experimental_dynamic-import') { + addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions); - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.log( - `[Sentry] Using \`${serverConfigFile}\` for server-side Sentry configuration. To activate Sentry on the Nuxt server-side, this file must be preloaded when starting your application. Make sure to add this where you deploy and/or run your application. Read more here: https://docs.sentry.io/platforms/javascript/guides/nuxt/install/.`, - ); - - if (nitro.options.dev) { + if (moduleOptions.debug) { + consoleSandbox(() => { // eslint-disable-next-line no-console console.log( - `[Sentry] During development, preload Sentry with the NODE_OPTIONS environment variable: \`NODE_OPTIONS='--import ${devConfigRelativePath}' nuxt dev\`. The file is generated in the build directory (usually '.nuxt'). If you delete the build directory, run \`nuxt prepare\` to regenerate it.`, + '[Sentry] Wrapping the server entry file with a dynamic `import()`, so Sentry can be preloaded before the server initializes.', ); - } else { - // eslint-disable-next-line no-console - console.log( - `[Sentry] When running your built application, preload Sentry via a command-line flag (\`node --import ${serverConfigRelativePath} [...]\`) or via an environment variable (\`NODE_OPTIONS='--import ${serverConfigRelativePath}' node [...]\`).`, - ); - } - }); - } - } - - if (moduleOptions.autoInjectServerSentry === 'top-level-import') { - addSentryTopImport(moduleOptions, nitro); - } - - if (moduleOptions.autoInjectServerSentry === 'experimental_dynamic-import') { - addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions); - - if (moduleOptions.debug) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.log( - '[Sentry] Wrapping the server entry file with a dynamic `import()`, so Sentry can be preloaded before the server initializes.', - ); - }); + }); + } } } } diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 754bdcd8592f..c952dfa5a65d 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -1,66 +1,28 @@ import { existsSync } from 'node:fs'; import { basename } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { addTemplate, createResolver } from '@nuxt/kit'; +import { addServerPlugin, addTemplate, createResolver } from '@nuxt/kit'; import type { Nuxt } from '@nuxt/schema'; -import { debug } from '@sentry/core'; +import { consoleSandbox, debug } from '@sentry/core'; import * as fs from 'fs'; import type { Nitro } from 'nitropack'; -import * as path from 'path'; import type { InputPluginOption } from 'rollup'; -import { NUXT_DEV_MODE_FLAG } from '../common/devMode'; +import { NUXT_DEV_MODE_FLAG, NUXT_PRERENDER_FLAG } from '../common/devMode'; import type { SentryNuxtModuleOptions } from '../common/types'; import { constructFunctionReExport, constructWrappedFunctionExportQuery, getFilenameFromNodeStartCommand, + isCloudflarePreset, QUERY_END_INDICATOR, removeSentryQueryFromPath, SENTRY_REEXPORTED_FUNCTIONS, SENTRY_WRAPPED_ENTRY, SENTRY_WRAPPED_FUNCTIONS, SERVER_CONFIG_FILENAME, - toImportSpecifier, toResolvablePath, } from './utils'; -/** Path of the generated dev-mode config file, relative to the Nuxt build directory. */ -export const DEV_SERVER_CONFIG_PATH = `dev/${SERVER_CONFIG_FILENAME}.mjs`; - -/** - * Writes the file users preload with `node --import` to enable Sentry in `nuxt dev` (for Nuxt 5 with Nitro 3). - * - * In dev-mode, Nitro v3 has no server bundle to emit into, so Node loads the server config file as it is written. - */ -export function addDevServerConfigFile(nuxt: Nuxt, serverConfigFile: string): void { - const configPath = createResolver(nuxt.options.rootDir).resolve(serverConfigFile); - const importSpecifier = toImportSpecifier( - nuxt.options.rootDir, - path.join(nuxt.options.buildDir, DEV_SERVER_CONFIG_PATH), - ); - - const failureMessage = - `[Sentry] Could not load \`${path.basename(configPath)}\`, so Sentry is disabled during development. ` + - 'Node loads this file without a build step, so it supports neither path aliases (like #import) nor non-erasable TypeScript syntax (like enums).'; - - addTemplate({ - filename: DEV_SERVER_CONFIG_PATH, - write: true, - getContents: () => - [ - '// Generated by @sentry/nuxt. Preload it to enable Sentry during development:', - `// NODE_OPTIONS='--import ${importSpecifier}' nuxt dev`, - // A static import would hoist above this assignment, and would make a broken config crash the dev server. - `globalThis.${NUXT_DEV_MODE_FLAG} = true;`, - 'try {', - ` await import(${JSON.stringify(pathToFileURL(configPath).href)});`, - '} catch (error) {', - ` console.warn(${JSON.stringify(failureMessage)}, error);`, - '}', - '', - ].join('\n'), - }); -} const CONFIG_EXTENSIONS = ['.ts', '.js', '.mjs', '.cjs', '.mts', '.cts']; function isServerConfigFile(sourcePath: string, resolvedPath: string): boolean { @@ -142,6 +104,103 @@ export function addSentryTopImport(moduleOptions: SentryNuxtModuleOptions, nitro }); } +/** + * Registers a Nitro plugin that statically imports the Sentry server config, so the SDK initializes + * at server startup without a `node --import` preload. + */ +export function addServerConfigPlugin(nuxt: Nuxt, serverConfigFile: string): void { + const configPath = createResolver(nuxt.options.rootDir).resolve(serverConfigFile); + + // `Sentry.init` reads these flags, and a statement above the config import would not survive + // import hoisting — so they live in their own module which is imported first. + const runtimeFlagsTemplate = addTemplate({ + filename: 'sentry-runtime-flags.mjs', + write: true, + getContents: () => + [ + '// Generated by @sentry/nuxt. Sets runtime flags before the Sentry server config evaluates.', + `globalThis.${NUXT_DEV_MODE_FLAG} = import.meta.dev === true;`, + `globalThis.${NUXT_PRERENDER_FLAG} = import.meta.prerender === true;`, + '', + ].join('\n'), + }); + + const configPluginTemplate = addTemplate({ + filename: 'sentry-server-config-plugin.mjs', + write: true, + getContents: () => + `import ${JSON.stringify(runtimeFlagsTemplate.dst)};\nimport ${JSON.stringify(configPath)};\nexport default () => {};\n`, + }); + + addServerPlugin(configPluginTemplate.dst); + + // Nitro treeshakes side-effect-only imports outside its runtime dir, which would silently drop + // the top-level `Sentry.init` and the flag assignments. + nuxt.options.nitro.moduleSideEffects = [ + ...(nuxt.options.nitro.moduleSideEffects ?? []), + configPath, + runtimeFlagsTemplate.dst, + ]; + + nuxt.hook('nitro:config', nitroConfig => { + // On Cloudflare the SDK is set up through `sentryCloudflareNitroPlugin`; the Node SDK config + // must not end up in the worker bundle. + if (isCloudflarePreset(nitroConfig.preset)) { + nitroConfig.plugins = (nitroConfig.plugins ?? []).filter(plugin => plugin !== configPluginTemplate.dst); + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] Found \`${basename(configPath)}\`, but the Nitro preset targets Cloudflare, where this file is not used. Set up the SDK with \`sentryCloudflareNitroPlugin\` instead: https://docs.sentry.io/platforms/javascript/guides/nuxt/install/cloudflare-workers/`, + ); + }); + return; + } + + // Front the config plugin so `Sentry.init` evaluates before plugins registered by other Nuxt + // modules (Nitro appends scanned user plugins after the configured ones anyway). + const plugins = nitroConfig.plugins ?? []; + nitroConfig.plugins = [configPluginTemplate.dst, ...plugins.filter(plugin => plugin !== configPluginTemplate.dst)]; + + // The Nitro v2 dev bundle would otherwise externalize these files, making Node load the raw + // `.ts` config — which needs type stripping (Node >= 22.18). Inlining keeps them transpiled. + const externals = (nitroConfig.externals ??= {}); + const inline = externals.inline; + const existingInline = Array.isArray(inline) ? inline : inline ? [inline] : []; + externals.inline = [...existingInline, configPath, configPluginTemplate.dst, runtimeFlagsTemplate.dst]; + }); +} + +/** + * Writes a shim to the former `--import` config path, so existing `node --import` start commands + * keep working now that the config is bundled into the server build. + */ +export function addServerConfigShimWithWarning(nitro: Nitro): void { + nitro.hooks.hook('close', async () => { + if (nitro.options.dev || nitro.options.preset === 'nitro-prerender' || isCloudflarePreset(nitro.options.preset)) { + return; + } + + const shimPath = createResolver(nitro.options.output.serverDir).resolve(`${SERVER_CONFIG_FILENAME}.mjs`); + const contents = [ + '// Generated by @sentry/nuxt.', + '// The Sentry server config is bundled into the server build and initializes automatically.', + '// This file only keeps existing `node --import ./.output/server/sentry.server.config.mjs` commands working.', + "console.warn('[Sentry] The `--import` flag for the Sentry server config is no longer needed and should be removed.');", + '', + ].join('\n'); + + try { + await fs.promises.writeFile(shimPath, contents, 'utf8'); + } catch (error) { + // A missing shim breaks `node --import` start commands, so always warn (`debug` is off at build time). + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn(`[Sentry] Could not write the \`--import\` compatibility shim to ${shimPath}`, error); + }); + } + }); +} + /** * This function modifies the Rollup configuration to include a plugin that wraps the entry file with a dynamic import (`import()`) * and adds the Sentry server config with the static `import` declaration. @@ -169,6 +228,7 @@ export function addDynamicImportEntryFileWrapper( nitro.options.rollupConfig.plugins.push( wrapEntryWithDynamicImport({ resolvedSentryConfigPath: createResolver(nitro.options.rootDir).resolve(serverConfigFile), + // oxlint-disable-next-line typescript/no-deprecated -- supported until removal experimental_entrypointWrappedFunctions: moduleOptions.experimental_entrypointWrappedFunctions, }), ); diff --git a/packages/nuxt/test/vite/addServerConfig.test.ts b/packages/nuxt/test/vite/addServerConfig.test.ts index 54fdc0e1a33d..aa1b8abfa84f 100644 --- a/packages/nuxt/test/vite/addServerConfig.test.ts +++ b/packages/nuxt/test/vite/addServerConfig.test.ts @@ -1,10 +1,11 @@ -import type { Nuxt } from '@nuxt/schema'; import { fileURLToPath, pathToFileURL } from 'node:url'; import * as path from 'path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as fs from 'fs'; +import type { NitroConfig } from 'nitropack'; import { - addDevServerConfigFile, - DEV_SERVER_CONFIG_PATH, + addServerConfigShimWithWarning, + addServerConfigPlugin, wrapEntryWithDynamicImport, } from '../../src/vite/addServerConfig'; import { @@ -113,9 +114,11 @@ describe('wrapEntryWithDynamicImport', () => { }); const addTemplateMock = vi.hoisted(() => vi.fn()); +const addServerPluginMock = vi.hoisted(() => vi.fn()); vi.mock('@nuxt/kit', () => ({ addTemplate: addTemplateMock, + addServerPlugin: addServerPluginMock, // `@nuxt/kit` resolves rather than joins, which is what lets an absolute layer path win over the base. createResolver: (base: string) => ({ resolve: (input: string) => path.resolve(base, input) }), })); @@ -123,62 +126,171 @@ vi.mock('@nuxt/kit', () => ({ const APP_ROOT = '/my/monorepo/apps/web'; // `findDefaultSdkInitFile` always returns an absolute path, built from the layer's own `cwd`. const APP_CONFIG = `${APP_ROOT}/sentry.server.config.ts`; -const LAYER_CONFIG = '/my/monorepo/layers/base/sentry.server.config.ts'; -function generate(serverConfigFile: string): string { - const nuxt = { options: { rootDir: APP_ROOT, buildDir: path.join(APP_ROOT, '.nuxt') } } as Nuxt; +describe('addServerConfigPlugin', () => { + const flagsDst = `${APP_ROOT}/.nuxt/sentry-runtime-flags.mjs`; + const pluginDst = `${APP_ROOT}/.nuxt/sentry-server-config-plugin.mjs`; + + function createFakeNuxt(): { + nuxt: Parameters[0]; + hooks: Record void>; + } { + const hooks: Record void> = {}; + const nuxt = { + options: { rootDir: APP_ROOT, buildDir: path.join(APP_ROOT, '.nuxt'), nitro: {} }, + hook: (name: string, callback: (nitroConfig: NitroConfig) => void) => { + hooks[name] = callback; + }, + } as unknown as Parameters[0]; + + return { nuxt, hooks }; + } + + function templateContents(filename: string): string { + const call = addTemplateMock.mock.calls.find(args => args[0]?.filename === filename); + expect(call).toBeDefined(); + return call?.[0].getContents(); + } - addDevServerConfigFile(nuxt, serverConfigFile); - - return addTemplateMock.mock.calls[0]?.[0].getContents(); -} - -describe('addDevServerConfigFile', () => { beforeEach(() => { vi.clearAllMocks(); + addTemplateMock.mockImplementation((opts: { filename: string }) => ({ + dst: `${APP_ROOT}/.nuxt/${opts.filename}`, + })); }); - it('writes the file into the build directory so `--import` can resolve it', () => { - generate(APP_CONFIG); + it('registers a plugin that evaluates the runtime flags before the config', () => { + const { nuxt } = createFakeNuxt(); - expect(addTemplateMock).toHaveBeenCalledWith({ - filename: DEV_SERVER_CONFIG_PATH, - write: true, - getContents: expect.any(Function), - }); + addServerConfigPlugin(nuxt, APP_CONFIG); + + expect(templateContents('sentry-server-config-plugin.mjs')).toBe( + `import ${JSON.stringify(flagsDst)};\nimport ${JSON.stringify(APP_CONFIG)};\nexport default () => {};\n`, + ); + expect(addServerPluginMock).toHaveBeenCalledWith(pluginDst); + }); + + it('derives the runtime flags from the build-time `import.meta` values', () => { + const { nuxt } = createFakeNuxt(); + + addServerConfigPlugin(nuxt, APP_CONFIG); + + const contents = templateContents('sentry-runtime-flags.mjs'); + expect(contents).toContain('globalThis.__SENTRY_NUXT_DEV_MODE__ = import.meta.dev === true;'); + expect(contents).toContain('globalThis.__SENTRY_NUXT_PRERENDER__ = import.meta.prerender === true;'); }); - it('imports the user config as a file URL so Node can load it directly', () => { - expect(generate(APP_CONFIG)).toContain(`await import(${JSON.stringify(pathToFileURL(APP_CONFIG).href)})`); + it('marks the config and the flags as side-effectful so tree shaking cannot drop them', () => { + const { nuxt } = createFakeNuxt(); + nuxt.options.nitro.moduleSideEffects = ['unenv/polyfill/']; + + addServerConfigPlugin(nuxt, APP_CONFIG); + + expect(nuxt.options.nitro.moduleSideEffects).toEqual(['unenv/polyfill/', APP_CONFIG, flagsDst]); }); - it('sets the dev flag before importing the config', () => { - const contents = generate(APP_CONFIG); + it('inlines the config and both templates so the dev bundle transpiles them', () => { + const { nuxt, hooks } = createFakeNuxt(); + addServerConfigPlugin(nuxt, APP_CONFIG); + const nitroConfig: NitroConfig = { externals: { inline: ['@sentry/'] } }; + + hooks['nitro:config']!(nitroConfig); - // A static import would be hoisted above the assignment and `Sentry.init()` would then see no flag. - expect(contents).not.toMatch(/^import /m); - expect(contents.indexOf('__SENTRY_NUXT_DEV_MODE__')).toBeLessThan(contents.indexOf('await import(')); + expect(nitroConfig.externals?.inline).toEqual(['@sentry/', APP_CONFIG, pluginDst, flagsDst]); }); - it('catches a config Node cannot load, so a broken config does not stop the dev server', () => { - const contents = generate(APP_CONFIG); + it('moves its plugin to the front when other modules registered plugins first', () => { + const { nuxt, hooks } = createFakeNuxt(); + addServerConfigPlugin(nuxt, APP_CONFIG); + const nitroConfig: NitroConfig = { plugins: ['other-module-plugin.mjs', pluginDst] }; - expect(contents).toMatch(/try \{[\s\S]*await import\([\s\S]*\} catch \(error\) \{[\s\S]*console\.warn\(/); - expect(contents).toContain('Could not load `sentry.server.config.ts`'); + hooks['nitro:config']!(nitroConfig); + + expect(nitroConfig.plugins).toEqual([pluginDst, 'other-module-plugin.mjs']); }); - it('documents the command that preloads the file', () => { - expect(generate(APP_CONFIG)).toContain("NODE_OPTIONS='--import ./.nuxt/dev/sentry.server.config.mjs'"); + it('removes the plugin and warns on Cloudflare presets instead of importing the Node SDK into workerd', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const { nuxt, hooks } = createFakeNuxt(); + addServerConfigPlugin(nuxt, APP_CONFIG); + const nitroConfig: NitroConfig = { preset: 'cloudflare_module', plugins: ['other-plugin.mjs', pluginDst] }; + + hooks['nitro:config']!(nitroConfig); + + expect(nitroConfig.plugins).toEqual(['other-plugin.mjs']); + expect(nitroConfig.externals).toBeUndefined(); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('sentryCloudflareNitroPlugin')); + consoleWarnSpy.mockRestore(); }); +}); - describe('when the config comes from a layer outside the project root', () => { - it('imports the config from the layer it belongs to', () => { - expect(generate(LAYER_CONFIG)).toContain(`await import(${JSON.stringify(pathToFileURL(LAYER_CONFIG).href)})`); - }); +describe('addImportCompatShim', () => { + function createFakeNitro(options: { dev?: boolean; preset?: string }): { + nitro: Parameters[0]; + runCloseHook: () => Promise; + } { + const hooks: Record Promise> = {}; + const nitro = { + hooks: { + hook: (name: string, callback: () => Promise) => { + hooks[name] = callback; + }, + }, + options: { + dev: options.dev ?? false, + preset: options.preset ?? 'node-server', + output: { serverDir: `${APP_ROOT}/.output/server` }, + }, + } as unknown as Parameters[0]; + + return { nitro, runCloseHook: () => hooks['close']!() }; + } - it('keeps the preload path relative to the project root', () => { - // The file we generate always lives in the app's own build directory, wherever the config came from. - expect(generate(LAYER_CONFIG)).toContain("NODE_OPTIONS='--import ./.nuxt/dev/sentry.server.config.mjs'"); - }); + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('writes the shim to the former config path after the build', async () => { + const writeFileSpy = vi.spyOn(fs.promises, 'writeFile').mockResolvedValue(); + const { nitro, runCloseHook } = createFakeNitro({}); + + addServerConfigShimWithWarning(nitro); + await runCloseHook(); + + expect(writeFileSpy).toHaveBeenCalledWith( + `${APP_ROOT}/.output/server/sentry.server.config.mjs`, + expect.stringContaining('no longer needed'), + 'utf8', + ); + }); + + it('does not write the shim for dev servers', async () => { + const writeFileSpy = vi.spyOn(fs.promises, 'writeFile').mockResolvedValue(); + const { nitro, runCloseHook } = createFakeNitro({ dev: true }); + + addServerConfigShimWithWarning(nitro); + await runCloseHook(); + + expect(writeFileSpy).not.toHaveBeenCalled(); + }); + + it('does not write the shim into the prerenderer output', async () => { + const writeFileSpy = vi.spyOn(fs.promises, 'writeFile').mockResolvedValue(); + const { nitro, runCloseHook } = createFakeNitro({ preset: 'nitro-prerender' }); + + addServerConfigShimWithWarning(nitro); + await runCloseHook(); + + expect(writeFileSpy).not.toHaveBeenCalled(); + }); + + it('does not write the shim for Cloudflare presets', async () => { + const writeFileSpy = vi.spyOn(fs.promises, 'writeFile').mockResolvedValue(); + const { nitro, runCloseHook } = createFakeNitro({ preset: 'cloudflare_module' }); + + addServerConfigShimWithWarning(nitro); + await runCloseHook(); + + expect(writeFileSpy).not.toHaveBeenCalled(); }); }); From 830a18ab738fcd4ccc5b236b70d398ee35c86b98 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:14:51 +0200 Subject: [PATCH 8/8] add migration guide --- MIGRATION.md | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index e9e4910c6db3..72c662ca865c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -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`. @@ -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 }`).