Skip to content
Draft
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
5 changes: 3 additions & 2 deletions packages/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@
"dependencies": {
"@nuxt/kit": "^3.13.2",
"@sentry/browser": "10.34.0",
"@sentry/bundler-plugin-core": "^4.7.0",
"@sentry/cloudflare": "10.34.0",
"@sentry/core": "10.34.0",
"@sentry/node": "10.34.0",
"@sentry/node-core": "10.34.0",
"@sentry/rollup-plugin": "^4.6.2",
"@sentry/vite-plugin": "^4.6.2",
"@sentry/rollup-plugin": "^4.7.0",
"@sentry/vite-plugin": "^4.7.0",
"@sentry/vue": "10.34.0"
},
"devDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ export default defineNuxtModule<ModuleOptions>({
}

nuxt.hooks.hook('nitro:init', nitro => {
if (nuxt.options?._prepare) {
return;
}

if (serverConfigFile) {
addMiddlewareInstrumentation(nitro);
}
Expand Down
130 changes: 130 additions & 0 deletions packages/nuxt/src/vite/buildEndUploadHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { existsSync } from 'node:fs';
import type { Nuxt } from '@nuxt/schema';
import type { createSentryBuildPluginManager as createSentryBuildPluginManagerType } from '@sentry/bundler-plugin-core';
import * as path from 'path';
import type { SentryNuxtModuleOptions } from '../common/types';
import { getPluginOptions } from './sourceMaps';

/**
* A build-end hook that handles Sentry release creation and source map uploads.
* It creates a new Sentry release if configured, uploads source maps to Sentry,
* and optionally deletes the source map files after upload.
*
* This runs after both Vite (Nuxt) and Rollup (Nitro) builds complete, ensuring
* debug IDs are injected and source maps uploaded only once.
*/
// eslint-disable-next-line complexity
export async function handleBuildDoneHook(
sentryModuleOptions: SentryNuxtModuleOptions,
nuxt: Nuxt,
shouldDeleteFilesFallback?: { client: boolean; server: boolean },
): Promise<void> {
const debug = sentryModuleOptions.debug ?? false;
if (debug) {
// eslint-disable-next-line no-console
console.log('[Sentry] Nuxt build ended. Starting to upload build-time info to Sentry (release, source maps)...');
}

let createSentryBuildPluginManager: typeof createSentryBuildPluginManagerType | undefined;
try {
const bundlerPluginCore = await import('@sentry/bundler-plugin-core');
createSentryBuildPluginManager = bundlerPluginCore.createSentryBuildPluginManager;
} catch (error) {
debug &&
// eslint-disable-next-line no-console
console.warn('[Sentry] Could not load build manager package. Will not upload build-time info to Sentry.', error);
return;
}

if (!createSentryBuildPluginManager) {
// eslint-disable-next-line no-console
debug && console.warn('[Sentry] Could not find createSentryBuildPluginManager in bundler plugin core.');
return;
}

const outputDir = nuxt.options.nitro?.output?.dir || path.join(nuxt.options.rootDir, '.output');

if (!existsSync(outputDir)) {
// eslint-disable-next-line no-console
debug && console.warn(`[Sentry] Output directory does not exist yet: ${outputDir}. Skipping source map upload.`);
return;
}

const options = getPluginOptions(sentryModuleOptions, shouldDeleteFilesFallback, 'full');

// eslint-disable-next-line deprecation/deprecation
const sourceMapsUploadOptions = sentryModuleOptions.sourceMapsUploadOptions || {};
const sourceMapsEnabled =
sentryModuleOptions.sourcemaps?.disable === true
? false
: sentryModuleOptions.sourcemaps?.disable === false
? true
: // eslint-disable-next-line deprecation/deprecation
(sourceMapsUploadOptions.enabled ?? true);

if (sourceMapsEnabled) {
const existingIgnore = options.sourcemaps?.ignore || [];
const ignorePatterns = Array.isArray(existingIgnore) ? existingIgnore : [existingIgnore];

// node_modules source maps are ignored
const nodeModulesPatterns = ['**/node_modules/**', '**/node_modules/**/*.map'];
const hasNodeModulesIgnore = ignorePatterns.some(
pattern => typeof pattern === 'string' && pattern.includes('node_modules'),
);

if (!hasNodeModulesIgnore) {
ignorePatterns.push(...nodeModulesPatterns);
}

options.sourcemaps = {
...options.sourcemaps,
ignore: ignorePatterns.length > 0 ? ignorePatterns : undefined,
};

if (debug && ignorePatterns.length > 0) {
// eslint-disable-next-line no-console
console.log(`[Sentry] Excluding patterns from source map upload: ${ignorePatterns.join(', ')}`);
}
}

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

await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal();
await sentryBuildPluginManager.createRelease();

// eslint-disable-next-line no-console
debug && console.log('[Sentry] Successfully uploaded release information.');

if (!sourceMapsEnabled) {
debug &&
// eslint-disable-next-line no-console
console.log('[Sentry] Source map upload is disabled. Skipping debugID injection and source map upload steps.');
} else {
await sentryBuildPluginManager.injectDebugIds([outputDir]);
// eslint-disable-next-line no-console
debug && console.log('[Sentry] Successfully injected Debug IDs.');

// todo: rewriteSources seems to not be applied
await sentryBuildPluginManager.uploadSourcemaps([outputDir], {
// We don't want to prepare the artifacts because we injected Debug IDs manually before
prepareArtifacts: false,
});
// eslint-disable-next-line no-console
debug && console.log('[Sentry] Successfully uploaded source maps.');

await sentryBuildPluginManager.deleteArtifacts();
debug &&
// eslint-disable-next-line no-console
console.log(
`[Sentry] Successfully deleted specified source map artifacts (${sentryModuleOptions.sourcemaps?.filesToDeleteAfterUpload ? '' : "based on Sentry's default "}\`filesToDeleteAfterUpload: [${options.sourcemaps?.filesToDeleteAfterUpload}\`]).`,
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error("[Sentry] Error during Sentry's build-end hook: ", error);
}
}
98 changes: 69 additions & 29 deletions packages/nuxt/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { sentryRollupPlugin, type SentryRollupPluginOptions } from '@sentry/roll
import { sentryVitePlugin, type SentryVitePluginOptions } from '@sentry/vite-plugin';
import type { NitroConfig } from 'nitropack';
import type { SentryNuxtModuleOptions } from '../common/types';
import { handleBuildDoneHook } from './buildEndUploadHook';
import { shouldDisableSourceMapsUpload } from './utils';

/**
* Whether the user enabled (true, 'hidden', 'inline') or disabled (false) source maps
Expand All @@ -12,6 +14,15 @@ export type UserSourceMapSetting = 'enabled' | 'disabled' | 'unset' | undefined;
/** A valid source map setting */
export type SourceMapSetting = boolean | 'hidden' | 'inline';

/**
* Controls what functionality the bundler plugin provides.
*
* - `'release-injection-only'`: Plugin only injects release information. Source maps upload,
* debug ID injection, and file deletion are handled by the build-end hook.
* - `'full'`: Plugin handles everything including source maps upload and file deletion.
*/
export type PluginMode = 'release-injection-only' | 'full';

/**
* Setup source maps for Sentry inside the Nuxt module during build time (in Vite for Nuxt and Rollup for Nitro).
*/
Expand All @@ -35,7 +46,7 @@ export function setupSourceMaps(moduleOptions: SentryNuxtModuleOptions, nuxt: Nu
let shouldDeleteFilesFallback = { client: true, server: true };

nuxt.hook('modules:done', () => {
if (sourceMapsEnabled && !nuxt.options.dev) {
if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) {
// Changing this setting will propagate:
// - for client to viteConfig.build.sourceMap
// - for server to viteConfig.build.sourceMap and nitro.sourceMap
Expand All @@ -49,17 +60,29 @@ export function setupSourceMaps(moduleOptions: SentryNuxtModuleOptions, nuxt: Nu
server: previousSourceMapSettings.server === 'unset',
};

if (
isDebug &&
!moduleOptions.sourcemaps?.filesToDeleteAfterUpload &&
// eslint-disable-next-line deprecation/deprecation
!sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload &&
(shouldDeleteFilesFallback.client || shouldDeleteFilesFallback.server)
) {
// eslint-disable-next-line no-console
console.log(
"[Sentry] As Sentry enabled `'hidden'` source maps, source maps will be automatically deleted after uploading them to Sentry.",
);
if (isDebug && (shouldDeleteFilesFallback.client || shouldDeleteFilesFallback.server)) {
const enabledDeleteFallbacks =
shouldDeleteFilesFallback.client && shouldDeleteFilesFallback.server
? 'client-side and server-side'
: shouldDeleteFilesFallback.server
? 'server-side'
: 'client-side';

if (
!moduleOptions.sourcemaps?.filesToDeleteAfterUpload &&
// eslint-disable-next-line deprecation/deprecation
!sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload
) {
// eslint-disable-next-line no-console
console.log(
`[Sentry] We enabled \`'hidden'\` source maps for your ${enabledDeleteFallbacks} build. Source map files will be automatically deleted after uploading them to Sentry.`,
);
} else {
// eslint-disable-next-line no-console
console.log(
`[Sentry] We enabled \`'hidden'\` source maps for your ${enabledDeleteFallbacks} build. Source map files will be deleted according to your \`sourcemaps.filesToDeleteAfterUpload\` configuration. To use automatic deletion instead, leave \`filesToDeleteAfterUpload\` empty.`,
);
}
}
}
});
Expand All @@ -86,20 +109,21 @@ export function setupSourceMaps(moduleOptions: SentryNuxtModuleOptions, nuxt: Nu
console.log("[Sentry] Cannot detect runtime (client/server) inside hook 'vite:extendConfig'.");
} else {
// eslint-disable-next-line no-console
console.log(`[Sentry] Adding Sentry Vite plugin to the ${runtime} runtime.`);
console.log(`[Sentry] Adding Sentry Vite plugin to the ${runtime} runtime for release injection.`);
}
}

// Add Sentry plugin
// Vite plugin is added on the client and server side (hook runs twice)
// Nuxt client source map is 'false' by default. Warning about this will be shown already in an earlier step, and it's also documented that `nuxt.sourcemap.client` needs to be enabled.
// Add Sentry Vite plugin for release injection only
// Source maps upload, debug ID injection, and artifact deletion are handled in the build:done hook
viteConfig.plugins = viteConfig.plugins || [];
viteConfig.plugins.push(sentryVitePlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback)));
viteConfig.plugins.push(
sentryVitePlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback, 'release-injection-only')),
);
}
});

