From 5a958e7085524f44885fa1c30c79c3054fbc659b Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 12 Feb 2026 12:04:20 -0500 Subject: [PATCH 1/4] 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 69a0019402f3..4ddd3714c736 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -38,6 +38,7 @@ "nitro": ">=3.0.1-alpha.1" }, "dependencies": { + "@sentry/bundler-plugin-core": "^4.9.0", "@sentry/core": "10.38.0", "@sentry/node": "10.38.0", "otel-tracing-channel": "^0.2.0" diff --git a/packages/nitro/rollup.npm.config.mjs b/packages/nitro/rollup.npm.config.mjs index 35b018b2d99c..1c7c4259db7e 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/, 'otel-tracing-channel', /^h3/, /^srvx/], + external: [/^nitro/, 'otel-tracing-channel', /^h3/, /^srvx/, '@sentry/bundler-plugin-core'], }, }), ), diff --git a/packages/nitro/src/config.ts b/packages/nitro/src/config.ts index 9b22023735e3..c02f1a46200d 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,7 +33,7 @@ export function withSentryConfig(config: NitroConfig, moduleOptions?: SentryNitr */ export function setupSentryNitroModule( config: NitroConfig, - _moduleOptions?: SentryNitroOptions, + moduleOptions?: SentryNitroOptions, _serverConfigFile?: string, ): NitroConfig { // @ts-expect-error Nitro tracing config is not out yet @@ -29,8 +42,33 @@ export function setupSentryNitroModule( config.tracing = 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 2632f4eb436cd4d7818cc8426c065dcbba5c97e6 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 12 Feb 2026 12:09:23 -0500 Subject: [PATCH 2/4] 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 c02f1a46200d..b90e314c82b6 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, @@ -42,33 +42,10 @@ export function setupSentryNitroModule( config.tracing = 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 62143c1a62e0043eeb43a8d8b0ff2efc318946b9 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 12 Feb 2026 12:30:21 -0500 Subject: [PATCH 3/4] 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 7192087cf51623340b3a480344df3f5550d07d65 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 12 Feb 2026 12:37:11 -0500 Subject: [PATCH 4/4] 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', () => {