diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index a89317151989..2918f420ff93 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -21,7 +21,7 @@ export type SourceMapSetting = boolean | 'hidden' | 'inline'; export function setupSourceMaps( moduleOptions: SentryNuxtModuleOptions, nuxt: Nuxt, - addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean }) => void, + addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean; server?: boolean }) => void, ): void { // TODO(v11): remove deprecated options (also from SentryNuxtModuleOptions type) @@ -83,13 +83,16 @@ export function setupSourceMaps( }); if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) { + // Also on the SSR build: Nitro's source maps reach the original files only through that build's maps. + addVitePlugin([validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled })], { + dev: false, + build: true, + }); + addVitePlugin( - [ - validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled }), - // Vite plugin is added on the client and server side (plugin runs for both builds) - ...sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))), - ], - { dev: false, build: true }, // Only add source map plugin during build + sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))), + // Nitro bundles the SSR output again, so debug IDs from the SSR build would point to the wrong maps. + { dev: false, build: true, server: false }, ); } @@ -211,7 +214,7 @@ export function getPluginOptions( sourcemaps: { disable: moduleOptions.sourcemaps?.disable, // The server/client files are in different places depending on the nitro preset (e.g. '.output/server' or '.netlify/functions-internal/server') - // We cannot determine automatically how the build folder looks like (depends on the preset), so we have to accept that source maps are uploaded multiple times (with the vitePlugin for Nuxt and the rollupPlugin for Nitro). + // We cannot determine automatically how the build folder looks like (depends on the preset), so each plugin uploads its own build output (the vitePlugin for the Nuxt client and the rollupPlugin for Nitro). // If we could know where the server/client assets are located, we could do something like this (based on the Nitro preset): isNitro ? ['./.output/server/**/*'] : ['./.output/public/**/*'], // eslint-disable-next-line typescript/no-deprecated assets: sourcemapsOptions.assets ?? deprecatedSourcemapsOptions.assets ?? undefined, diff --git a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts index cbdc995b5041..515ac164b717 100644 --- a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts +++ b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts @@ -4,16 +4,16 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites import type { SourceMapSetting } from '../../src/vite/sourceMaps'; function createMockAddVitePlugin() { - let capturedPlugins: Plugin[] | null = null; + const capturedPlugins: Plugin[] = []; const mockAddVitePlugin = vi.fn((plugins: Plugin[]) => { - capturedPlugins = plugins; + capturedPlugins.push(...plugins); }); return { mockAddVitePlugin, - getCapturedPlugin: () => capturedPlugins?.[0] ?? null, - getCapturedPlugins: () => capturedPlugins, + getValidationPlugin: () => + capturedPlugins.find(plugin => plugin.name === 'sentry-nuxt-source-map-validation') ?? null, }; } @@ -92,16 +92,32 @@ describe('setupSourceMaps hooks', () => { }); describe('vite plugin registration', () => { - it('calls `addVitePlugin` when setupSourceMaps is called', async () => { + it('adds the source map validation plugin to the client and server builds', async () => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - const plugin = getCapturedPlugin(); - expect(plugin).not.toBeNull(); - expect(plugin?.name).toBe('sentry-nuxt-source-map-validation'); + expect(mockAddVitePlugin).toHaveBeenCalledWith( + [expect.objectContaining({ name: 'sentry-nuxt-source-map-validation' })], + { dev: false, build: true }, + ); + }); + + it('adds the Sentry Vite plugin to the client build only', async () => { + const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); + const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); + const { mockAddVitePlugin } = createMockAddVitePlugin(); + + setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); + + expect(mockAddVitePlugin).toHaveBeenCalledTimes(2); + expect(mockAddVitePlugin).toHaveBeenCalledWith([{ name: 'sentry-vite-plugin' }], { + dev: false, + build: true, + server: false, + }); }); it.each([ @@ -123,22 +139,6 @@ describe('setupSourceMaps hooks', () => { expect(mockAddVitePlugin).not.toHaveBeenCalled(); }); - - it.each([ - { label: 'server (SSR) build', buildConfig: { build: { ssr: true }, plugins: [] } }, - { label: 'client build', buildConfig: { build: { ssr: false }, plugins: [] } }, - ])('adds sentry vite plugin to vite config for $label in production', async () => { - const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); - const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugins } = createMockAddVitePlugin(); - - setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - - const plugins = getCapturedPlugins(); - expect(plugins).not.toBeNull(); - expect(plugins?.length).toBeGreaterThan(0); - expect(mockSentryVitePlugin).toHaveBeenCalled(); - }); }); describe('sentry vite plugin calls', () => { @@ -330,12 +330,12 @@ describe('setupSourceMaps hooks', () => { it('logs a [Sentry] message in production mode', async () => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin, getValidationPlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); await mockNuxt.triggerHook('modules:done'); - const plugin = getCapturedPlugin(); + const plugin = getValidationPlugin(); if (plugin && typeof plugin.config === 'function') { plugin.config({ build: { ssr: false }, plugins: [] } as UserConfig, { mode: 'production', command: 'build' }); } @@ -354,12 +354,12 @@ describe('setupSourceMaps hooks', () => { it('does not log a [Sentry] messages in prepare mode', async () => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt({ _prepare: true }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin, getValidationPlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); await mockNuxt.triggerHook('modules:done'); - const plugin = getCapturedPlugin(); + const plugin = getValidationPlugin(); if (plugin && typeof plugin.config === 'function') { plugin.config({ build: {}, plugins: [] } as UserConfig, { mode: 'production', command: 'build' }); }