diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.d.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.d.ts new file mode 100644 index 000000000000..192b614bc29b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.d.ts @@ -0,0 +1,2 @@ +import type { SentryRollupPluginOptions } from "@sentry/bundler-plugins/rollup"; +export declare const sentryConfig: SentryRollupPluginOptions; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.js new file mode 100644 index 000000000000..91b4c2a30c83 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.js @@ -0,0 +1,6 @@ +export const sentryConfig = { + telemetry: false, + sourcemaps: { + disable: "disable-upload", + }, +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.config.ts new file mode 100644 index 000000000000..aec427a21800 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.config.ts @@ -0,0 +1,19 @@ +import { sentryVitePlugin } from "@sentry/bundler-plugins/vite"; +import { defineConfig } from "vite"; +import { sentryConfig } from "../configs/sourcemaps-disable-upload.config.js"; + +export default defineConfig({ + build: { + minify: false, + sourcemap: true, + rollupOptions: { + input: "src/dynamic-import.js", + output: { + dir: "out/sourcemaps-disable-upload", + entryFileNames: "[name].js", + chunkFileNames: "[name].js", + }, + }, + }, + plugins: [sentryVitePlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.test.ts new file mode 100644 index 000000000000..a3c65f8e7755 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { test } from "./utils"; + +// Vite rewrites the source map of chunks with dynamic imports after the Sentry plugin stamps it. +// The debug ID must survive that for every chunk. Files are read raw here because +// `readOutputFiles` zeroes debug IDs, which would hide a mismatch between chunk and map. +test(import.meta.url, ({ runBundler, outDir }) => { + runBundler(); + + const entry = readFileSync(join(outDir, "dynamic-import.js"), "utf-8"); + expect(entry).toContain("__vitePreload"); + + for (const fileName of ["dynamic-import.js", "import.js"]) { + const chunk = readFileSync(join(outDir, fileName), "utf-8"); + const debugId = chunk.match(/sentry-dbid-([0-9a-f-]{36})/)?.[1]; + const map = JSON.parse(readFileSync(join(outDir, `${fileName}.map`), "utf-8")); + + expect(debugId).toBeDefined(); + expect(chunk).toContain(`//# debugId=${debugId}`); + expect(map.debug_id).toBe(debugId); + expect(map.debugId).toBe(debugId); + } +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/src/dynamic-import.js b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/src/dynamic-import.js new file mode 100644 index 000000000000..717d73bf4fe3 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/src/dynamic-import.js @@ -0,0 +1,5 @@ +export async function load() { + return import("./import.js"); +} + +load(); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.config.ts new file mode 100644 index 000000000000..aec427a21800 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.config.ts @@ -0,0 +1,19 @@ +import { sentryVitePlugin } from "@sentry/bundler-plugins/vite"; +import { defineConfig } from "vite"; +import { sentryConfig } from "../configs/sourcemaps-disable-upload.config.js"; + +export default defineConfig({ + build: { + minify: false, + sourcemap: true, + rollupOptions: { + input: "src/dynamic-import.js", + output: { + dir: "out/sourcemaps-disable-upload", + entryFileNames: "[name].js", + chunkFileNames: "[name].js", + }, + }, + }, + plugins: [sentryVitePlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.test.ts new file mode 100644 index 000000000000..a3c65f8e7755 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { test } from "./utils"; + +// Vite rewrites the source map of chunks with dynamic imports after the Sentry plugin stamps it. +// The debug ID must survive that for every chunk. Files are read raw here because +// `readOutputFiles` zeroes debug IDs, which would hide a mismatch between chunk and map. +test(import.meta.url, ({ runBundler, outDir }) => { + runBundler(); + + const entry = readFileSync(join(outDir, "dynamic-import.js"), "utf-8"); + expect(entry).toContain("__vitePreload"); + + for (const fileName of ["dynamic-import.js", "import.js"]) { + const chunk = readFileSync(join(outDir, fileName), "utf-8"); + const debugId = chunk.match(/sentry-dbid-([0-9a-f-]{36})/)?.[1]; + const map = JSON.parse(readFileSync(join(outDir, `${fileName}.map`), "utf-8")); + + expect(debugId).toBeDefined(); + expect(chunk).toContain(`//# debugId=${debugId}`); + expect(map.debug_id).toBe(debugId); + expect(map.debugId).toBe(debugId); + } +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/src/dynamic-import.js b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/src/dynamic-import.js new file mode 100644 index 000000000000..717d73bf4fe3 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/src/dynamic-import.js @@ -0,0 +1,5 @@ +export async function load() { + return import("./import.js"); +} + +load(); diff --git a/packages/bundler-plugins/src/rollup/debug-id-stamping.ts b/packages/bundler-plugins/src/rollup/debug-id-stamping.ts new file mode 100644 index 000000000000..f2d3d959df97 --- /dev/null +++ b/packages/bundler-plugins/src/rollup/debug-id-stamping.ts @@ -0,0 +1,32 @@ +import { isJsFile, stampDebugId } from '../core'; + +// The subset of Rollup's `OutputBundle` the stamping hooks read. +export type OutputBundle = Record< + string, + | { type: 'chunk'; fileName: string; code: string; sourcemapFileName?: string | null } + | { type: 'asset'; fileName: string; source: string | Uint8Array } +>; + +export function stampDebugIds(bundle: OutputBundle, updateChunkCode: boolean): void { + for (const output of Object.values(bundle)) { + if (output.type !== 'chunk' || !isJsFile(output.fileName)) { + continue; + } + + const sourceMapAsset = bundle[output.sourcemapFileName ?? `${output.fileName}.map`]; + const sourceMapSource = + sourceMapAsset?.type === 'asset' && typeof sourceMapAsset.source === 'string' ? sourceMapAsset.source : undefined; + + const stamped = stampDebugId(output.code, sourceMapSource); + if (!stamped) { + continue; + } + + if (updateChunkCode) { + output.code = stamped.bundleSource; + } + if (stamped.sourceMapSource !== undefined && sourceMapAsset?.type === 'asset') { + sourceMapAsset.source = stamped.sourceMapSource; + } + } +} diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts index 935163f961cc..b3cc6861f55e 100644 --- a/packages/bundler-plugins/src/rollup/index.ts +++ b/packages/bundler-plugins/src/rollup/index.ts @@ -11,7 +11,6 @@ import { createComponentNameAnnotateHooks, replaceBooleanFlagsInCode, CodeInjection, - stampDebugId, getCodeInjectionPosition, } from '../core'; import type { ComponentAnnotationTransformMeta } from '../core/component-annotation-oxc'; @@ -20,19 +19,13 @@ import MagicString from 'magic-string'; 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'; // The subset of Rollup's `TransformResult` that this plugin's `transform` // hook actually returns. Defined locally instead of imported from `rollup` // because `rollup` is an optional dependency. type TransformResult = { code: string; map?: SourceMap | string | { mappings: string } | null } | null | undefined; -// The subset of Rollup's `OutputBundle` the stamping hook reads. -type OutputBundle = Record< - string, - | { type: 'chunk'; fileName: string; code: string; sourcemapFileName?: string | null } - | { type: 'asset'; fileName: string; source: string | Uint8Array } ->; - type ViteModule = { parseAstAsync?: (code: string, options: { lang: 'jsx' | 'tsx' }) => Promise; }; @@ -251,27 +244,7 @@ export function _rollupPluginInternal( return; } - for (const output of Object.values(bundle)) { - if (output.type !== 'chunk' || !isJsFile(output.fileName)) { - continue; - } - - const sourceMapAsset = bundle[output.sourcemapFileName ?? `${output.fileName}.map`]; - const sourceMapSource = - sourceMapAsset?.type === 'asset' && typeof sourceMapAsset.source === 'string' - ? sourceMapAsset.source - : undefined; - - const stamped = stampDebugId(output.code, sourceMapSource); - if (!stamped) { - continue; - } - - output.code = stamped.bundleSource; - if (stamped.sourceMapSource !== undefined && sourceMapAsset?.type === 'asset') { - sourceMapAsset.source = stamped.sourceMapSource; - } - } + stampDebugIds(bundle, true); } async function writeBundle( diff --git a/packages/bundler-plugins/src/vite/index.ts b/packages/bundler-plugins/src/vite/index.ts index ea24f64e5d73..d9b06efd9c25 100644 --- a/packages/bundler-plugins/src/vite/index.ts +++ b/packages/bundler-plugins/src/vite/index.ts @@ -1,10 +1,15 @@ import type { SentryRollupPluginOptions } from '../rollup'; import { _rollupPluginInternal } from '../rollup'; import { createRequire } from 'node:module'; +import { stampDebugIds, type OutputBundle } from '../rollup/debug-id-stamping'; interface SentryVitePlugin { name: string; - enforce: 'pre'; + enforce: 'pre' | 'post'; + generateBundle?: { + order: 'pre' | 'post'; + handler: (_outputOptions: unknown, bundle: OutputBundle) => void; + }; } function getViteMajorVersion(): string | undefined { @@ -22,12 +27,30 @@ function getViteMajorVersion(): string | undefined { } export const sentryVitePlugin = (options?: SentryRollupPluginOptions): SentryVitePlugin[] => { - return [ + const plugins: SentryVitePlugin[] = [ { enforce: 'pre', ..._rollupPluginInternal(options, 'vite', getViteMajorVersion()), }, ]; + + if (!options?.disable && options?.sourcemaps?.disable === 'disable-upload') { + plugins.push({ + name: 'sentry-vite-debug-id-sourcemaps', + enforce: 'post', + generateBundle: { + order: 'post', + handler(_outputOptions, bundle) { + // Vite's module preload rewrite in `generateBundle` regenerates the source map of every + // chunk with a dynamic import, which drops the debug ID the main plugin stamped in its + // `pre` hook. Restore it without touching the chunk code, so file hashes stay valid. + stampDebugIds(bundle, false); + }, + }, + }); + } + + return plugins; }; export type { Options as SentryVitePluginOptions } from '../core';