nuxt.hook('nitro:config', (nitroConfig: NitroConfig) => {
if (sourceMapsEnabled && !nitroConfig.dev) {
if (sourceMapsEnabled && !nitroConfig.dev && !nuxt.options?._prepare) {
if (!nitroConfig.rollupConfig) {
nitroConfig.rollupConfig = {};
}
Expand All @@ -115,16 +139,24 @@ export function setupSourceMaps(moduleOptions: SentryNuxtModuleOptions, nuxt: Nu

if (isDebug) {
// eslint-disable-next-line no-console
console.log('[Sentry] Adding Sentry Rollup plugin to the server runtime.');
console.log('[Sentry] Adding Sentry Rollup plugin to the server runtime for release injection.');
}

// Add Sentry plugin
// Runs only on server-side (Nitro)
// Add Sentry Rollup plugin for release injection only
// Source maps upload, debug ID injection, and artifact deletion are handled in the build:done hook
nitroConfig.rollupConfig.plugins.push(
sentryRollupPlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback)),
sentryRollupPlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback, 'release-injection-only')),
);
}
});

// This ensures debug IDs are injected and source maps uploaded only once at the end of the build
nuxt.hook('close', async () => {
// `nuxt prepare` runs during package installation -> we don't need to upload anything here
if (!nuxt.options.dev && !nuxt.options._prepare) {
await handleBuildDoneHook(moduleOptions, nuxt, shouldDeleteFilesFallback);
}
});
}

