Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"dependencies": {
"@nuxt/kit": "^3.13.2",
"@sentry/browser": "10.70.0",
"@sentry/bundler-plugin-core": "^5.3.0",
"@sentry/cloudflare": "10.70.0",
"@sentry/core": "10.70.0",
"@sentry/node": "10.70.0",
Expand Down
34 changes: 34 additions & 0 deletions packages/nuxt/src/vite/sourceMapDeletion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createSentryBuildPluginManager, type Options } from '@sentry/bundler-plugin-core';

export function withoutSourceMapDeletion(options: Options): Options {
return {
...options,
sourcemaps: {
...options.sourcemaps,
filesToDeleteAfterUpload: undefined,
},
};
}

export async function deleteSourceMapsAfterBuild(options: Options): Promise<void> {
const filesToDeleteAfterUpload = await options.sourcemaps?.filesToDeleteAfterUpload;

if (filesToDeleteAfterUpload === undefined) {
return;
}

const deletionOptions: Options = {
...options,
sourcemaps: {
...options.sourcemaps,
filesToDeleteAfterUpload,
},
};

const sentryBuildPluginManager = createSentryBuildPluginManager(deletionOptions, {
buildTool: 'nuxt',
loggerPrefix: '[Sentry Nuxt]',
});

await sentryBuildPluginManager.deleteArtifacts();
}
11 changes: 9 additions & 2 deletions packages/nuxt/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { sentryVitePlugin, type SentryVitePluginOptions } from '@sentry/vite-plu
import type { NitroConfig } from 'nitropack';
import type { Plugin } from 'vite';
import type { SentryNuxtModuleOptions } from '../common/types';
import { deleteSourceMapsAfterBuild, withoutSourceMapDeletion } from './sourceMapDeletion';
import { validateSourceMapsOptionsPlugin } from './sentryVitePlugin';

/**
Expand Down Expand Up @@ -86,7 +87,7 @@ export function setupSourceMaps(
[
validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled }),
// Vite plugin is added on the client and server side (plugin runs for both builds)
...sentryVitePlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback)),
...sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))),
],
{ dev: false, build: true }, // Only add source map plugin during build
);
Expand All @@ -113,10 +114,16 @@ export function setupSourceMaps(
// Add Sentry plugin
// Runs only on server-side (Nitro)
nitroConfig.rollupConfig.plugins.push(
sentryRollupPlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback)),
sentryRollupPlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))),
);
}
});

nuxt.hook('close', async () => {
if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) {
await deleteSourceMapsAfterBuild(getPluginOptions(moduleOptions, shouldDeleteFilesFallback));
}
});
}

/**
Expand Down
119 changes: 104 additions & 15 deletions packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,25 @@ function createMockNuxt(options: {
}

describe('setupSourceMaps hooks', () => {
const defaultFilesToDeleteAfterUpload = [
'.*/**/public/**/*.map',
'.*/**/server/**/*.map',
'.*/**/output/**/*.map',
'.*/**/function/**/*.map',
];

const mockSentryVitePlugin = vi.fn(() => [{ name: 'sentry-vite-plugin' }]);
const mockSentryRollupPlugin = vi.fn(() => ({ name: 'sentry-rollup-plugin' }));
const mockDeleteArtifacts = vi.fn().mockResolvedValue(undefined);
const mockCreateSentryBuildPluginManager = vi.fn(() => ({ deleteArtifacts: mockDeleteArtifacts }));

const consoleLogSpy = vi.spyOn(console, 'log');
const consoleWarnSpy = vi.spyOn(console, 'warn');

