Skip to content

Commit 2cf2dd0

Browse files
authored
fix(v10/nuxt): Do not inject debug IDs into the SSR build (#25094)
Backport of #25080 Nuxt server source maps can still resolve to wrong lines (more info in Nitro bundles Nuxt's SSR build again. The Sentry Vite plugin also ran on the SSR build, so its debug IDs got into Nitro's output. Many server files used such an ID instead of Nitro's own, and both builds uploaded different maps under that ID. The Vite plugin now runs only on the client build. Nitro's plugin uploads the server maps, which already point to the original sources. Fixes #18974
1 parent d508f2b commit 2cf2dd0

2 files changed

Lines changed: 40 additions & 37 deletions

File tree

‎packages/nuxt/src/vite/sourceMaps.ts‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export type SourceMapSetting = boolean | 'hidden' | 'inline';
2121
export function setupSourceMaps(
2222
moduleOptions: SentryNuxtModuleOptions,
2323
nuxt: Nuxt,
24-
addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean }) => void,
24+
addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean; server?: boolean }) => void,
2525
): void {
2626
// TODO(v11): remove deprecated options (also from SentryNuxtModuleOptions type)
2727

@@ -83,13 +83,16 @@ export function setupSourceMaps(
8383
});
8484

8585
if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) {
86+
// Also on the SSR build: Nitro's source maps reach the original files only through that build's maps.
87+
addVitePlugin([validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled })], {
88+
dev: false,
89+
build: true,
90+
});
91+
8692
addVitePlugin(
87-
[
88-
validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled }),
89-
// Vite plugin is added on the client and server side (plugin runs for both builds)
90-
...sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))),
91-
],
92-
{ dev: false, build: true }, // Only add source map plugin during build
93+
sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))),
94+
// Nitro bundles the SSR output again, so debug IDs from the SSR build would point to the wrong maps.
95+
{ dev: false, build: true, server: false },
9396
);
9497
}
9598

@@ -211,7 +214,7 @@ export function getPluginOptions(
211214
sourcemaps: {
212215
disable: moduleOptions.sourcemaps?.disable,
213216
// The server/client files are in different places depending on the nitro preset (e.g. '.output/server' or '.netlify/functions-internal/server')
214-
// 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).
217+
// 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).
215218
// 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/**/*'],
216219
// eslint-disable-next-line typescript/no-deprecated
217220
assets: sourcemapsOptions.assets ?? deprecatedSourcemapsOptions.assets ?? undefined,

‎packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts‎

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites
44
import type { SourceMapSetting } from '../../src/vite/sourceMaps';
55

66
function createMockAddVitePlugin() {
7-
let capturedPlugins: Plugin[] | null = null;
7+
const capturedPlugins: Plugin[] = [];
88

99
const mockAddVitePlugin = vi.fn((plugins: Plugin[]) => {
10-
capturedPlugins = plugins;
10+
capturedPlugins.push(...plugins);
1111
});
1212

1313
return {
1414
mockAddVitePlugin,
15-
getCapturedPlugin: () => capturedPlugins?.[0] ?? null,
16-
getCapturedPlugins: () => capturedPlugins,
15+
getValidationPlugin: () =>
16+
capturedPlugins.find(plugin => plugin.name === 'sentry-nuxt-source-map-validation') ?? null,
1717
};
1818
}
1919

@@ -92,16 +92,32 @@ describe('setupSourceMaps hooks', () => {
9292
});
9393

9494
describe('vite plugin registration', () => {
95-
it('calls `addVitePlugin` when setupSourceMaps is called', async () => {
95+
it('adds the source map validation plugin to the client and server builds', async () => {
9696
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
9797
const mockNuxt = createMockNuxt({ _prepare: false, dev: false });
98-
const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin();
98+
const { mockAddVitePlugin } = createMockAddVitePlugin();
9999

100100
setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
101101

102-
const plugin = getCapturedPlugin();
103-
expect(plugin).not.toBeNull();
104-
expect(plugin?.name).toBe('sentry-nuxt-source-map-validation');
102+
expect(mockAddVitePlugin).toHaveBeenCalledWith(
103+
[expect.objectContaining({ name: 'sentry-nuxt-source-map-validation' })],
104+
{ dev: false, build: true },
105+
);
106+
});
107+
108+
it('adds the Sentry Vite plugin to the client build only', async () => {
109+
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
110+
const mockNuxt = createMockNuxt({ _prepare: false, dev: false });
111+
const { mockAddVitePlugin } = createMockAddVitePlugin();
112+
113+
setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
114+
115+
expect(mockAddVitePlugin).toHaveBeenCalledTimes(2);
116+
expect(mockAddVitePlugin).toHaveBeenCalledWith([{ name: 'sentry-vite-plugin' }], {
117+
dev: false,
118+
build: true,
119+
server: false,
120+
});
105121
});
106122

107123
it.each([
@@ -123,22 +139,6 @@ describe('setupSourceMaps hooks', () => {
123139

124140
expect(mockAddVitePlugin).not.toHaveBeenCalled();
125141
});
126-
127-
it.each([
128-
{ label: 'server (SSR) build', buildConfig: { build: { ssr: true }, plugins: [] } },
129-
{ label: 'client build', buildConfig: { build: { ssr: false }, plugins: [] } },
130-
])('adds sentry vite plugin to vite config for $label in production', async () => {
131-
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
132-
const mockNuxt = createMockNuxt({ _prepare: false, dev: false });
133-
const { mockAddVitePlugin, getCapturedPlugins } = createMockAddVitePlugin();
134-
135-
setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
136-
137-
const plugins = getCapturedPlugins();
138-
expect(plugins).not.toBeNull();
139-
expect(plugins?.length).toBeGreaterThan(0);
140-
expect(mockSentryVitePlugin).toHaveBeenCalled();
141-
});
142142
});
143143

144144
describe('sentry vite plugin calls', () => {
@@ -330,12 +330,12 @@ describe('setupSourceMaps hooks', () => {
330330
it('logs a [Sentry] message in production mode', async () => {
331331
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
332332
const mockNuxt = createMockNuxt({ _prepare: false, dev: false });
333-
const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin();
333+
const { mockAddVitePlugin, getValidationPlugin } = createMockAddVitePlugin();
334334

335335
setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
336336
await mockNuxt.triggerHook('modules:done');
337337

338-
const plugin = getCapturedPlugin();
338+
const plugin = getValidationPlugin();
339339
if (plugin && typeof plugin.config === 'function') {
340340
plugin.config({ build: { ssr: false }, plugins: [] } as UserConfig, { mode: 'production', command: 'build' });
341341
}
@@ -354,12 +354,12 @@ describe('setupSourceMaps hooks', () => {
354354
it('does not log a [Sentry] messages in prepare mode', async () => {
355355
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
356356
const mockNuxt = createMockNuxt({ _prepare: true });
357-
const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin();
357+
const { mockAddVitePlugin, getValidationPlugin } = createMockAddVitePlugin();
358358

359359
setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
360360
await mockNuxt.triggerHook('modules:done');
361361

362-
const plugin = getCapturedPlugin();
362+
const plugin = getValidationPlugin();
363363
if (plugin && typeof plugin.config === 'function') {
364364
plugin.config({ build: {}, plugins: [] } as UserConfig, { mode: 'production', command: 'build' });
365365
}

0 commit comments

Comments
 (0)