Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.nitro
.cache
dist
.netlify

# Node dependencies
node_modules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,35 @@ const keepClientSourceMaps = process.env.E2E_KEEP_CLIENT_SOURCEMAPS === 'true';
/** `nuxt generate` emits no `.output/server`. Keyed on the command so a missing one under `nuxt build` still fails. */
const isStaticBuild = process.env.NUXT_COMMAND === 'generate';

const CLIENT_OUTPUT = path.join('.output', 'public');
const nitroPreset = process.env.NITRO_PRESET ?? 'node-server';

/** If Nitro moves one of these directories, the debug ID checks below fail. */
const OUTPUT_DIRS: Record<string, { client: string; server: string }> = {
'node-server': { client: path.join('.output', 'public'), server: path.join('.output', 'server') },
netlify: { client: 'dist', server: path.join('.netlify', 'functions-internal', 'server') },
'cloudflare-pages': { client: 'dist', server: path.join('dist', '_worker.js') },
};

const outputDirs = OUTPUT_DIRS[nitroPreset];
assert.ok(outputDirs, `Expected output directories for the Nitro preset ${nitroPreset}`);

const CLIENT_OUTPUT = outputDirs.client;
/** Vite's own output directory. Nitro copies it to `CLIENT_OUTPUT`, and Sentry deletes no maps here. */
const CLIENT_BUILD_OUTPUT = path.join('.nuxt', 'dist', 'client');
const SERVER_OUTPUT = path.join('.output', 'server');
const SERVER_OUTPUT = outputDirs.server;

/** Both markers sit in comments, so bundlers strip them from the code but keep them in `sourcesContent`. */
const CLIENT_MARKER = 'SOURCEMAP_MARKER_CLIENT';
const SERVER_MARKER = 'SOURCEMAP_MARKER_SERVER';

const UUID_REGEX = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;

/** Some presets write the server into the client output, e.g. Cloudflare Pages. */
function isClientFile(file: string): boolean {
const relativePath = path.relative(SERVER_OUTPUT, file);
return relativePath.startsWith('..') || path.isAbsolute(relativePath);
}