beforeAll(() => {
vi.doMock('@sentry/bundler-plugin-core', () => ({
createSentryBuildPluginManager: mockCreateSentryBuildPluginManager,
}));
vi.doMock('@sentry/vite-plugin', () => ({
sentryVitePlugin: mockSentryVitePlugin,
}));
Expand All @@ -65,6 +77,7 @@ describe('setupSourceMaps hooks', () => {
afterAll(() => {
consoleLogSpy.mockRestore();
consoleWarnSpy.mockRestore();
vi.doUnmock('@sentry/bundler-plugin-core');
vi.doUnmock('@sentry/vite-plugin');
vi.doUnmock('@sentry/rollup-plugin');
});
Expand All @@ -74,6 +87,8 @@ describe('setupSourceMaps hooks', () => {
consoleWarnSpy.mockClear();
mockSentryVitePlugin.mockClear();
mockSentryRollupPlugin.mockClear();
mockCreateSentryBuildPluginManager.mockClear();
mockDeleteArtifacts.mockClear();
});

describe('vite plugin registration', () => {
Expand Down Expand Up @@ -152,14 +167,7 @@ describe('setupSourceMaps hooks', () => {
});

describe('shouldDeleteFilesFallback passed to getPluginOptions in Vite plugin', () => {
const defaultFilesToDeleteAfterUpload = [
'.*/**/public/**/*.map',
'.*/**/server/**/*.map',
'.*/**/output/**/*.map',
'.*/**/function/**/*.map',
];

it('sentryVitePlugin is called with fallback filesToDeleteAfterUpload when source maps are unset', async () => {
it('does not pass fallback deletion patterns to the Vite plugin', async () => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt({
_prepare: false,
Expand All @@ -172,9 +180,7 @@ describe('setupSourceMaps hooks', () => {

expect(mockSentryVitePlugin).toHaveBeenCalledWith(
expect.objectContaining({
sourcemaps: expect.objectContaining({
filesToDeleteAfterUpload: defaultFilesToDeleteAfterUpload,
}),
sourcemaps: expect.objectContaining({ filesToDeleteAfterUpload: undefined }),
}),
);
});
Expand All @@ -194,10 +200,93 @@ describe('setupSourceMaps hooks', () => {
const nitroConfig = { rollupConfig: { plugins: [] as unknown[], output: {} }, dev: false };
await mockNuxt.triggerHook('nitro:config', nitroConfig);

const pluginOptions = (mockSentryRollupPlugin?.mock?.calls?.[0] as unknown[])?.[0] as {
sourcemaps?: { filesToDeleteAfterUpload?: string[] };
};
expect(pluginOptions?.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined();
expect(mockSentryRollupPlugin).toHaveBeenCalledWith(
expect.objectContaining({
sourcemaps: expect.objectContaining({ filesToDeleteAfterUpload: undefined }),
}),
);
});
});

describe('close hook', () => {
it('deletes source maps after the build using fallback patterns', async () => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt({
_prepare: false,
dev: false,
sourcemap: { client: undefined, server: undefined },
});
const { mockAddVitePlugin } = createMockAddVitePlugin();

setupSourceMaps({ debug: false }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
await mockNuxt.triggerHook('modules:done');
await mockNuxt.triggerHook('close');

expect(mockCreateSentryBuildPluginManager).toHaveBeenCalledWith(
expect.objectContaining({
sourcemaps: expect.objectContaining({ filesToDeleteAfterUpload: defaultFilesToDeleteAfterUpload }),
}),
{ buildTool: 'nuxt', loggerPrefix: '[Sentry Nuxt]' },
);
expect(mockDeleteArtifacts).toHaveBeenCalledTimes(1);
});

it('uses user-provided deletion patterns after the build', async () => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt({
_prepare: false,
dev: false,
sourcemap: { client: true, server: true },
});
const { mockAddVitePlugin } = createMockAddVitePlugin();
const filesToDeleteAfterUpload = ['.output/**/*.map'];

setupSourceMaps({ sourcemaps: { filesToDeleteAfterUpload } }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
await mockNuxt.triggerHook('modules:done');
await mockNuxt.triggerHook('close');

expect(mockCreateSentryBuildPluginManager).toHaveBeenCalledWith(
expect.objectContaining({
sourcemaps: expect.objectContaining({ filesToDeleteAfterUpload }),
}),
{ buildTool: 'nuxt', loggerPrefix: '[Sentry Nuxt]' },
);
expect(mockDeleteArtifacts).toHaveBeenCalledTimes(1);
});

it('does not create a manager when deletion is not configured', async () => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt({
_prepare: false,
dev: false,
sourcemap: { client: true, server: true },
});
const { mockAddVitePlugin } = createMockAddVitePlugin();

setupSourceMaps({}, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
await mockNuxt.triggerHook('modules:done');
await mockNuxt.triggerHook('close');

expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled();
expect(mockDeleteArtifacts).not.toHaveBeenCalled();
});

it.each([
{ label: 'prepare mode', nuxtOptions: { _prepare: true, dev: false } },
{ label: 'dev mode', nuxtOptions: { _prepare: false, dev: true } },
])('does not delete source maps in $label', async ({ nuxtOptions }) => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt(nuxtOptions);
const { mockAddVitePlugin } = createMockAddVitePlugin();

setupSourceMaps(
{ sourcemaps: { filesToDeleteAfterUpload: ['.output/**/*.map'] } },
mockNuxt as unknown as Nuxt,
mockAddVitePlugin,
);
await mockNuxt.triggerHook('close');

expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled();
});
});

Expand Down
Loading