diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts index b648cee7c415..9b23588858ac 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts @@ -20,6 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts index b648cee7c415..9b23588858ac 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts @@ -20,6 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts index cd5480b81387..44729ef41b57 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts @@ -19,6 +19,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts index 1a1d23690e7d..6295b31445f3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts index 1a1d23690e7d..6295b31445f3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts index 85b1040db223..377c85d97b9a 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts @@ -10,6 +10,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts index 1a1d23690e7d..6295b31445f3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts index 1a1d23690e7d..6295b31445f3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts index 85b1040db223..377c85d97b9a 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts @@ -10,6 +10,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 396413b64939..c717b00a0603 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -118,6 +118,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { ...sourcemaps, assets: sourcemaps?.assets ?? getSourcemapsAssetsGlobs(config), filesToDeleteAfterUpload: sourcemaps?.filesToDeleteAfterUpload ?? updatedFilesToDeleteAfterUpload, + deleteSourcemapsAfterUpload: false, }, bundleSizeOptimizations: { ...bundleSizeOptimizations, diff --git a/packages/astro/test/integration/index.test.ts b/packages/astro/test/integration/index.test.ts index 42e9af789247..c19a9efb6497 100644 --- a/packages/astro/test/integration/index.test.ts +++ b/packages/astro/test/integration/index.test.ts @@ -118,6 +118,7 @@ describe('sentryAstro integration', () => { './dist/**/_astro/**/*.map', './dist/**/.prerender/**/*.map', ], + deleteSourcemapsAfterUpload: false, }, _metaOptions: { telemetry: { @@ -154,6 +155,7 @@ describe('sentryAstro integration', () => { './dist/**/_astro/**/*.map', './dist/**/.prerender/**/*.map', ], + deleteSourcemapsAfterUpload: false, }, _metaOptions: { telemetry: { @@ -198,6 +200,7 @@ describe('sentryAstro integration', () => { './dist/**/_astro/**/*.map', './dist/**/.prerender/**/*.map', ], + deleteSourcemapsAfterUpload: false, }, _metaOptions: { telemetry: { @@ -242,6 +245,7 @@ describe('sentryAstro integration', () => { './dist/**/_astro/**/*.map', './dist/**/.prerender/**/*.map', ], + deleteSourcemapsAfterUpload: false, }, _metaOptions: { telemetry: { diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts index 227f162eae8f..29badf550597 100644 --- a/packages/bundler-plugins/src/rollup/index.ts +++ b/packages/bundler-plugins/src/rollup/index.ts @@ -21,6 +21,7 @@ import * as path from 'node:path'; import { createRequire } from 'node:module'; import { finalizeRolldownDebugIds, getDebugIdForChunk, hasExistingDebugID } from './debug-id-injection'; import { stampDebugIds, type OutputBundle } from './debug-id-stamping'; +import { createSourceMapHandling, type OutputDestinationOptions } from './source-map-handling'; // The subset of Rollup's `TransformResult` that this plugin's `transform` // hook actually returns. Defined locally instead of imported from `rollup` @@ -81,10 +82,14 @@ function getViteParseAstAsync(): Promise { } /** - * @ignore - this is the internal plugin factory function only used for the Vite plugin! + * @ignore - internal plugin factory shared by the Rollup and Vite plugins. + * + * `main` carries injection, release creation and the upload. `post` runs last in `generateBundle`, + * copies chunks and their source maps into the upload staging folder and drops the map assets + * from the bundle, so the bundler never writes them. */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function _rollupPluginInternal( +export function _createRollupPluginInternals( userOptions: Options = {}, buildTool: 'rollup' | 'vite', buildToolMajorVersion?: string, @@ -105,7 +110,10 @@ export function _rollupPluginInternal( if (options.disable) { return { - name: 'sentry-noop-plugin', + main: { + name: 'sentry-noop-plugin', + }, + post: undefined, }; } @@ -116,6 +124,7 @@ export function _rollupPluginInternal( const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); const sourcemapsEnabled = options.sourcemaps?.disable !== true; + const sourceMapHandling = createSourceMapHandling(sentryBuildPluginManager); const releaseInjectionCode = new CodeInjection(); const chunkInjectionCode = new CodeInjection(); @@ -256,9 +265,11 @@ export function _rollupPluginInternal( } async function writeBundle( - outputOptions: { dir?: string; file?: string }, + outputOptions: OutputDestinationOptions, bundle: { [fileName: string]: unknown }, ): Promise { + const staging = sourceMapHandling.takeStaging(outputOptions); + try { await sentryBuildPluginManager.createRelease(); @@ -274,13 +285,15 @@ export function _rollupPluginInternal( '/**/*.cjs.map', ].map(q => `${q}?(\\?*)?(#*)`); // We want to allow query and hash strings at the end of files const buildArtifacts = await globFiles(JS_AND_MAP_PATTERNS, { root: outputDir }); - await upload(buildArtifacts); + await upload(buildArtifacts, { staging }); } else if (outputOptions.file) { - await upload([outputOptions.file]); + await upload([outputOptions.file], { staging }); } else { const buildArtifacts = Object.keys(bundle).map(asset => path.join(path.resolve(), asset)); - await upload(buildArtifacts); + await upload(buildArtifacts, { staging }); } + } else { + await staging?.discard(); } } finally { freeGlobalDependencyOnBuildArtifacts(); @@ -297,21 +310,44 @@ export function _rollupPluginInternal( } : transform; - return { + const main = { name, buildStart, ...(shouldTransform ? { transform: transformHook } : {}), + // Vite warns about `rollupOptions.output.sourcemap`, so the Vite plugin + // decides in its `config` hook instead. + ...(buildTool === 'rollup' ? { outputOptions: sourceMapHandling.outputOptions } : {}), renderChunk, generateBundle: { order: 'pre' as const, handler: generateBundle }, writeBundle, }; + + const post = { + name: `sentry-${buildTool}-upload-staging`, + generateBundle: { order: 'post' as const, handler: sourceMapHandling.stageSourceMaps }, + }; + + return { main, post }; +} + +/** + * @ignore - this is the internal plugin factory function only used for the Vite plugin! + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function _rollupPluginInternal( + userOptions: Options = {}, + buildTool: 'rollup' | 'vite', + buildToolMajorVersion?: string, +) { + return _createRollupPluginInternals(userOptions, buildTool, buildToolMajorVersion).main; } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any export function sentryRollupPlugin(userOptions: Options = {}): any { + const { main, post } = _createRollupPluginInternals(userOptions, 'rollup'); // We return an array here so we don't break backwards compatibility with what // unplugin used to return - return [_rollupPluginInternal(userOptions, 'rollup')]; + return post ? [main, post] : [main]; } export type { Options as SentryRollupPluginOptions } from '../core'; diff --git a/packages/bundler-plugins/src/rollup/source-map-handling.ts b/packages/bundler-plugins/src/rollup/source-map-handling.ts new file mode 100644 index 000000000000..79c96ec9d0b7 --- /dev/null +++ b/packages/bundler-plugins/src/rollup/source-map-handling.ts @@ -0,0 +1,118 @@ +import * as path from 'node:path'; +import type { SentryBuildPluginManager, SourceMapDecision, SourceMapKind, UploadStaging } from '../core'; +import { isJsFile } from '../core'; +import type { OutputBundle } from './debug-id-stamping'; + +export type RollupSourceMapSetting = boolean | 'inline' | 'hidden' | undefined; + +export type OutputDestinationOptions = { dir?: string; file?: string }; + +/** Maps Rollup's (and Vite's) `sourcemap` setting onto the plugin's source map kinds. */ +export function sourceMapKindFromRollupSetting(setting: RollupSourceMapSetting): SourceMapKind { + switch (setting) { + case undefined: + return 'unset'; + case false: + return 'off'; + case true: + return 'visible'; + default: + return setting; + } +} + +/** The directory an output writes to. Decisions and stagings are keyed by it. */ +export function getOutputDestination(outputOptions: OutputDestinationOptions): string { + return path.resolve(outputOptions.dir ?? path.dirname(outputOptions.file ?? '.')); +} + +export type SourceMapHandling = { + /** Rollup's `outputOptions` hook: decides for the output and switches it to hidden source maps when needed. */ + outputOptions: ( + rawOutputOptions: OutputDestinationOptions & { sourcemap?: RollupSourceMapSetting }, + ) => (OutputDestinationOptions & { sourcemap?: RollupSourceMapSetting }) | null; + /** Records the decision for an output destination, for bundlers that decide outside `outputOptions`. */ + setDecision: (destination: string, decision: SourceMapDecision) => void; + /** + * `generateBundle` with `order: 'post'`: copies each chunk and its source map into a staging folder + * and drops the map asset from the bundle, so the bundler never writes it. + */ + stageSourceMaps: (outputOptions: OutputDestinationOptions, bundle: OutputBundle, isWrite?: boolean) => Promise; + /** Hands the staging of this output to `writeBundle`, first in first out. */ + takeStaging: (outputOptions: OutputDestinationOptions) => UploadStaging | undefined; +}; + +/** + * Source map decisions and upload stagings of one plugin instance. Keyed by output destination + * because one instance can serve several outputs, and watch mode or several `write()` calls + * rebuild the same one. + */ +export function createSourceMapHandling(sentryBuildPluginManager: SentryBuildPluginManager): SourceMapHandling { + const decisions = new Map(); + const pendingStagings = new Map(); + + return { + outputOptions: rawOutputOptions => { + const decision = sentryBuildPluginManager.sourceMaps.decide( + sourceMapKindFromRollupSetting(rawOutputOptions.sourcemap), + ); + decisions.set(getOutputDestination(rawOutputOptions), decision); + + return decision.enableHidden ? { ...rawOutputOptions, sourcemap: 'hidden' } : null; + }, + + setDecision: (destination, decision) => { + decisions.set(destination, decision); + }, + + stageSourceMaps: async (outputOptions, bundle, isWrite) => { + // `generate()` never reaches `writeBundle`, so nothing would consume the staging. + if (isWrite === false) { + return; + } + + const destination = getOutputDestination(outputOptions); + if (!decisions.get(destination)?.deleteAfterUpload) { + return; + } + + const staging = await sentryBuildPluginManager.createUploadStaging(); + + for (const output of Object.values(bundle)) { + if (output.type !== 'chunk' || !isJsFile(output.fileName)) { + continue; + } + + const mapFileName = output.sourcemapFileName ?? `${output.fileName}.map`; + const mapAsset = bundle[mapFileName]; + if (mapAsset?.type !== 'asset' || typeof mapAsset.source !== 'string') { + continue; + } + + const result = await staging.stage({ + code: output.code, + mapSource: mapAsset.source, + mapDir: path.join(destination, path.dirname(output.fileName)), + fileName: output.fileName, + }); + + if (result === 'staged') { + // oxlint-disable-next-line typescript/no-dynamic-delete -- Rollup drops an asset when its key is deleted from the bundle + delete bundle[mapFileName]; + } else { + sentryBuildPluginManager.logger.warn( + `Could not prepare ${output.fileName} for upload, so its source map ${mapFileName} stays in the build output.`, + ); + } + } + + const stagings = pendingStagings.get(destination) ?? []; + stagings.push(staging); + pendingStagings.set(destination, stagings); + }, + + takeStaging: outputOptions => { + return pendingStagings.get(getOutputDestination(outputOptions))?.shift(); + }, + }; +} diff --git a/packages/bundler-plugins/test/rollup/delete-sourcemaps-after-upload.test.ts b/packages/bundler-plugins/test/rollup/delete-sourcemaps-after-upload.test.ts new file mode 100644 index 000000000000..adcd34c3918d --- /dev/null +++ b/packages/bundler-plugins/test/rollup/delete-sourcemaps-after-upload.test.ts @@ -0,0 +1,257 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { rollup } from 'rollup'; +import { rolldown } from 'rolldown'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { sentryRollupPlugin } from '../../src/rollup'; +import type { Options } from '../../src/core'; + +const { mockSourcemapUpload } = vi.hoisted(() => ({ mockSourcemapUpload: vi.fn() })); + +vi.mock('sentry', () => ({ + createSentrySDK: () => ({ + release: { create: vi.fn(), finalize: vi.fn(), 'set-commits': vi.fn(), deploy: vi.fn() }, + sourcemap: { upload: mockSourcemapUpload, inject: vi.fn() }, + run: vi.fn(), + }), +})); + +const DEBUG_ID_MARKER = /sentry-dbid-([0-9a-f-]{36})/; + +const credentials: Options = { + authToken: 't', + org: 'o', + project: 'p', + release: { name: 'test-release', create: false, finalize: false }, + telemetry: false, + silent: true, +}; + +type Upload = { directory: string; files: Record }; + +describe('sourcemaps.deleteSourcemapsAfterUpload (rollup / rolldown)', () => { + let tmpDir: string; + let entry: string; + let outDir: string; + let uploads: Upload[]; + + beforeEach(() => { + vi.clearAllMocks(); + uploads = []; + mockSourcemapUpload.mockImplementation(async ({ directory }: { directory: string }) => { + const files: Record = {}; + for (const file of fs.readdirSync(directory)) { + files[file] = fs.readFileSync(path.join(directory, file), 'utf8'); + } + uploads.push({ directory, files }); + }); + + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-rollup-delete-maps-')); + entry = path.join(tmpDir, 'entry.js'); + outDir = path.join(tmpDir, 'dist'); + fs.writeFileSync(entry, 'export const answer = 42;\nconsole.log(answer);\n'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function expectUploadedPair(upload: Upload, chunk: string): void { + const debugId = chunk.match(DEBUG_ID_MARKER)?.[1]; + expect(debugId).toBeDefined(); + expect(Object.keys(upload.files).sort()).toEqual([`${debugId}-0.js`, `${debugId}-0.js.map`]); + expect(upload.files[`${debugId}-0.js`]).toContain(`//# debugId=${debugId}`); + expect(JSON.parse(upload.files[`${debugId}-0.js.map`]!)).toMatchObject({ debug_id: debugId }); + expect(fs.existsSync(upload.directory)).toBe(false); + } + + describe.each([ + ['rollup', rollup], + ['rolldown', rolldown], + ])('%s', (_, bundle) => { + it('enables hidden source maps, uploads them and leaves none in the output when the setting is unset', async () => { + const build = await bundle({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + const chunk = fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8'); + expect(chunk).not.toContain('sourceMappingURL'); + + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, chunk); + }); + + it('stages chunks whose debug ID the bundler injects itself', async () => { + const build = await bundle({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir, sourcemapDebugIds: true }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + const chunk = fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8'); + expect(chunk).toContain('//# debugId='); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, chunk); + }); + + it('switches visible source maps to hidden and removes them when the option is true', async () => { + const build = await bundle({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { deleteSourcemapsAfterUpload: true } })], + }); + await build.write({ dir: outDir, sourcemap: true }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + const chunk = fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8'); + expect(chunk).not.toContain('sourceMappingURL'); + + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, chunk); + }); + }); + + it('keeps hidden source maps the user enabled and uploads them from disk', async () => { + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir, sourcemap: 'hidden' }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + }); + + it('removes hidden source maps the user enabled when the option is true', async () => { + const build = await rollup({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { deleteSourcemapsAfterUpload: true } })], + }); + await build.write({ dir: outDir, sourcemap: 'hidden' }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(uploads).toHaveLength(1); + }); + + it('keeps visible source maps when the option is unset', async () => { + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir, sourcemap: true }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')).toContain('//# sourceMappingURL=entry.js.map'); + }); + + it('does not enable source maps when the option is false', async () => { + const build = await rollup({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { deleteSourcemapsAfterUpload: false } })], + }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(uploads).toHaveLength(0); + }); + + it('warns when source maps are turned off', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin({ ...credentials, silent: false })] }); + await build.write({ dir: outDir, sourcemap: false }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Source map generation is turned off')); + }); + + it('warns when source maps are inlined and keeps them', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const build = await rollup({ + input: entry, + plugins: [ + sentryRollupPlugin({ ...credentials, silent: false, sourcemaps: { deleteSourcemapsAfterUpload: true } }), + ], + }); + await build.write({ dir: outDir, sourcemap: 'inline' }); + + expect(fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')).toContain('sourceMappingURL=data:'); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Source maps are inlined')); + }); + + it('does not enable source maps when no upload can run', async () => { + const mkdtempSpy = vi.spyOn(fs.promises, 'mkdtemp'); + const build = await rollup({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, authToken: undefined })], + }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(uploads).toHaveLength(0); + expect(mkdtempSpy).not.toHaveBeenCalled(); + }); + + it('does not create a staging folder for generate()', async () => { + const mkdtempSpy = vi.spyOn(fs.promises, 'mkdtemp'); + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.generate({ dir: outDir }); + + expect(mkdtempSpy).not.toHaveBeenCalled(); + expect(uploads).toHaveLength(0); + }); + + it('uploads each output on its own', async () => { + const otherOutDir = path.join(tmpDir, 'dist-other'); + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir }); + await build.write({ dir: otherOutDir, entryFileNames: 'other.js' }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(fs.readdirSync(otherOutDir)).toEqual(['other.js']); + expect(uploads).toHaveLength(2); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + expectUploadedPair(uploads[1]!, fs.readFileSync(path.join(otherOutDir, 'other.js'), 'utf8')); + }); + + it('removes the map asset before other plugins see the written bundle', async () => { + let writtenFiles: string[] = []; + const build = await rollup({ + input: entry, + plugins: [ + sentryRollupPlugin(credentials), + { + name: 'observer', + writeBundle(_options: unknown, bundle: Record) { + writtenFiles = Object.keys(bundle); + }, + }, + ], + }); + await build.write({ dir: outDir }); + + expect(writtenFiles).toEqual(['entry.js']); + }); + + it('keeps the map of a chunk that already carries a debug ID instead of dropping it unuploaded', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const build = await rollup({ + input: entry, + plugins: [ + { + name: 'foreign-debug-id', + renderChunk: (code: string) => `${code}\n//# debugId=11111111-2222-3333-4444-555555555555`, + }, + sentryRollupPlugin({ ...credentials, silent: false }), + ], + }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('stays in the build output')); + }); + + it('leaves "disable-upload" builds untouched', async () => { + const build = await rollup({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { disable: 'disable-upload' } })], + }); + await build.write({ dir: outDir, sourcemap: true }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(uploads).toHaveLength(0); + }); +}); diff --git a/packages/bundler-plugins/test/rollup/public-api.test.ts b/packages/bundler-plugins/test/rollup/public-api.test.ts index aee8cdb5034b..85fa16562da1 100644 --- a/packages/bundler-plugins/test/rollup/public-api.test.ts +++ b/packages/bundler-plugins/test/rollup/public-api.test.ts @@ -111,7 +111,7 @@ describe('sentryRollupPlugin', () => { vi.clearAllMocks(); }); - it('returns an array of rollup plugins (although only one)', () => { + it('returns the main plugin and the upload staging plugin', () => { const plugins = sentryRollupPlugin({ authToken: 'test-token', org: 'test-org', @@ -119,9 +119,13 @@ describe('sentryRollupPlugin', () => { }) as Plugin[]; expect(Array.isArray(plugins)).toBe(true); - expect(plugins).toHaveLength(1); + expect(plugins.map(plugin => plugin.name)).toEqual(['sentry-rollup-plugin', 'sentry-rollup-upload-staging']); + }); + + it('returns only the noop plugin when disabled', () => { + const plugins = sentryRollupPlugin({ disable: true }) as Plugin[]; - expect(plugins[0]?.name).toBe('sentry-rollup-plugin'); + expect(plugins.map(plugin => plugin.name)).toEqual(['sentry-noop-plugin']); }); }); diff --git a/packages/gatsby/gatsby-node.js b/packages/gatsby/gatsby-node.js index f6fb559f68d8..9e9022476c21 100644 --- a/packages/gatsby/gatsby-node.js +++ b/packages/gatsby/gatsby-node.js @@ -32,6 +32,7 @@ exports.onCreateWebpackConfig = ({ getConfig, actions }, options) => { assets: ['./public/**'], // Delete source files after uploading filesToDeleteAfterUpload: deleteSourcemapsAfterUpload ? ['./public/**/*.map'] : undefined, + deleteSourcemapsAfterUpload: false, // Ignore files that aren't users' source code related ignore: [ 'polyfill-*', // related to polyfills diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index fb3defba4c50..4a1fa5d57278 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -348,6 +348,7 @@ export function getBuildPluginOptions({ assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets, ignore: finalIgnorePatterns, filesToDeleteAfterUpload, + deleteSourcemapsAfterUpload: false, resolveSourceMap: sentryBuildOptions.sourcemaps?.resolveSourceMap, }, release: createReleaseConfig(releaseName, sentryBuildOptions), diff --git a/packages/nuxt/src/vite/sourceMapDeletion.ts b/packages/nuxt/src/vite/sourceMapDeletion.ts index 7f9784e3bd37..874381d66b36 100644 --- a/packages/nuxt/src/vite/sourceMapDeletion.ts +++ b/packages/nuxt/src/vite/sourceMapDeletion.ts @@ -48,6 +48,7 @@ export function withoutSourceMapDeletion(options: Options): Options { sourcemaps: { ...options.sourcemaps, filesToDeleteAfterUpload: undefined, + deleteSourcemapsAfterUpload: false, }, }; } diff --git a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts index b6bc0b5109ba..ac10b8a871df 100644 --- a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts +++ b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts @@ -247,6 +247,7 @@ describe('setupSourceMaps hooks', () => { assets: undefined, ignore: undefined, filesToDeleteAfterUpload: undefined, + deleteSourcemapsAfterUpload: false, rewriteSources: expect.any(Function), }, }); diff --git a/packages/remix/src/vite/sourceMaps.ts b/packages/remix/src/vite/sourceMaps.ts index e53860f39c28..ac392a89637e 100644 --- a/packages/remix/src/vite/sourceMaps.ts +++ b/packages/remix/src/vite/sourceMaps.ts @@ -97,6 +97,7 @@ export function makeAddSentryVitePlugin(options: SentryRemixVitePluginOptions): rewriteSources: sourcemaps?.rewriteSources, resolveSourceMap: sourcemaps?.resolveSourceMap, filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise, + deleteSourcemapsAfterUpload: false, }, telemetry: telemetry ?? true, url: sentryUrl, diff --git a/packages/solidstart/src/vite/sourceMaps.ts b/packages/solidstart/src/vite/sourceMaps.ts index 6e48ab647077..641686359d5c 100644 --- a/packages/solidstart/src/vite/sourceMaps.ts +++ b/packages/solidstart/src/vite/sourceMaps.ts @@ -66,6 +66,7 @@ export function makeAddSentryVitePlugin(options: SentrySolidStartPluginOptions, sourcemaps: { ...sourcemaps, filesToDeleteAfterUpload: sourcemaps?.filesToDeleteAfterUpload ?? updatedFilesToDeleteAfterUpload, + deleteSourcemapsAfterUpload: false, }, _metaOptions: { telemetry: { @@ -140,6 +141,7 @@ export function makeAddSentryVitePluginSolidStart2(options: SentrySolidStartOpti sourcemaps: { ...sourcemaps, filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise, + deleteSourcemapsAfterUpload: false, }, _metaOptions: { telemetry: { diff --git a/packages/solidstart/test/vite/sourceMaps.test.ts b/packages/solidstart/test/vite/sourceMaps.test.ts index d1d82b4866e7..831bedaf7a07 100644 --- a/packages/solidstart/test/vite/sourceMaps.test.ts +++ b/packages/solidstart/test/vite/sourceMaps.test.ts @@ -104,6 +104,7 @@ describe('makeAddSentryVitePlugin()', () => { moduleMetadata: { team: 'sdk' }, sourcemaps: { filesToDeleteAfterUpload: ['baz/*.js'], + deleteSourcemapsAfterUpload: false, }, bundleSizeOptimizations: { excludeTracing: true, diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 7cfa82ed6f6d..7afd0ff088d1 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -77,6 +77,7 @@ export async function makeCustomSentryVitePlugins( sourcemaps: { ...options?.sourcemaps, filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise, + deleteSourcemapsAfterUpload: false, }, }; diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index d4f2d3c06db8..5c6134f71ca5 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -385,6 +385,7 @@ describe('deleteFilesAfterUpload', () => { }, sourcemaps: { filesToDeleteAfterUpload: expect.any(Promise), + deleteSourcemapsAfterUpload: false, }, }); @@ -463,6 +464,7 @@ describe('deleteFilesAfterUpload', () => { }, sourcemaps: { filesToDeleteAfterUpload: expect.any(Promise), + deleteSourcemapsAfterUpload: false, }, }); diff --git a/packages/tanstackstart-react/src/vite/sourceMaps.ts b/packages/tanstackstart-react/src/vite/sourceMaps.ts index f84c52692899..51a295c7da3b 100644 --- a/packages/tanstackstart-react/src/vite/sourceMaps.ts +++ b/packages/tanstackstart-react/src/vite/sourceMaps.ts @@ -113,6 +113,7 @@ export function makeAddSentryVitePlugin(options: SentryTanstackStartOptions): Pl rewriteSources: sourcemaps?.rewriteSources ?? rewriteTanstackStartSource, resolveSourceMap: sourcemaps?.resolveSourceMap, filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise, + deleteSourcemapsAfterUpload: false, }, reactComponentAnnotation: { enabled: reactComponentAnnotation?.enabled ?? undefined,