From 572a8305d0631387dd3d93b1559a0e9e98aac5ad Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 12 Feb 2026 12:04:20 -0500 Subject: [PATCH 01/13] feat: sourcemaps upload and debug ID injection --- packages/nitro/package.json | 1 + packages/nitro/rollup.npm.config.mjs | 2 +- packages/nitro/src/config.ts | 58 ++++- packages/nitro/src/module.ts | 5 +- packages/nitro/src/sourceMaps.ts | 91 ++++++++ packages/nitro/test/sourceMaps.test.ts | 301 +++++++++++++++++++++++++ 6 files changed, 446 insertions(+), 12 deletions(-) create mode 100644 packages/nitro/src/sourceMaps.ts create mode 100644 packages/nitro/test/sourceMaps.test.ts diff --git a/packages/nitro/package.json b/packages/nitro/package.json index 2f5ee0d52fa8..db7632b6326d 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -38,6 +38,7 @@ "nitro": ">=3.0.0-0 <4.0.0 || 3.0.260311-beta || 3.0.260415-beta" }, "dependencies": { + "@sentry/bundler-plugin-core": "^5.2.0", "@sentry/core": "10.49.0", "@sentry/node": "10.49.0", "@sentry/opentelemetry": "10.49.0" diff --git a/packages/nitro/rollup.npm.config.mjs b/packages/nitro/rollup.npm.config.mjs index 1e41829a3a3a..3be46cb5f15b 100644 --- a/packages/nitro/rollup.npm.config.mjs +++ b/packages/nitro/rollup.npm.config.mjs @@ -5,7 +5,7 @@ export default [ makeBaseNPMConfig({ entrypoints: ['src/index.ts', 'src/runtime/plugins/server.ts'], packageSpecificConfig: { - external: [/^nitro/, /^h3/, /^srvx/, /^@sentry\/opentelemetry/], + external: [/^nitro/, /^h3/, /^srvx/, /^@sentry\/opentelemetry/, '@sentry/bundler-plugin-core'], }, }), ), diff --git a/packages/nitro/src/config.ts b/packages/nitro/src/config.ts index 219eb453fb18..7660464b4dc8 100644 --- a/packages/nitro/src/config.ts +++ b/packages/nitro/src/config.ts @@ -1,18 +1,31 @@ +import type { Options as SentryBundlerPluginOptions } from '@sentry/bundler-plugin-core'; +import { debug } from '@sentry/core'; import type { NitroConfig } from 'nitro/types'; import { createNitroModule } from './module'; -type SentryNitroOptions = { - // TODO: Add options -}; +export type SentryNitroOptions = Pick< + SentryBundlerPluginOptions, + | 'org' + | 'project' + | 'authToken' + | 'url' + | 'headers' + | 'debug' + | 'silent' + | 'errorHandler' + | 'telemetry' + | 'disable' + | 'sourcemaps' + | 'release' + | 'bundleSizeOptimizations' + | '_metaOptions' +>; /** * Modifies the passed in Nitro configuration with automatic build-time instrumentation. - * - * @param config A Nitro configuration object, as usually exported in `nitro.config.ts` or `nitro.config.mjs`. - * @returns The modified config to be exported */ -export function withSentryConfig(config: NitroConfig, moduleOptions?: SentryNitroOptions): NitroConfig { - return setupSentryNitroModule(config, moduleOptions); +export function withSentryConfig(config: NitroConfig, sentryOptions?: SentryNitroOptions): NitroConfig { + return setupSentryNitroModule(config, sentryOptions); } /** @@ -20,15 +33,40 @@ export function withSentryConfig(config: NitroConfig, moduleOptions?: SentryNitr */ export function setupSentryNitroModule( config: NitroConfig, - _moduleOptions?: SentryNitroOptions, + moduleOptions?: SentryNitroOptions, _serverConfigFile?: string, ): NitroConfig { if (!config.tracingChannel) { config.tracingChannel = true; } + const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true || moduleOptions?.disable === true; + + if (!sourcemapUploadDisabled) { + configureSourcemapSettings(config, moduleOptions); + } + config.modules = config.modules || []; - config.modules.push(createNitroModule()); + config.modules.push(createNitroModule(moduleOptions)); return config; } + +function configureSourcemapSettings(config: NitroConfig, moduleOptions?: SentryNitroOptions): void { + if (config.sourcemap === false) { + debug.warn( + '[Sentry] You have explicitly disabled source maps (`sourcemap: false`). Sentry is overriding this to `true` so that errors can be un-minified in Sentry. To disable Sentry source map uploads entirely, use `sourcemaps: { disable: true }` in your Sentry options instead.', + ); + } + config.sourcemap = true; + + // Nitro v3 has a `sourcemapMinify` plugin that destructively deletes `sourcesContent`, + // `x_google_ignoreList`, and clears `mappings` for any chunk containing `node_modules`. + // This makes sourcemaps unusable for Sentry. + config.experimental = config.experimental || {}; + config.experimental.sourcemapMinify = false; + + if (moduleOptions?.debug) { + debug.log('[Sentry] Enabled source map generation and configured build settings for Sentry source map uploads.'); + } +} diff --git a/packages/nitro/src/module.ts b/packages/nitro/src/module.ts index 1f0955301813..6cda58554a25 100644 --- a/packages/nitro/src/module.ts +++ b/packages/nitro/src/module.ts @@ -1,14 +1,17 @@ import type { NitroModule } from 'nitro/types'; +import type { SentryNitroOptions } from './config'; import { instrumentServer } from './instruments/instrumentServer'; +import { setupSourceMaps } from './sourceMaps'; /** * Creates a Nitro module to setup the Sentry SDK. */ -export function createNitroModule(): NitroModule { +export function createNitroModule(sentryOptions?: SentryNitroOptions): NitroModule { return { name: 'sentry', setup: nitro => { instrumentServer(nitro); + setupSourceMaps(nitro, sentryOptions); }, }; } diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts new file mode 100644 index 000000000000..c83652008bce --- /dev/null +++ b/packages/nitro/src/sourceMaps.ts @@ -0,0 +1,91 @@ +import type { Options } from '@sentry/bundler-plugin-core'; +import { createSentryBuildPluginManager } from '@sentry/bundler-plugin-core'; +import type { Nitro } from 'nitro/types'; +import type { SentryNitroOptions } from './config'; + +/** + * Registers a `compiled` hook to upload source maps after the build completes. + */ +export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions): void { + // The `compiled` hook fires on EVERY rebuild during `nitro dev` watch mode. + // nitro.options.dev is reliably set by the time module setup runs. + if (nitro.options.dev) { + return; + } + + // Respect user's explicit disable + if (options?.sourcemaps?.disable === true || options?.disable === true) { + return; + } + + nitro.hooks.hook('compiled', async (_nitro: Nitro) => { + await handleSourceMapUpload(_nitro, options); + }); +} + +/** + * Handles the actual source map upload after the build completes. + */ +async function handleSourceMapUpload(nitro: Nitro, options?: SentryNitroOptions): Promise { + const outputDir = nitro.options.output.serverDir; + const pluginOptions = getPluginOptions(options); + + const sentryBuildPluginManager = createSentryBuildPluginManager(pluginOptions, { + buildTool: 'nitro', + loggerPrefix: '[@sentry/nitro]', + }); + + await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); + await sentryBuildPluginManager.createRelease(); + + if (options?.sourcemaps?.disable !== 'disable-upload') { + await sentryBuildPluginManager.injectDebugIds([outputDir]); + await sentryBuildPluginManager.uploadSourcemaps([outputDir], { + // We don't prepare the artifacts because we injected debug IDs manually before + prepareArtifacts: false, + }); + } + + await sentryBuildPluginManager.deleteArtifacts(); +} + +/** + * Normalizes the beginning of a path from e.g. ../../../ to ./ + */ +function normalizePath(path: string): string { + return path.replace(/^(\.\.\/)+/, './'); +} + +/** + * Builds the plugin options for `createSentryBuildPluginManager` from the Sentry Nitro options. + * + * Only exported for testing purposes. + */ +export function getPluginOptions(options?: SentryNitroOptions): Options { + return { + org: options?.org ?? process.env.SENTRY_ORG, + project: options?.project ?? process.env.SENTRY_PROJECT, + authToken: options?.authToken ?? process.env.SENTRY_AUTH_TOKEN, + url: options?.url ?? process.env.SENTRY_URL, + headers: options?.headers, + telemetry: options?.telemetry ?? true, + debug: options?.debug ?? false, + silent: options?.silent ?? false, + errorHandler: options?.errorHandler, + sourcemaps: { + disable: options?.sourcemaps?.disable, + assets: options?.sourcemaps?.assets, + ignore: options?.sourcemaps?.ignore, + filesToDeleteAfterUpload: options?.sourcemaps?.filesToDeleteAfterUpload ?? ['**/*.map'], + rewriteSources: (source: string) => normalizePath(source), + }, + release: options?.release, + bundleSizeOptimizations: options?.bundleSizeOptimizations, + _metaOptions: { + telemetry: { + metaFramework: 'nitro', + }, + ...options?._metaOptions, + }, + }; +} diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts new file mode 100644 index 000000000000..1e7d5895c544 --- /dev/null +++ b/packages/nitro/test/sourceMaps.test.ts @@ -0,0 +1,301 @@ +import type { NitroConfig } from 'nitro/types'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { SentryNitroOptions } from '../src/config'; +import { setupSentryNitroModule } from '../src/config'; +import { getPluginOptions, setupSourceMaps } from '../src/sourceMaps'; + +vi.mock('../src/instruments/instrumentServer', () => ({ + instrumentServer: vi.fn(), +})); + +describe('getPluginOptions', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('returns default options when no options are provided', () => { + const options = getPluginOptions(); + + expect(options).toEqual( + expect.objectContaining({ + telemetry: true, + debug: false, + silent: false, + sourcemaps: expect.objectContaining({ + filesToDeleteAfterUpload: ['**/*.map'], + rewriteSources: expect.any(Function), + }), + _metaOptions: expect.objectContaining({ + telemetry: expect.objectContaining({ + metaFramework: 'nitro', + }), + }), + }), + ); + expect(options.org).toBeUndefined(); + expect(options.project).toBeUndefined(); + expect(options.authToken).toBeUndefined(); + expect(options.url).toBeUndefined(); + }); + + it('uses environment variables as fallback', () => { + process.env.SENTRY_ORG = 'env-org'; + process.env.SENTRY_PROJECT = 'env-project'; + process.env.SENTRY_AUTH_TOKEN = 'env-token'; + process.env.SENTRY_URL = 'https://custom.sentry.io'; + + const options = getPluginOptions(); + + expect(options.org).toBe('env-org'); + expect(options.project).toBe('env-project'); + expect(options.authToken).toBe('env-token'); + expect(options.url).toBe('https://custom.sentry.io'); + }); + + it('prefers direct options over environment variables', () => { + process.env.SENTRY_ORG = 'env-org'; + process.env.SENTRY_AUTH_TOKEN = 'env-token'; + + const options = getPluginOptions({ + org: 'direct-org', + authToken: 'direct-token', + }); + + expect(options.org).toBe('direct-org'); + expect(options.authToken).toBe('direct-token'); + }); + + it('passes through all user options', () => { + const sentryOptions: SentryNitroOptions = { + org: 'my-org', + project: 'my-project', + authToken: 'my-token', + url: 'https://my-sentry.io', + headers: { 'X-Custom': 'header' }, + debug: true, + silent: true, + telemetry: false, + errorHandler: () => {}, + release: { name: 'v1.0.0' }, + sourcemaps: { + assets: ['dist/**'], + ignore: ['dist/test/**'], + filesToDeleteAfterUpload: ['dist/**/*.map'], + }, + }; + + const options = getPluginOptions(sentryOptions); + + expect(options.org).toBe('my-org'); + expect(options.project).toBe('my-project'); + expect(options.authToken).toBe('my-token'); + expect(options.url).toBe('https://my-sentry.io'); + expect(options.headers).toEqual({ 'X-Custom': 'header' }); + expect(options.debug).toBe(true); + expect(options.silent).toBe(true); + expect(options.telemetry).toBe(false); + expect(options.errorHandler).toBeDefined(); + expect(options.release).toEqual({ name: 'v1.0.0' }); + expect(options.sourcemaps?.assets).toEqual(['dist/**']); + expect(options.sourcemaps?.ignore).toEqual(['dist/test/**']); + expect(options.sourcemaps?.filesToDeleteAfterUpload).toEqual(['dist/**/*.map']); + }); + + it('normalizes source paths via rewriteSources', () => { + const options = getPluginOptions(); + const rewriteSources = options.sourcemaps?.rewriteSources; + + expect(rewriteSources?.('../../../src/index.ts', undefined)).toBe('./src/index.ts'); + expect(rewriteSources?.('../../lib/utils.ts', undefined)).toBe('./lib/utils.ts'); + expect(rewriteSources?.('./src/index.ts', undefined)).toBe('./src/index.ts'); + expect(rewriteSources?.('src/index.ts', undefined)).toBe('src/index.ts'); + }); + + it('always sets metaFramework to nitro', () => { + const options = getPluginOptions({ _metaOptions: { loggerPrefixOverride: '[custom]' } }); + + expect(options._metaOptions?.telemetry?.metaFramework).toBe('nitro'); + expect(options._metaOptions?.loggerPrefixOverride).toBe('[custom]'); + }); + + it('passes through sourcemaps.disable', () => { + const options = getPluginOptions({ sourcemaps: { disable: 'disable-upload' } }); + + expect(options.sourcemaps?.disable).toBe('disable-upload'); + }); +}); + +describe('setupSentryNitroModule', () => { + it('enables sourcemap generation on the config', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config); + + expect(config.sourcemap).toBe(true); + }); + + it('forces sourcemap to true even when user set it to false', () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const config: NitroConfig = { sourcemap: false }; + setupSentryNitroModule(config); + + expect(config.sourcemap).toBe(true); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('overriding this to `true`')); + consoleSpy.mockRestore(); + }); + + it('keeps sourcemap true when user already set it', () => { + const config: NitroConfig = { sourcemap: true }; + setupSentryNitroModule(config); + + expect(config.sourcemap).toBe(true); + }); + + it('disables experimental sourcemapMinify', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config); + + expect(config.experimental?.sourcemapMinify).toBe(false); + }); + + it('sets sourcemapExcludeSources to false in rollupConfig', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config); + + expect(config.rollupConfig?.output?.sourcemapExcludeSources).toBe(false); + }); + + it('preserves existing experimental config', () => { + const config: NitroConfig = { + experimental: { + sourcemapMinify: undefined, + }, + }; + setupSentryNitroModule(config); + + expect(config.experimental?.sourcemapMinify).toBe(false); + }); + + it('preserves existing rollupConfig', () => { + const config: NitroConfig = { + rollupConfig: { + output: { + format: 'esm' as const, + }, + }, + }; + setupSentryNitroModule(config); + + expect(config.rollupConfig?.output?.format).toBe('esm'); + expect(config.rollupConfig?.output?.sourcemapExcludeSources).toBe(false); + }); + + it('skips sourcemap config when sourcemaps.disable is true', () => { + const config: NitroConfig = { sourcemap: false }; + setupSentryNitroModule(config, { sourcemaps: { disable: true } }); + + // Should NOT override the user's sourcemap: false + expect(config.sourcemap).toBe(false); + expect(config.rollupConfig).toBeUndefined(); + }); + + it('skips sourcemap config when disable is true', () => { + const config: NitroConfig = { sourcemap: false }; + setupSentryNitroModule(config, { disable: true }); + + // Should NOT override the user's sourcemap: false + expect(config.sourcemap).toBe(false); + expect(config.rollupConfig).toBeUndefined(); + }); + + it('still adds module when sourcemaps are disabled', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config, { sourcemaps: { disable: true } }); + + expect(config.modules).toBeDefined(); + expect(config.modules?.length).toBe(1); + }); + + it('enables tracing', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config); + + // @ts-expect-error -- Nitro tracing config is not out yet + expect(config.tracing).toBe(true); + }); + + it('adds the sentry module', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config); + + expect(config.modules).toBeDefined(); + expect(config.modules?.length).toBe(1); + }); +}); + +describe('setupSourceMaps', () => { + it('does not register hook in dev mode', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: true, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro); + + expect(hookFn).not.toHaveBeenCalled(); + }); + + it('does not register hook when sourcemaps.disable is true', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro, { sourcemaps: { disable: true } }); + + expect(hookFn).not.toHaveBeenCalled(); + }); + + it('does not register hook when disable is true', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro, { disable: true }); + + expect(hookFn).not.toHaveBeenCalled(); + }); + + it('registers compiled hook in production mode', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro); + + expect(hookFn).toHaveBeenCalledWith('compiled', expect.any(Function)); + }); + + it('registers compiled hook with custom options', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro, { org: 'my-org', project: 'my-project' }); + + expect(hookFn).toHaveBeenCalledWith('compiled', expect.any(Function)); + }); +}); From d3b0f0f12b8d9ad0a7c7d6f4b0ea2a1449dfe56c Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 12 Feb 2026 12:09:23 -0500 Subject: [PATCH 02/13] ref: minor refactor --- packages/nitro/src/config.ts | 27 ++------------------------- packages/nitro/src/sourceMaps.ts | 30 +++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/nitro/src/config.ts b/packages/nitro/src/config.ts index 7660464b4dc8..cbc980f35f57 100644 --- a/packages/nitro/src/config.ts +++ b/packages/nitro/src/config.ts @@ -1,7 +1,7 @@ import type { Options as SentryBundlerPluginOptions } from '@sentry/bundler-plugin-core'; -import { debug } from '@sentry/core'; import type { NitroConfig } from 'nitro/types'; import { createNitroModule } from './module'; +import { configureSourcemapSettings } from './sourceMaps'; export type SentryNitroOptions = Pick< SentryBundlerPluginOptions, @@ -40,33 +40,10 @@ export function setupSentryNitroModule( config.tracingChannel = true; } - const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true || moduleOptions?.disable === true; - - if (!sourcemapUploadDisabled) { - configureSourcemapSettings(config, moduleOptions); - } + configureSourcemapSettings(config, moduleOptions); config.modules = config.modules || []; config.modules.push(createNitroModule(moduleOptions)); return config; } - -function configureSourcemapSettings(config: NitroConfig, moduleOptions?: SentryNitroOptions): void { - if (config.sourcemap === false) { - debug.warn( - '[Sentry] You have explicitly disabled source maps (`sourcemap: false`). Sentry is overriding this to `true` so that errors can be un-minified in Sentry. To disable Sentry source map uploads entirely, use `sourcemaps: { disable: true }` in your Sentry options instead.', - ); - } - config.sourcemap = true; - - // Nitro v3 has a `sourcemapMinify` plugin that destructively deletes `sourcesContent`, - // `x_google_ignoreList`, and clears `mappings` for any chunk containing `node_modules`. - // This makes sourcemaps unusable for Sentry. - config.experimental = config.experimental || {}; - config.experimental.sourcemapMinify = false; - - if (moduleOptions?.debug) { - debug.log('[Sentry] Enabled source map generation and configured build settings for Sentry source map uploads.'); - } -} diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index c83652008bce..37dd7476084e 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -1,6 +1,7 @@ import type { Options } from '@sentry/bundler-plugin-core'; import { createSentryBuildPluginManager } from '@sentry/bundler-plugin-core'; -import type { Nitro } from 'nitro/types'; +import { debug } from '@sentry/core'; +import type { Nitro, NitroConfig } from 'nitro/types'; import type { SentryNitroOptions } from './config'; /** @@ -89,3 +90,30 @@ export function getPluginOptions(options?: SentryNitroOptions): Options { }, }; } + +/** + * Configures the Nitro config to enable source map generation. + */ +export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: SentryNitroOptions): void { + const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true || moduleOptions?.disable === true; + if (!sourcemapUploadDisabled) { + return; + } + + if (config.sourcemap === false) { + debug.warn( + '[Sentry] You have explicitly disabled source maps (`sourcemap: false`). Sentry is overriding this to `true` so that errors can be un-minified in Sentry. To disable Sentry source map uploads entirely, use `sourcemaps: { disable: true }` in your Sentry options instead.', + ); + } + config.sourcemap = true; + + // Nitro v3 has a `sourcemapMinify` plugin that destructively deletes `sourcesContent`, + // `x_google_ignoreList`, and clears `mappings` for any chunk containing `node_modules`. + // This makes sourcemaps unusable for Sentry. + config.experimental = config.experimental || {}; + config.experimental.sourcemapMinify = false; + + if (moduleOptions?.debug) { + debug.log('[Sentry] Enabled source map generation and configured build settings for Sentry source map uploads.'); + } +} From 06d6b677344ecb116663661543ed1ede624e4ebb Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 12 Feb 2026 12:30:21 -0500 Subject: [PATCH 03/13] fix: boolean oopsie --- packages/nitro/src/sourceMaps.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index 37dd7476084e..a24b17926a0c 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -96,7 +96,7 @@ export function getPluginOptions(options?: SentryNitroOptions): Options { */ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: SentryNitroOptions): void { const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true || moduleOptions?.disable === true; - if (!sourcemapUploadDisabled) { + if (sourcemapUploadDisabled) { return; } @@ -105,11 +105,13 @@ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: '[Sentry] You have explicitly disabled source maps (`sourcemap: false`). Sentry is overriding this to `true` so that errors can be un-minified in Sentry. To disable Sentry source map uploads entirely, use `sourcemaps: { disable: true }` in your Sentry options instead.', ); } + config.sourcemap = true; // Nitro v3 has a `sourcemapMinify` plugin that destructively deletes `sourcesContent`, // `x_google_ignoreList`, and clears `mappings` for any chunk containing `node_modules`. // This makes sourcemaps unusable for Sentry. + // FIXME: Not sure about this one, it works either way? config.experimental = config.experimental || {}; config.experimental.sourcemapMinify = false; From 51175703f5e40aa00064b453e31a911e26a11503 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 12 Feb 2026 12:37:11 -0500 Subject: [PATCH 04/13] test: update test expectations --- packages/nitro/test/sourceMaps.test.ts | 68 +++++++++----------------- 1 file changed, 23 insertions(+), 45 deletions(-) diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts index 1e7d5895c544..1117dd2b2208 100644 --- a/packages/nitro/test/sourceMaps.test.ts +++ b/packages/nitro/test/sourceMaps.test.ts @@ -1,8 +1,9 @@ +import { debug } from '@sentry/core'; import type { NitroConfig } from 'nitro/types'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { SentryNitroOptions } from '../src/config'; import { setupSentryNitroModule } from '../src/config'; -import { getPluginOptions, setupSourceMaps } from '../src/sourceMaps'; +import { configureSourcemapSettings, getPluginOptions, setupSourceMaps } from '../src/sourceMaps'; vi.mock('../src/instruments/instrumentServer', () => ({ instrumentServer: vi.fn(), @@ -131,96 +132,65 @@ describe('getPluginOptions', () => { }); }); -describe('setupSentryNitroModule', () => { +describe('configureSourcemapSettings', () => { it('enables sourcemap generation on the config', () => { const config: NitroConfig = {}; - setupSentryNitroModule(config); + configureSourcemapSettings(config); expect(config.sourcemap).toBe(true); }); it('forces sourcemap to true even when user set it to false', () => { - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const debugSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const config: NitroConfig = { sourcemap: false }; - setupSentryNitroModule(config); + configureSourcemapSettings(config); expect(config.sourcemap).toBe(true); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('overriding this to `true`')); - consoleSpy.mockRestore(); + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('overriding this to `true`')); + debugSpy.mockRestore(); }); it('keeps sourcemap true when user already set it', () => { const config: NitroConfig = { sourcemap: true }; - setupSentryNitroModule(config); + configureSourcemapSettings(config); expect(config.sourcemap).toBe(true); }); it('disables experimental sourcemapMinify', () => { const config: NitroConfig = {}; - setupSentryNitroModule(config); + configureSourcemapSettings(config); expect(config.experimental?.sourcemapMinify).toBe(false); }); - it('sets sourcemapExcludeSources to false in rollupConfig', () => { - const config: NitroConfig = {}; - setupSentryNitroModule(config); - - expect(config.rollupConfig?.output?.sourcemapExcludeSources).toBe(false); - }); - it('preserves existing experimental config', () => { const config: NitroConfig = { experimental: { sourcemapMinify: undefined, }, }; - setupSentryNitroModule(config); + configureSourcemapSettings(config); expect(config.experimental?.sourcemapMinify).toBe(false); }); - it('preserves existing rollupConfig', () => { - const config: NitroConfig = { - rollupConfig: { - output: { - format: 'esm' as const, - }, - }, - }; - setupSentryNitroModule(config); - - expect(config.rollupConfig?.output?.format).toBe('esm'); - expect(config.rollupConfig?.output?.sourcemapExcludeSources).toBe(false); - }); - it('skips sourcemap config when sourcemaps.disable is true', () => { const config: NitroConfig = { sourcemap: false }; - setupSentryNitroModule(config, { sourcemaps: { disable: true } }); + configureSourcemapSettings(config, { sourcemaps: { disable: true } }); - // Should NOT override the user's sourcemap: false expect(config.sourcemap).toBe(false); - expect(config.rollupConfig).toBeUndefined(); }); it('skips sourcemap config when disable is true', () => { const config: NitroConfig = { sourcemap: false }; - setupSentryNitroModule(config, { disable: true }); + configureSourcemapSettings(config, { disable: true }); - // Should NOT override the user's sourcemap: false expect(config.sourcemap).toBe(false); - expect(config.rollupConfig).toBeUndefined(); - }); - - it('still adds module when sourcemaps are disabled', () => { - const config: NitroConfig = {}; - setupSentryNitroModule(config, { sourcemaps: { disable: true } }); - - expect(config.modules).toBeDefined(); - expect(config.modules?.length).toBe(1); }); +}); +describe('setupSentryNitroModule', () => { it('enables tracing', () => { const config: NitroConfig = {}; setupSentryNitroModule(config); @@ -236,6 +206,14 @@ describe('setupSentryNitroModule', () => { expect(config.modules).toBeDefined(); expect(config.modules?.length).toBe(1); }); + + it('still adds module when sourcemaps are disabled', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config, { sourcemaps: { disable: true } }); + + expect(config.modules).toBeDefined(); + expect(config.modules?.length).toBe(1); + }); }); describe('setupSourceMaps', () => { From ea8606ee0d3634cea4630c93e6b6e41bb5fe040e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Apr 2026 10:58:23 -0400 Subject: [PATCH 05/13] fix: revert change --- packages/nitro/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nitro/package.json b/packages/nitro/package.json index db7632b6326d..54dab47a7157 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -44,8 +44,8 @@ "@sentry/opentelemetry": "10.49.0" }, "devDependencies": { - "h3": "^2.0.1-rc.13", - "nitro": "^3.0.260415-beta" + "nitro": "^3.0.260415-beta", + "h3": "^2.0.1-rc.13" }, "scripts": { "build": "run-p build:transpile build:types", From d114ac357a9f767bbdafc8bf249d215fa702d515 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Apr 2026 13:58:20 -0400 Subject: [PATCH 06/13] test: fix unit test assertion --- packages/nitro/test/sourceMaps.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts index 1117dd2b2208..2ee2907df69b 100644 --- a/packages/nitro/test/sourceMaps.test.ts +++ b/packages/nitro/test/sourceMaps.test.ts @@ -195,8 +195,7 @@ describe('setupSentryNitroModule', () => { const config: NitroConfig = {}; setupSentryNitroModule(config); - // @ts-expect-error -- Nitro tracing config is not out yet - expect(config.tracing).toBe(true); + expect(config.tracingChannel).toBe(true); }); it('adds the sentry module', () => { From af913040a0b4c0aec8932353be7acea401128276 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 16 Apr 2026 09:48:01 -0400 Subject: [PATCH 07/13] fix(nitro): Preserve debug IDs and source maps when `disable-upload` is set When `sourcemaps.disable` is `'disable-upload'`, the plugin should still inject debug IDs and keep `.map` files so users can upload them manually. Previously, both `injectDebugIds()` and `deleteArtifacts()` were incorrectly gated or ungated, causing debug IDs to be skipped and source maps to be deleted even in the manual-upload workflow. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/nitro/src/sourceMaps.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index a24b17926a0c..1fec4ded77dc 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -39,15 +39,15 @@ async function handleSourceMapUpload(nitro: Nitro, options?: SentryNitroOptions) await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); await sentryBuildPluginManager.createRelease(); + await sentryBuildPluginManager.injectDebugIds([outputDir]); + if (options?.sourcemaps?.disable !== 'disable-upload') { - await sentryBuildPluginManager.injectDebugIds([outputDir]); await sentryBuildPluginManager.uploadSourcemaps([outputDir], { // We don't prepare the artifacts because we injected debug IDs manually before prepareArtifacts: false, }); + await sentryBuildPluginManager.deleteArtifacts(); } - - await sentryBuildPluginManager.deleteArtifacts(); } /** From 4a4a87324fea800c6b7b89b02b97414e5c22e80e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 16 Apr 2026 09:53:48 -0400 Subject: [PATCH 08/13] ref(nitro): Use `BuildTimeOptionsBase` for `SentryNitroOptions` type Derive `SentryNitroOptions` from `BuildTimeOptionsBase` (from `@sentry/core`) instead of picking from the bundler plugin's internal `Options` type. This provides a stable public API that follows this repo's semver, rather than exposing bundler-plugin-specific fields like `_metaOptions` and top-level `disable`. Key changes: - `url` option renamed to `sentryUrl` (consistent with other SDKs) - Top-level `disable` removed (use `sourcemaps.disable` instead) - `_metaOptions` no longer user-configurable Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/nitro/src/config.ts | 20 ++--------------- packages/nitro/src/sourceMaps.ts | 11 +++++----- packages/nitro/test/sourceMaps.test.ts | 30 +++++++++----------------- 3 files changed, 17 insertions(+), 44 deletions(-) diff --git a/packages/nitro/src/config.ts b/packages/nitro/src/config.ts index cbc980f35f57..12eb3b658e95 100644 --- a/packages/nitro/src/config.ts +++ b/packages/nitro/src/config.ts @@ -1,25 +1,9 @@ -import type { Options as SentryBundlerPluginOptions } from '@sentry/bundler-plugin-core'; +import type { BuildTimeOptionsBase } from '@sentry/core'; import type { NitroConfig } from 'nitro/types'; import { createNitroModule } from './module'; import { configureSourcemapSettings } from './sourceMaps'; -export type SentryNitroOptions = Pick< - SentryBundlerPluginOptions, - | 'org' - | 'project' - | 'authToken' - | 'url' - | 'headers' - | 'debug' - | 'silent' - | 'errorHandler' - | 'telemetry' - | 'disable' - | 'sourcemaps' - | 'release' - | 'bundleSizeOptimizations' - | '_metaOptions' ->; +export type SentryNitroOptions = BuildTimeOptionsBase; /** * Modifies the passed in Nitro configuration with automatic build-time instrumentation. diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index 1fec4ded77dc..00acd0366857 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -1,4 +1,4 @@ -import type { Options } from '@sentry/bundler-plugin-core'; +import type { Options as BundlerPluginOptions } from '@sentry/bundler-plugin-core'; import { createSentryBuildPluginManager } from '@sentry/bundler-plugin-core'; import { debug } from '@sentry/core'; import type { Nitro, NitroConfig } from 'nitro/types'; @@ -15,7 +15,7 @@ export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions): voi } // Respect user's explicit disable - if (options?.sourcemaps?.disable === true || options?.disable === true) { + if (options?.sourcemaps?.disable === true) { return; } @@ -62,12 +62,12 @@ function normalizePath(path: string): string { * * Only exported for testing purposes. */ -export function getPluginOptions(options?: SentryNitroOptions): Options { +export function getPluginOptions(options?: SentryNitroOptions): BundlerPluginOptions { return { org: options?.org ?? process.env.SENTRY_ORG, project: options?.project ?? process.env.SENTRY_PROJECT, authToken: options?.authToken ?? process.env.SENTRY_AUTH_TOKEN, - url: options?.url ?? process.env.SENTRY_URL, + url: options?.sentryUrl ?? process.env.SENTRY_URL, headers: options?.headers, telemetry: options?.telemetry ?? true, debug: options?.debug ?? false, @@ -86,7 +86,6 @@ export function getPluginOptions(options?: SentryNitroOptions): Options { telemetry: { metaFramework: 'nitro', }, - ...options?._metaOptions, }, }; } @@ -95,7 +94,7 @@ export function getPluginOptions(options?: SentryNitroOptions): Options { * Configures the Nitro config to enable source map generation. */ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: SentryNitroOptions): void { - const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true || moduleOptions?.disable === true; + const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true; if (sourcemapUploadDisabled) { return; } diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts index 2ee2907df69b..806f1dff793d 100644 --- a/packages/nitro/test/sourceMaps.test.ts +++ b/packages/nitro/test/sourceMaps.test.ts @@ -56,20 +56,23 @@ describe('getPluginOptions', () => { expect(options.org).toBe('env-org'); expect(options.project).toBe('env-project'); expect(options.authToken).toBe('env-token'); - expect(options.url).toBe('https://custom.sentry.io'); + expect(options.url).toBe('https://custom.sentry.io'); // sentryUrl maps to url }); it('prefers direct options over environment variables', () => { process.env.SENTRY_ORG = 'env-org'; process.env.SENTRY_AUTH_TOKEN = 'env-token'; + process.env.SENTRY_URL = 'https://env.sentry.io'; const options = getPluginOptions({ org: 'direct-org', authToken: 'direct-token', + sentryUrl: 'https://direct.sentry.io', }); expect(options.org).toBe('direct-org'); expect(options.authToken).toBe('direct-token'); + expect(options.url).toBe('https://direct.sentry.io'); }); it('passes through all user options', () => { @@ -77,7 +80,7 @@ describe('getPluginOptions', () => { org: 'my-org', project: 'my-project', authToken: 'my-token', - url: 'https://my-sentry.io', + sentryUrl: 'https://my-sentry.io', headers: { 'X-Custom': 'header' }, debug: true, silent: true, @@ -119,10 +122,9 @@ describe('getPluginOptions', () => { }); it('always sets metaFramework to nitro', () => { - const options = getPluginOptions({ _metaOptions: { loggerPrefixOverride: '[custom]' } }); + const options = getPluginOptions(); expect(options._metaOptions?.telemetry?.metaFramework).toBe('nitro'); - expect(options._metaOptions?.loggerPrefixOverride).toBe('[custom]'); }); it('passes through sourcemaps.disable', () => { @@ -182,11 +184,11 @@ describe('configureSourcemapSettings', () => { expect(config.sourcemap).toBe(false); }); - it('skips sourcemap config when disable is true', () => { - const config: NitroConfig = { sourcemap: false }; - configureSourcemapSettings(config, { disable: true }); + it('still configures sourcemaps when sourcemaps.disable is disable-upload', () => { + const config: NitroConfig = {}; + configureSourcemapSettings(config, { sourcemaps: { disable: 'disable-upload' } }); - expect(config.sourcemap).toBe(false); + expect(config.sourcemap).toBe(true); }); }); @@ -240,18 +242,6 @@ describe('setupSourceMaps', () => { expect(hookFn).not.toHaveBeenCalled(); }); - it('does not register hook when disable is true', () => { - const hookFn = vi.fn(); - const nitro = { - options: { dev: false, output: { serverDir: '/output/server' } }, - hooks: { hook: hookFn }, - } as any; - - setupSourceMaps(nitro, { disable: true }); - - expect(hookFn).not.toHaveBeenCalled(); - }); - it('registers compiled hook in production mode', () => { const hookFn = vi.fn(); const nitro = { From 2f9347246f75c05915f64f9ff163dfce44a2037b Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 16 Apr 2026 09:54:38 -0400 Subject: [PATCH 09/13] fix(nitro): Respect user's explicit sourcemap setting instead of forcing `true` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, `configureSourcemapSettings` unconditionally set `config.sourcemap = true`, overriding the user's explicit `false`. This could expose source maps publicly when users intentionally disabled them. Now follows the same 3-case pattern as other meta-framework SDKs (Nuxt): 1. User disabled (false) → keep their setting, warn about unminified errors 2. User enabled (true) → keep their setting 3. User didn't set (undefined) → enable for Sentry Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/nitro/src/sourceMaps.ts | 33 ++++++++++++++++++-------- packages/nitro/test/sourceMaps.test.ts | 15 +++++++++--- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index 00acd0366857..7d3b4bbd18f7 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -90,9 +90,16 @@ export function getPluginOptions(options?: SentryNitroOptions): BundlerPluginOpt }; } -/** - * Configures the Nitro config to enable source map generation. - */ +/* Source map configuration rules: + 1. User explicitly disabled source maps (sourcemap: false) + - Keep their setting, emit a warning that errors won't be unminified in Sentry + - We will not upload anything + 2. User enabled source map generation (true) + - Keep their setting (don't modify besides uploading) + 3. User did not set source maps (undefined) + - We enable source maps for Sentry + - Configure `filesToDeleteAfterUpload` to clean up .map files after upload +*/ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: SentryNitroOptions): void { const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true; if (sourcemapUploadDisabled) { @@ -101,20 +108,26 @@ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: if (config.sourcemap === false) { debug.warn( - '[Sentry] You have explicitly disabled source maps (`sourcemap: false`). Sentry is overriding this to `true` so that errors can be un-minified in Sentry. To disable Sentry source map uploads entirely, use `sourcemaps: { disable: true }` in your Sentry options instead.', + '[Sentry] You have explicitly disabled source maps (`sourcemap: false`). Sentry will not upload source maps, and errors will not be unminified. To let Sentry handle source maps, remove the `sourcemap` option from your Nitro config, or use `sourcemaps: { disable: true }` in your Sentry options to silence this warning.', ); + return; } - config.sourcemap = true; + if (config.sourcemap === true) { + if (moduleOptions?.debug) { + debug.log('[Sentry] Source maps are already enabled. Sentry will upload them for error unminification.'); + } + } else { + // User did not explicitly set sourcemap — enable it for Sentry + config.sourcemap = true; + if (moduleOptions?.debug) { + debug.log('[Sentry] Enabled source map generation for Sentry. Source map files will be deleted after upload.'); + } + } // Nitro v3 has a `sourcemapMinify` plugin that destructively deletes `sourcesContent`, // `x_google_ignoreList`, and clears `mappings` for any chunk containing `node_modules`. // This makes sourcemaps unusable for Sentry. - // FIXME: Not sure about this one, it works either way? config.experimental = config.experimental || {}; config.experimental.sourcemapMinify = false; - - if (moduleOptions?.debug) { - debug.log('[Sentry] Enabled source map generation and configured build settings for Sentry source map uploads.'); - } } diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts index 806f1dff793d..0e5075224668 100644 --- a/packages/nitro/test/sourceMaps.test.ts +++ b/packages/nitro/test/sourceMaps.test.ts @@ -142,16 +142,25 @@ describe('configureSourcemapSettings', () => { expect(config.sourcemap).toBe(true); }); - it('forces sourcemap to true even when user set it to false', () => { + it('respects user explicitly disabling sourcemaps and warns', () => { const debugSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const config: NitroConfig = { sourcemap: false }; configureSourcemapSettings(config); - expect(config.sourcemap).toBe(true); - expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('overriding this to `true`')); + expect(config.sourcemap).toBe(false); + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('explicitly disabled source maps')); debugSpy.mockRestore(); }); + it('does not modify experimental config when user disabled sourcemaps', () => { + vi.spyOn(debug, 'warn').mockImplementation(() => {}); + const config: NitroConfig = { sourcemap: false }; + configureSourcemapSettings(config); + + expect(config.experimental).toBeUndefined(); + vi.restoreAllMocks(); + }); + it('keeps sourcemap true when user already set it', () => { const config: NitroConfig = { sourcemap: true }; configureSourcemapSettings(config); From 41954647009d5b302b050743fd79f70abf81e2d0 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 16 Apr 2026 10:21:26 -0400 Subject: [PATCH 10/13] fix(nitro): Use console instead of debug logger for build-time messages `configureSourcemapSettings` runs at build time, but the `debug` logger from `@sentry/core` is only initialized by `Sentry.init()` at runtime. Switch to `console.warn`/`console.log` so build-time warnings (e.g. source maps explicitly disabled) are actually visible to developers. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/nitro/src/sourceMaps.ts | 14 +++++++++----- packages/nitro/test/sourceMaps.test.ts | 9 ++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index 7d3b4bbd18f7..6e4a9b08fd5b 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -1,6 +1,5 @@ import type { Options as BundlerPluginOptions } from '@sentry/bundler-plugin-core'; import { createSentryBuildPluginManager } from '@sentry/bundler-plugin-core'; -import { debug } from '@sentry/core'; import type { Nitro, NitroConfig } from 'nitro/types'; import type { SentryNitroOptions } from './config'; @@ -107,21 +106,26 @@ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: } if (config.sourcemap === false) { - debug.warn( - '[Sentry] You have explicitly disabled source maps (`sourcemap: false`). Sentry will not upload source maps, and errors will not be unminified. To let Sentry handle source maps, remove the `sourcemap` option from your Nitro config, or use `sourcemaps: { disable: true }` in your Sentry options to silence this warning.', + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nitro] You have explicitly disabled source maps (`sourcemap: false`). Sentry will not upload source maps, and errors will not be unminified. To let Sentry handle source maps, remove the `sourcemap` option from your Nitro config, or use `sourcemaps: { disable: true }` in your Sentry options to silence this warning.', ); return; } if (config.sourcemap === true) { if (moduleOptions?.debug) { - debug.log('[Sentry] Source maps are already enabled. Sentry will upload them for error unminification.'); + // eslint-disable-next-line no-console + console.log('[@sentry/nitro] Source maps are already enabled. Sentry will upload them for error unminification.'); } } else { // User did not explicitly set sourcemap — enable it for Sentry config.sourcemap = true; if (moduleOptions?.debug) { - debug.log('[Sentry] Enabled source map generation for Sentry. Source map files will be deleted after upload.'); + // eslint-disable-next-line no-console + console.log( + '[@sentry/nitro] Enabled source map generation for Sentry. Source map files will be deleted after upload.', + ); } } diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts index 0e5075224668..a2b531704599 100644 --- a/packages/nitro/test/sourceMaps.test.ts +++ b/packages/nitro/test/sourceMaps.test.ts @@ -1,4 +1,3 @@ -import { debug } from '@sentry/core'; import type { NitroConfig } from 'nitro/types'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { SentryNitroOptions } from '../src/config'; @@ -143,17 +142,17 @@ describe('configureSourcemapSettings', () => { }); it('respects user explicitly disabling sourcemaps and warns', () => { - const debugSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const config: NitroConfig = { sourcemap: false }; configureSourcemapSettings(config); expect(config.sourcemap).toBe(false); - expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('explicitly disabled source maps')); - debugSpy.mockRestore(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('explicitly disabled source maps')); + warnSpy.mockRestore(); }); it('does not modify experimental config when user disabled sourcemaps', () => { - vi.spyOn(debug, 'warn').mockImplementation(() => {}); + vi.spyOn(console, 'warn').mockImplementation(() => {}); const config: NitroConfig = { sourcemap: false }; configureSourcemapSettings(config); From 80bc2ee9eb1c89a8f846ac84378a494152eff36d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 16 Apr 2026 10:25:06 -0400 Subject: [PATCH 11/13] fix(nitro): Respect user-provided `rewriteSources` option The `rewriteSources` option was hardcoded to `normalizePath`, silently discarding any custom function provided by the user. Now falls back to `normalizePath` only when the user doesn't provide their own function, matching the behavior of other meta-framework SDKs (Nuxt, Next.js). Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/nitro/src/sourceMaps.ts | 2 +- packages/nitro/test/sourceMaps.test.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index 6e4a9b08fd5b..745886cc57f4 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -77,7 +77,7 @@ export function getPluginOptions(options?: SentryNitroOptions): BundlerPluginOpt assets: options?.sourcemaps?.assets, ignore: options?.sourcemaps?.ignore, filesToDeleteAfterUpload: options?.sourcemaps?.filesToDeleteAfterUpload ?? ['**/*.map'], - rewriteSources: (source: string) => normalizePath(source), + rewriteSources: options?.sourcemaps?.rewriteSources ?? ((source: string) => normalizePath(source)), }, release: options?.release, bundleSizeOptimizations: options?.bundleSizeOptimizations, diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts index a2b531704599..588a52fcd42d 100644 --- a/packages/nitro/test/sourceMaps.test.ts +++ b/packages/nitro/test/sourceMaps.test.ts @@ -120,6 +120,15 @@ describe('getPluginOptions', () => { expect(rewriteSources?.('src/index.ts', undefined)).toBe('src/index.ts'); }); + it('uses user-provided rewriteSources when given', () => { + const customRewrite = (source: string) => `/custom/${source}`; + const options = getPluginOptions({ sourcemaps: { rewriteSources: customRewrite } }); + + expect(options.sourcemaps?.rewriteSources?.('../../../src/index.ts', undefined)).toBe( + '/custom/../../../src/index.ts', + ); + }); + it('always sets metaFramework to nitro', () => { const options = getPluginOptions(); From fb181b59029f73d8bc3335d2f731b160e9a185df Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 17 Apr 2026 12:04:28 -0400 Subject: [PATCH 12/13] fix(nitro): Only auto-delete source maps when Sentry enabled them Previously `filesToDeleteAfterUpload` defaulted to `['**/*.map']` unconditionally, deleting user-generated source maps even when the user explicitly set `sourcemap: true` in their Nitro config. Now the default only applies when Sentry was the one to enable sourcemap generation. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/nitro/src/config.ts | 4 ++-- packages/nitro/src/module.ts | 4 ++-- packages/nitro/src/sourceMaps.ts | 33 +++++++++++++++++++------- packages/nitro/test/sourceMaps.test.ts | 23 ++++++++++++++---- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/packages/nitro/src/config.ts b/packages/nitro/src/config.ts index 12eb3b658e95..cdc0f2b00dfb 100644 --- a/packages/nitro/src/config.ts +++ b/packages/nitro/src/config.ts @@ -24,10 +24,10 @@ export function setupSentryNitroModule( config.tracingChannel = true; } - configureSourcemapSettings(config, moduleOptions); + const { sentryEnabledSourcemaps } = configureSourcemapSettings(config, moduleOptions); config.modules = config.modules || []; - config.modules.push(createNitroModule(moduleOptions)); + config.modules.push(createNitroModule(moduleOptions, sentryEnabledSourcemaps)); return config; } diff --git a/packages/nitro/src/module.ts b/packages/nitro/src/module.ts index 6cda58554a25..1a4e5b0478d1 100644 --- a/packages/nitro/src/module.ts +++ b/packages/nitro/src/module.ts @@ -6,12 +6,12 @@ import { setupSourceMaps } from './sourceMaps'; /** * Creates a Nitro module to setup the Sentry SDK. */ -export function createNitroModule(sentryOptions?: SentryNitroOptions): NitroModule { +export function createNitroModule(sentryOptions?: SentryNitroOptions, sentryEnabledSourcemaps?: boolean): NitroModule { return { name: 'sentry', setup: nitro => { instrumentServer(nitro); - setupSourceMaps(nitro, sentryOptions); + setupSourceMaps(nitro, sentryOptions, sentryEnabledSourcemaps); }, }; } diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index 745886cc57f4..d2a8030376a9 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -6,7 +6,7 @@ import type { SentryNitroOptions } from './config'; /** * Registers a `compiled` hook to upload source maps after the build completes. */ -export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions): void { +export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions, sentryEnabledSourcemaps?: boolean): void { // The `compiled` hook fires on EVERY rebuild during `nitro dev` watch mode. // nitro.options.dev is reliably set by the time module setup runs. if (nitro.options.dev) { @@ -19,16 +19,20 @@ export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions): voi } nitro.hooks.hook('compiled', async (_nitro: Nitro) => { - await handleSourceMapUpload(_nitro, options); + await handleSourceMapUpload(_nitro, options, sentryEnabledSourcemaps); }); } /** * Handles the actual source map upload after the build completes. */ -async function handleSourceMapUpload(nitro: Nitro, options?: SentryNitroOptions): Promise { +async function handleSourceMapUpload( + nitro: Nitro, + options?: SentryNitroOptions, + sentryEnabledSourcemaps?: boolean, +): Promise { const outputDir = nitro.options.output.serverDir; - const pluginOptions = getPluginOptions(options); + const pluginOptions = getPluginOptions(options, sentryEnabledSourcemaps); const sentryBuildPluginManager = createSentryBuildPluginManager(pluginOptions, { buildTool: 'nitro', @@ -61,7 +65,10 @@ function normalizePath(path: string): string { * * Only exported for testing purposes. */ -export function getPluginOptions(options?: SentryNitroOptions): BundlerPluginOptions { +export function getPluginOptions( + options?: SentryNitroOptions, + sentryEnabledSourcemaps?: boolean, +): BundlerPluginOptions { return { org: options?.org ?? process.env.SENTRY_ORG, project: options?.project ?? process.env.SENTRY_PROJECT, @@ -76,7 +83,8 @@ export function getPluginOptions(options?: SentryNitroOptions): BundlerPluginOpt disable: options?.sourcemaps?.disable, assets: options?.sourcemaps?.assets, ignore: options?.sourcemaps?.ignore, - filesToDeleteAfterUpload: options?.sourcemaps?.filesToDeleteAfterUpload ?? ['**/*.map'], + filesToDeleteAfterUpload: + options?.sourcemaps?.filesToDeleteAfterUpload ?? (sentryEnabledSourcemaps ? ['**/*.map'] : undefined), rewriteSources: options?.sourcemaps?.rewriteSources ?? ((source: string) => normalizePath(source)), }, release: options?.release, @@ -99,10 +107,13 @@ export function getPluginOptions(options?: SentryNitroOptions): BundlerPluginOpt - We enable source maps for Sentry - Configure `filesToDeleteAfterUpload` to clean up .map files after upload */ -export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: SentryNitroOptions): void { +export function configureSourcemapSettings( + config: NitroConfig, + moduleOptions?: SentryNitroOptions, +): { sentryEnabledSourcemaps: boolean } { const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true; if (sourcemapUploadDisabled) { - return; + return { sentryEnabledSourcemaps: false }; } if (config.sourcemap === false) { @@ -110,9 +121,10 @@ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: console.warn( '[@sentry/nitro] You have explicitly disabled source maps (`sourcemap: false`). Sentry will not upload source maps, and errors will not be unminified. To let Sentry handle source maps, remove the `sourcemap` option from your Nitro config, or use `sourcemaps: { disable: true }` in your Sentry options to silence this warning.', ); - return; + return { sentryEnabledSourcemaps: false }; } + let sentryEnabledSourcemaps = false; if (config.sourcemap === true) { if (moduleOptions?.debug) { // eslint-disable-next-line no-console @@ -121,6 +133,7 @@ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: } else { // User did not explicitly set sourcemap — enable it for Sentry config.sourcemap = true; + sentryEnabledSourcemaps = true; if (moduleOptions?.debug) { // eslint-disable-next-line no-console console.log( @@ -134,4 +147,6 @@ export function configureSourcemapSettings(config: NitroConfig, moduleOptions?: // This makes sourcemaps unusable for Sentry. config.experimental = config.experimental || {}; config.experimental.sourcemapMinify = false; + + return { sentryEnabledSourcemaps }; } diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts index 588a52fcd42d..d9401208dbce 100644 --- a/packages/nitro/test/sourceMaps.test.ts +++ b/packages/nitro/test/sourceMaps.test.ts @@ -20,7 +20,7 @@ describe('getPluginOptions', () => { }); it('returns default options when no options are provided', () => { - const options = getPluginOptions(); + const options = getPluginOptions(undefined, true); expect(options).toEqual( expect.objectContaining({ @@ -44,6 +44,18 @@ describe('getPluginOptions', () => { expect(options.url).toBeUndefined(); }); + it('does not default filesToDeleteAfterUpload when user enabled sourcemaps themselves', () => { + const options = getPluginOptions(undefined, false); + + expect(options.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); + }); + + it('respects user-provided filesToDeleteAfterUpload even when Sentry enabled sourcemaps', () => { + const options = getPluginOptions({ sourcemaps: { filesToDeleteAfterUpload: ['dist/**/*.map'] } }, true); + + expect(options.sourcemaps?.filesToDeleteAfterUpload).toEqual(['dist/**/*.map']); + }); + it('uses environment variables as fallback', () => { process.env.SENTRY_ORG = 'env-org'; process.env.SENTRY_PROJECT = 'env-project'; @@ -145,17 +157,19 @@ describe('getPluginOptions', () => { describe('configureSourcemapSettings', () => { it('enables sourcemap generation on the config', () => { const config: NitroConfig = {}; - configureSourcemapSettings(config); + const result = configureSourcemapSettings(config); expect(config.sourcemap).toBe(true); + expect(result.sentryEnabledSourcemaps).toBe(true); }); it('respects user explicitly disabling sourcemaps and warns', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const config: NitroConfig = { sourcemap: false }; - configureSourcemapSettings(config); + const result = configureSourcemapSettings(config); expect(config.sourcemap).toBe(false); + expect(result.sentryEnabledSourcemaps).toBe(false); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('explicitly disabled source maps')); warnSpy.mockRestore(); }); @@ -171,9 +185,10 @@ describe('configureSourcemapSettings', () => { it('keeps sourcemap true when user already set it', () => { const config: NitroConfig = { sourcemap: true }; - configureSourcemapSettings(config); + const result = configureSourcemapSettings(config); expect(config.sourcemap).toBe(true); + expect(result.sentryEnabledSourcemaps).toBe(false); }); it('disables experimental sourcemapMinify', () => { From 14b31ea42c23eb180b42d7d61edb476acd3c9b40 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 17 Apr 2026 12:30:52 -0400 Subject: [PATCH 13/13] fix(nitro): Skip source map upload in nitro-prerender nested build Nitro spawns a nested Nitro instance for prerendering with the user's `modules` array re-installed, which caused the Sentry module to run twice and upload source maps + create a release twice per build. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/nitro/src/sourceMaps.ts | 6 ++++++ packages/nitro/test/sourceMaps.test.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts index d2a8030376a9..baaad94b658f 100644 --- a/packages/nitro/src/sourceMaps.ts +++ b/packages/nitro/src/sourceMaps.ts @@ -13,6 +13,12 @@ export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions, sent return; } + // Nitro spawns a nested Nitro instance for prerendering with the user's `modules` re-installed. + // Uploading here would double-upload source maps and create a duplicate release. + if (nitro.options.preset === 'nitro-prerender') { + return; + } + // Respect user's explicit disable if (options?.sourcemaps?.disable === true) { return; diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts index d9401208dbce..ffc0c0295979 100644 --- a/packages/nitro/test/sourceMaps.test.ts +++ b/packages/nitro/test/sourceMaps.test.ts @@ -274,6 +274,18 @@ describe('setupSourceMaps', () => { expect(hookFn).not.toHaveBeenCalled(); }); + it('does not register hook in nitro-prerender preset', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, preset: 'nitro-prerender', output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro); + + expect(hookFn).not.toHaveBeenCalled(); + }); + it('registers compiled hook in production mode', () => { const hookFn = vi.fn(); const nitro = {