function filesContaining(dir: string, needle: string): string[] {
return fs
.readdirSync(dir, { recursive: true, withFileTypes: true })
Expand Down Expand Up @@ -64,7 +82,7 @@ function debugIdsOfChunksWithoutSourceMap(dir: string): Set<string> {
}

console.log(
`Variant: ${isStaticBuild ? 'nuxt generate' : 'nuxt build'}, ` +
`Variant: ${isStaticBuild ? 'nuxt generate' : 'nuxt build'}, Nitro preset ${nitroPreset}, ` +
`client source maps ${keepClientSourceMaps ? 'kept' : 'deleted'}\n`,
);

Expand Down Expand Up @@ -218,7 +236,7 @@ console.log('');

// --- What the build leaves behind in the client output ---

const clientSourceMaps = findSourceMapFiles({ outputDir: CLIENT_OUTPUT });
const clientSourceMaps = findSourceMapFiles({ outputDir: CLIENT_OUTPUT }).filter(isClientFile);

if (keepClientSourceMaps) {
assert.ok(clientSourceMaps.length > 0, `Expected Sentry to leave the user-enabled maps in ${CLIENT_OUTPUT}`);
Expand All @@ -228,20 +246,30 @@ if (keepClientSourceMaps) {
assert.deepEqual(clientSourceMaps, [], `Expected no source maps in ${CLIENT_OUTPUT} after upload`);

// The maps are gone, so a surviving reference only 404s in devtools and leaks where they were.
const danglingReferences = findSourceMappingUrlComments({ outputDir: CLIENT_OUTPUT });
const danglingReferences = findSourceMappingUrlComments({ outputDir: CLIENT_OUTPUT }).filter(isClientFile);
assert.deepEqual(danglingReferences, [], `Expected no sourceMappingURL comments in ${CLIENT_OUTPUT}`);

// Catches source maps inlined as `data:` URIs, which the reference check above skips by design.
const leakedSource = filesContaining(CLIENT_OUTPUT, CLIENT_MARKER);
const leakedSource = filesContaining(CLIENT_OUTPUT, CLIENT_MARKER).filter(isClientFile);
assert.deepEqual(leakedSource, [], `Expected no original client source under ${CLIENT_OUTPUT}`);

console.log(` ${CLIENT_OUTPUT} is free of source maps, sourceMappingURL comments and original source\n`);
}

// --- What the built server answers for the client source maps ---
// --- What the build leaves behind in the server output ---

// `nuxt generate` emits no server, and a static host serves only the files that exist.
// Nuxt enables the server maps by default, so they are the user's and Sentry must keep them.
if (!isStaticBuild) {
const serverSourceMaps = findSourceMapFiles({ outputDir: SERVER_OUTPUT });
assert.ok(serverSourceMaps.length > 0, `Expected Sentry to keep the server source maps in ${SERVER_OUTPUT}`);
console.log(` ${serverSourceMaps.length} source map(s) kept in ${SERVER_OUTPUT}, as Nuxt enables them\n`);
}

// --- What the built server answers for the client source maps ---

// `nuxt generate` emits no server, and a static host serves only the files that exist. The other presets build a
// server for their platform, which Node cannot start.
if (!isStaticBuild && nitroPreset === 'node-server') {
const clientChunkUrls = fs
.readdirSync(CLIENT_OUTPUT, { recursive: true, withFileTypes: true })
.filter(entry => entry.isFile() && entry.name.endsWith('.js'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
// own warning tells users to set, which makes it the setup worth regression-testing.
const keepClientSourceMaps = process.env.E2E_KEEP_CLIENT_SOURCEMAPS === 'true';

// With `sourcemap.client` unset, Sentry enables the client maps and finds their output directory itself, which
// differs per Nitro preset.
const nitroPreset = process.env.NITRO_PRESET;

export default defineNuxtConfig({
compatibilityDate: '2025-06-06',
imports: { autoImport: false },

sourcemap: { client: 'hidden' },
sourcemap: { client: nitroPreset ? undefined : 'hidden' },

nitro: { preset: nitroPreset },

modules: ['@sentry/nuxt/module'],

Expand All @@ -27,7 +33,7 @@ export default defineNuxtConfig({
release: { name: 'test-release' },
// Dropping `filesToDeleteAfterUpload` is the whole point of the "kept" variant: Sentry should
// upload the maps and leave the emitted files alone.
sourcemaps: keepClientSourceMaps ? {} : { filesToDeleteAfterUpload: ['.output/public/**/*.map'] },
sourcemaps: keepClientSourceMaps || nitroPreset ? {} : { filesToDeleteAfterUpload: ['.output/public/**/*.map'] },
debug: true,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
"test:build:keep-client-sourcemaps": "E2E_KEEP_CLIENT_SOURCEMAPS=true pnpm test:build",
"test:assert:keep-client-sourcemaps": "E2E_KEEP_CLIENT_SOURCEMAPS=true pnpm test:assert",
"test:build:static": "NUXT_COMMAND=generate pnpm test:build",
"test:assert:static": "NUXT_COMMAND=generate pnpm test:assert"
"test:assert:static": "NUXT_COMMAND=generate pnpm test:assert",
"test:build:netlify": "NITRO_PRESET=netlify pnpm test:build",
"test:assert:netlify": "NITRO_PRESET=netlify pnpm test:assert",
"test:build:cloudflare-pages": "NITRO_PRESET=cloudflare-pages pnpm test:build",
"test:assert:cloudflare-pages": "NITRO_PRESET=cloudflare-pages pnpm test:assert"
},
"dependencies": {
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
Expand All @@ -38,6 +42,16 @@
"build-command": "pnpm test:build:static",
"assert-command": "pnpm test:assert:static",
"label": "nuxt-4-sourcemaps (static / nuxt generate)"
},
{
"build-command": "pnpm test:build:netlify",
"assert-command": "pnpm test:assert:netlify",
"label": "nuxt-4-sourcemaps (netlify preset)"
},
{
"build-command": "pnpm test:build:cloudflare-pages",
"assert-command": "pnpm test:assert:cloudflare-pages",
"label": "nuxt-4-sourcemaps (cloudflare-pages preset)"
}
]
}
Expand Down
40 changes: 40 additions & 0 deletions packages/nuxt/src/vite/sourceMapDeletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@ import { createSentryBuildPluginManager, globFiles, type Options } from '@sentry
import * as fs from 'node:fs';
import * as path from 'node:path';

/** Nitro's output directories, which depend on the Nitro preset, e.g. `dist` for the public files on Netlify. */
type NitroOutputDirs = { publicDir: string; serverDir: string };

/** The sides whose source maps only exist because Sentry enabled them, and where Nitro writes them once it is set up. */
export type DeletionFallback = { client: boolean; server: boolean; outputDirs?: NitroOutputDirs };

/**
* Globs for the source maps in Nitro's output directories of the sides whose source maps only exist because Sentry
* enabled them. Empty until Nitro is set up.
*/
export function getFallbackFilesToDelete(fallback: DeletionFallback | undefined): string[] {
if (!fallback?.outputDirs) {
return [];
}

const { client, server, outputDirs } = fallback;
const { publicDir, serverDir } = outputDirs;
return [
...(client ? getPublicSourceMapGlobs(publicDir, server ? undefined : serverDir) : []),
...(server ? [`${toGlobPath(serverDir)}/**/*.map`] : []),
];
}

/** Some presets write the server into the public dir, e.g. Cloudflare Pages into `dist/_worker.js`. */
function getPublicSourceMapGlobs(publicDir: string, keptServerDir: string | undefined): string[] {
const publicGlob = toGlobPath(publicDir);

if (keptServerDir === undefined || !isInDirectory(keptServerDir, publicDir)) {
return [`${publicGlob}/**/*.map`];
}

const [serverDirName] = path.relative(publicDir, keptServerDir).split(path.sep);
return [`${publicGlob}/*.map`, `${publicGlob}/!(${serverDirName})/**/*.map`];
}

/** `filesToDeleteAfterUpload` is matched from the working directory and needs forward slashes, also on Windows. */
function toGlobPath(directory: string): string {
return path.relative(process.cwd(), directory).split(path.sep).join('/') || '.';
}

export function withoutSourceMapDeletion(options: Options): Options {
return {
...options,
Expand Down
28 changes: 17 additions & 11 deletions packages/nuxt/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import { warnOnRemovedBuildOptions } from '@sentry/core';
import type { NitroConfig } from 'nitropack';
import type { Plugin } from 'vite';
import type { SentryNuxtModuleOptions } from '../common/types';
import { deletePublicSourceMaps, deleteSourceMapsAfterBuild, withoutSourceMapDeletion } from './sourceMapDeletion';
import {
deletePublicSourceMaps,
deleteSourceMapsAfterBuild,
type DeletionFallback,
getFallbackFilesToDelete,
withoutSourceMapDeletion,
} from './sourceMapDeletion';
import { validateSourceMapsOptionsPlugin } from './sentryVitePlugin';

/**
Expand Down Expand Up @@ -33,7 +39,7 @@ export function setupSourceMaps(
const sourceMapsEnabled = moduleOptions.sourcemaps?.disable !== true;

// In case we overwrite the source map settings, we default to deleting the files
const shouldDeleteFilesFallback = { client: true, server: true };
const shouldDeleteFilesFallback: DeletionFallback = { client: true, server: true };

nuxt.hook('modules:done', () => {
if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) {
Expand Down Expand Up @@ -99,6 +105,9 @@ export function setupSourceMaps(

validateNitroSourceMapSettings(nuxt, nitroConfig, moduleOptions);

// `rollupConfig.output.sourcemap` overrides `sourcemap.server`, so the server source maps are the user's.
shouldDeleteFilesFallback.server &&= nitroConfig.rollupConfig.output?.sourcemap === undefined;

if (isDebug) {
// eslint-disable-next-line no-console
console.log('[Sentry] Adding Sentry Rollup plugin to the server runtime.');
Expand All @@ -115,6 +124,7 @@ export function setupSourceMaps(
nuxt.hook('nitro:init', nitro => {
if (sourceMapsEnabled && !nitro.options.dev && !nuxt.options?._prepare) {
const publicDir = nitro.options.output.publicDir;
shouldDeleteFilesFallback.outputDirs = { publicDir, serverDir: nitro.options.output.serverDir };
const rollupConfig = (nitro.options.rollupConfig ??= { output: {} });

rollupConfig.plugins = [
Expand Down Expand Up @@ -151,7 +161,7 @@ function normalizePath(path: string): string {
*/
export function getPluginOptions(
moduleOptions: SentryNuxtModuleOptions,
shouldDeleteFilesFallback?: { client: boolean; server: boolean },
shouldDeleteFilesFallback?: DeletionFallback,
): SentryVitePluginOptions | SentryRollupPluginOptions {
const sourcemapsOptions = moduleOptions.sourcemaps || {};
const filesToDeleteAfterUpload = resolveFilesToDeleteAfterUpload(moduleOptions, shouldDeleteFilesFallback);
Expand Down Expand Up @@ -200,19 +210,15 @@ export function getPluginOptions(
* (client/server) whose `sourcemap` setting resolved to `undefined`, where Sentry stepped in and
* enabled `'hidden'`. Given Nuxt's default of `{ server: true, client: false }`, this isn't the
* default case: it only kicks in when a side is left `undefined` (whole config or a single sub-key).
* Sentry deletes them in Nitro's output directories, which are only known once Nitro is set up.
* @see https://nuxt.com/docs/4.x/api/nuxt-config#sourcemap
*/
function resolveFilesToDeleteAfterUpload(
moduleOptions: SentryNuxtModuleOptions,
shouldDeleteFilesFallback?: { client: boolean; server: boolean },
shouldDeleteFilesFallback?: DeletionFallback,
): string | Array<string> | undefined {
const shouldDeleteFilesAfterUpload = shouldDeleteFilesFallback?.client || shouldDeleteFilesFallback?.server;
const fallbackFilesToDelete = [
...(shouldDeleteFilesFallback?.client ? ['.*/**/public/**/*.map'] : []),
...(shouldDeleteFilesFallback?.server
? ['.*/**/server/**/*.map', '.*/**/output/**/*.map', '.*/**/function/**/*.map']
: []),
];
const fallbackFilesToDelete = getFallbackFilesToDelete(shouldDeleteFilesFallback);
const shouldDeleteFilesAfterUpload = fallbackFilesToDelete.length > 0;

const filesToDeleteAfterUpload = moduleOptions.sourcemaps?.filesToDeleteAfterUpload;

Expand Down
Loading
Loading