/**
Expand All @@ -144,11 +176,14 @@ function normalizePath(path: string): string {
export function getPluginOptions(
moduleOptions: SentryNuxtModuleOptions,
shouldDeleteFilesFallback?: { client: boolean; server: boolean },
pluginMode: PluginMode = 'release-injection-only',
): SentryVitePluginOptions | SentryRollupPluginOptions {
// eslint-disable-next-line deprecation/deprecation
const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {};

const shouldDeleteFilesAfterUpload = shouldDeleteFilesFallback?.client || shouldDeleteFilesFallback?.server;
const shouldDeleteFilesAfterUpload =
pluginMode === 'full' && (shouldDeleteFilesFallback?.client || shouldDeleteFilesFallback?.server);

const fallbackFilesToDelete = [
...(shouldDeleteFilesFallback?.client ? ['.*/**/public/**/*.map'] : []),
...(shouldDeleteFilesFallback?.server
Expand Down Expand Up @@ -197,6 +232,8 @@ export function getPluginOptions(
release: {
// eslint-disable-next-line deprecation/deprecation
name: moduleOptions.release?.name ?? sourceMapsUploadOptions.release?.name,
// todo: release is injected twice sometimes (fix in bundler plugins)
inject: moduleOptions.release?.inject,
// Support all release options from BuildTimeOptionsBase
...moduleOptions.release,
...moduleOptions?.unstable_sentryBundlerPluginOptions?.release,
Expand All @@ -209,19 +246,22 @@ export function getPluginOptions(
...moduleOptions?.unstable_sentryBundlerPluginOptions,

sourcemaps: {
disable: moduleOptions.sourcemaps?.disable,
disable: shouldDisableSourceMapsUpload(moduleOptions, pluginMode),
// 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).
// 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 deprecation/deprecation
assets: sourcemapsOptions.assets ?? deprecatedSourcemapsOptions.assets ?? undefined,
// eslint-disable-next-line deprecation/deprecation
ignore: sourcemapsOptions.ignore ?? deprecatedSourcemapsOptions.ignore ?? undefined,
filesToDeleteAfterUpload: filesToDeleteAfterUpload
? filesToDeleteAfterUpload
: shouldDeleteFilesFallback?.server || shouldDeleteFilesFallback?.client
? fallbackFilesToDelete
: undefined,
filesToDeleteAfterUpload:
pluginMode === 'release-injection-only'
? undefined // Setting this to `undefined` to only delete files during buildEndUploadHook (not before, when vite/rollup plugins run)
: filesToDeleteAfterUpload
? filesToDeleteAfterUpload
: shouldDeleteFilesFallback?.server || shouldDeleteFilesFallback?.client
? fallbackFilesToDelete
: undefined,
rewriteSources: (source: string) => normalizePath(source),
...moduleOptions?.unstable_sentryBundlerPluginOptions?.sourcemaps,
},
Expand Down
43 changes: 43 additions & 0 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,49 @@ import { consoleSandbox } from '@sentry/core';
import * as fs from 'fs';
import type { Nuxt } from 'nuxt/schema';
import * as path from 'path';
import type { SentryNuxtModuleOptions } from '../common/types';
import type { PluginMode } from './sourceMaps';

/**
* Determines whether source maps upload should be disabled in the bundler plugin.
*
* The logic follows a clear precedence:
* 1. Plugin mode check: If mode is `'release-injection-only'`, always disable upload
* (upload is handled by the build-end hook instead)
* 2. When mode is `'full'`, check user options with precedence:
* a. New option: `moduleOptions.sourcemaps.disable` (takes precedence)
* b. Deprecated option: `sourceMapsUploadOptions.enabled` (inverted, used as fallback)
* c. Default: `false` (upload enabled when mode is 'full' and no user option set)
*
* Only exported for testing.
*/
export function shouldDisableSourceMapsUpload(
moduleOptions: SentryNuxtModuleOptions,
pluginMode: PluginMode = 'release-injection-only',
): boolean {
// Step 1: If plugin mode is 'release-injection-only', always disable upload
if (pluginMode !== 'full') {
return true;
}

// Step 2: Plugin mode is 'full' - check user options
// Note: disable can be boolean or 'disable-upload' - both truthy values mean disable upload
const disableOption = moduleOptions.sourcemaps?.disable;
if (disableOption !== undefined) {
// true or 'disable-upload' -> disable upload; false -> enable upload
return disableOption !== false;
}

// Priority 2: Deprecated option
// eslint-disable-next-line deprecation/deprecation
const deprecatedEnabled = moduleOptions.sourceMapsUploadOptions?.enabled;
if (deprecatedEnabled !== undefined) {
return !deprecatedEnabled;
}

// Default: upload enabled when plugin mode is 'full'
return false;
}

/**
* Find the default SDK init file for the given type (client or server).
Expand Down
Loading