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
13 changes: 8 additions & 5 deletions packages/bundler-plugins/src/core/build-plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { normalizeUserOptions, validateOptions } from './options-mapping';
import type { Logger } from './logger';
import { createLogger } from './logger';
import { allowedToSendTelemetry, createSentryInstance, safeFlushTelemetry } from './sentry/telemetry';
import type { Options, SentrySDKBuildFlags } from './types';
import type { InternalOptions, SentrySDKBuildFlags } from './types';
import { arrayify, getProjects, getTurborepoEnvPassthroughWarning, stripQueryAndHashFromPath } from './utils';
import {
defaultRewriteSourcesHook,
Expand Down Expand Up @@ -153,7 +153,7 @@ export type SentryBuildPluginManager = {
* Decides how the plugin treats the user's source map setting (see `decideSourceMapHandling`).
* Logs the decision's message once per message, unless `log` is `false`.
*/
decide(kind: SourceMapKind, options?: { log?: boolean }): SourceMapDecision;
decide(kind: SourceMapKind, options?: { log?: boolean; environment?: string }): SourceMapDecision;
/** Logs the decision's message, once per message. */
logDecision(decision: SourceMapDecision): void;
/**
Expand Down Expand Up @@ -183,7 +183,7 @@ export type SentryBuildPluginManager = {
* The build plugin manager's behavior strongly depends on the options that are passed in.
*/
export function createSentryBuildPluginManager(
userOptions: Options,
userOptions: InternalOptions,
bundlerPluginMetaContext: {
/**
* E.g. `webpack` or `nextjs` or `turbopack`
Expand Down Expand Up @@ -970,8 +970,11 @@ export function createSentryBuildPluginManager(

sourceMaps: {
uploadPossible: sourceMapUploadPossible,
decide(kind, { log = true } = {}) {
const decision = decideSourceMapHandling(kind, options, sourceMapUploadPossible);
decide(kind, { log = true, environment } = {}) {
// Watch builds in development keep their source maps.
const decision = isDevMode
? { enableHidden: false, deleteAfterUpload: false }
: decideSourceMapHandling(kind, options, sourceMapUploadPossible, environment);
if (log) {
logDecision(decision);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/bundler-plugins/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function getDebugIdSnippet(debugId: string): CodeInjection {
}

export type { Logger } from './logger';
export type { Options, SentrySDKBuildFlags } from './types';
export type { InternalOptions, Options, SentrySDKBuildFlags } from './types';
export {
CodeInjection,
replaceBooleanFlagsInCode,
Expand All @@ -112,6 +112,6 @@ export {
export { createSentryBuildPluginManager } from './build-plugin-manager';
export type { SentryBuildPluginManager, UploadResult, UploadStaging } from './build-plugin-manager';
export { createDebugIdUploadFunction, addDebugIdToEmittedArtifacts, stampDebugId } from './debug-id-upload';
export { decideSourceMapHandling } from './sourcemap-settings';
export { decideSourceMapHandling, getDeleteSourcemapsAfterUpload, getSdkEnabledSourceMaps } from './sourcemap-settings';
export type { SourceMapDecision, SourceMapKind } from './sourcemap-settings';
export type { UploadFilter } from './upload-filter';
12 changes: 3 additions & 9 deletions packages/bundler-plugins/src/core/options-mapping.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getVercelEnv } from '@sentry/core';
import type { Logger } from './logger';
import type {
Options as UserOptions,
InternalOptions as UserOptions,
SetCommitsOptions,
RewriteSourcesHook,
ResolveSourceMapHook,
Expand Down Expand Up @@ -30,7 +30,8 @@ export type NormalizedOptions = {
rewriteSources?: RewriteSourcesHook;
resolveSourceMap?: ResolveSourceMapHook;
filesToDeleteAfterUpload?: string | string[] | Promise<string | string[] | undefined>;
deleteSourcemapsAfterUpload?: boolean;
deleteSourcemapsAfterUpload?: boolean | ((environment?: string) => boolean | undefined);
sdkEnabledSourceMaps?: boolean | ((environment?: string) => boolean | undefined);
}
| undefined;
release: {
Expand Down Expand Up @@ -217,13 +218,6 @@ export function validateOptions(options: NormalizedOptions, logger: Logger): boo
return false;
}

if (options.sourcemaps?.deleteSourcemapsAfterUpload && options.sourcemaps.disable) {
logger.warn(
'The `sourcemaps.deleteSourcemapsAfterUpload` option is set but source map upload is disabled via `sourcemaps.disable`.',
'Source maps will not be deleted.',
);
}

if (options.project && Array.isArray(options.project)) {
if (options.project.length === 0) {
logger.error(
Expand Down
6 changes: 5 additions & 1 deletion packages/bundler-plugins/src/core/sentry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ export function setTelemetryDataOnScope(
scope.setTag('delete-after-upload', !!sourcemaps?.filesToDeleteAfterUpload);
scope.setTag(
'delete-sourcemaps-after-upload',
sourcemaps?.deleteSourcemapsAfterUpload === undefined ? 'auto' : String(sourcemaps.deleteSourcemapsAfterUpload),
typeof sourcemaps?.deleteSourcemapsAfterUpload === 'function'
? 'function'
: sourcemaps?.deleteSourcemapsAfterUpload === undefined
? 'auto'
: String(sourcemaps.deleteSourcemapsAfterUpload),
);
scope.setTag('sourcemaps-disabled', !!sourcemaps?.disable);

Expand Down
54 changes: 51 additions & 3 deletions packages/bundler-plugins/src/core/sourcemap-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,28 @@ export type SourceMapDecision = {
enableHidden: boolean;
/** Remove the source maps from the build output once they are uploaded. */
deleteAfterUpload: boolean;
/**
* With `deleteAfterUpload`, remove every source map of the build, also those of CSS and of chunks
* the upload leaves out. Set when the plugin or the SDK turned the source maps on.
*/
removeAllSourceMaps?: boolean;
log?: { level: 'info' | 'warn'; message: string };
};

const NO_CHANGE: SourceMapDecision = { enableHidden: false, deleteAfterUpload: false };

/** Reads `sourcemaps.deleteSourcemapsAfterUpload`, calling the internal function form if given. */
export function getDeleteSourcemapsAfterUpload(options: NormalizedOptions, environment?: string): boolean | undefined {
const value = options.sourcemaps?.deleteSourcemapsAfterUpload;
return typeof value === 'function' ? value(environment) : value;
}

/** Reads the internal `sourcemaps.sdkEnabledSourceMaps` option, calling its function form if given. */
export function getSdkEnabledSourceMaps(options: NormalizedOptions, environment?: string): boolean {
const value = options.sourcemaps?.sdkEnabledSourceMaps;
return (typeof value === 'function' ? value(environment) : value) === true;
}

/**
* Decides whether the plugin turns hidden source maps on and whether it removes the maps from the
* build output after the upload. The plugin only turns maps on when an upload can run, and only
Expand All @@ -30,11 +47,37 @@ export function decideSourceMapHandling(
kind: SourceMapKind,
options: NormalizedOptions,
uploadPossible: boolean,
environment?: string,
): SourceMapDecision {
const deleteOption = options.sourcemaps?.deleteSourcemapsAfterUpload;
const deleteOption = getDeleteSourcemapsAfterUpload(options, environment);
const sdkEnabledSourceMaps = getSdkEnabledSourceMaps(options, environment);

if (!uploadPossible || options.sourcemaps?.disable) {
return NO_CHANGE;
if (deleteOption !== true || (kind !== 'hidden' && kind !== 'visible')) {
return NO_CHANGE;
}
// Users set `sourcemaps.disable` to upload the maps themselves. Maps the
// SDK turned on are still removed, as the SDKs' own globs did.
if (options.sourcemaps?.disable && !sdkEnabledSourceMaps) {
return {
...NO_CHANGE,
log: {
level: 'warn',
message:
'`sourcemaps.deleteSourcemapsAfterUpload` is set, but source map upload is disabled with `sourcemaps.disable`, so the source maps are not removed.',
},
};
}
return {
enableHidden: kind === 'visible',
deleteAfterUpload: true,
removeAllSourceMaps: sdkEnabledSourceMaps,
log: {
level: 'warn',
message:
'Source maps are not uploaded, but they are removed from the build output because `sourcemaps.deleteSourcemapsAfterUpload` is set.',
},
};
}

switch (kind) {
Expand Down Expand Up @@ -74,6 +117,7 @@ export function decideSourceMapHandling(
return {
enableHidden: true,
deleteAfterUpload: true,
removeAllSourceMaps: true,
log: {
level: 'info',
message:
Expand All @@ -83,7 +127,10 @@ export function decideSourceMapHandling(
};

case 'hidden':
return { enableHidden: false, deleteAfterUpload: deleteOption === true };
if (deleteOption !== true) {
return NO_CHANGE;
}
return { enableHidden: false, deleteAfterUpload: true, removeAllSourceMaps: sdkEnabledSourceMaps };

case 'visible':
if (deleteOption !== true) {
Expand All @@ -92,6 +139,7 @@ export function decideSourceMapHandling(
return {
enableHidden: true,
deleteAfterUpload: true,
removeAllSourceMaps: sdkEnabledSourceMaps,
log: {
level: 'info',
message:
Expand Down
28 changes: 28 additions & 0 deletions packages/bundler-plugins/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export interface Options {
* Options related to source maps upload and processing.
*/
sourcemaps?: {
// TODO(v12): Replace `disable` with separate options, for example
// `upload`, `debugIds` and `deleteSourcemapsAfterUpload`, so that one
// option no longer controls several features.
/**
* Disables all functionality related to sourcemaps if set to `true`.
*
Expand Down Expand Up @@ -204,6 +207,10 @@ export interface Options {
* `sourcemaps.assets` or `sourcemaps.ignore` keep out of the upload. Source maps you enabled yourself are only
* removed for the JavaScript chunks that were uploaded; excluded chunks keep theirs.
*
* `true` removes source maps you enabled yourself even when no upload runs, for example without an auth token.
* With `sourcemaps.disable` set, the plugin keeps them for a manual upload and warns. The plugin does not turn
* source maps on when it cannot upload them.
*
* With webpack's array form of `devtool`, `true` replaces the array with one hidden source map setting for
* all asset types, because webpack hides the array from plugins.
*
Expand Down Expand Up @@ -668,3 +675,24 @@ type DeployOptions = {
};

export type HandleRecoverableErrorFn = (error: unknown, throwByDefault: boolean) => void;

/**
* @internal For Sentry SDK packages only. It can change in any release.
*
* Like `Options`, but `sourcemaps.deleteSourcemapsAfterUpload` can also be a function, and
* `sourcemaps.sdkEnabledSourceMaps` is added. The plugin calls the functions when it decides how
* to treat the bundler's source map setting, so an SDK can give a value that it only knows after it
* created the plugin. Vite passes the name of an environment with its own `build.sourcemap`, and
* `undefined` for the top-level `build.sourcemap`.
*/
export type InternalOptions = Omit<Options, 'sourcemaps'> & {
sourcemaps?: Omit<NonNullable<Options['sourcemaps']>, 'deleteSourcemapsAfterUpload'> & {
deleteSourcemapsAfterUpload?: boolean | ((environment?: string) => boolean | undefined);
/**
* The SDK turned on the bundler's source maps. With `deleteSourcemapsAfterUpload: true` the
* plugin then removes them like source maps it turned on itself: all of them, also those of CSS
* and of chunks that the upload leaves out.
*/
sdkEnabledSourceMaps?: boolean | ((environment?: string) => boolean | undefined);
};
};
74 changes: 42 additions & 32 deletions packages/bundler-plugins/src/esbuild/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Options, SourceMapDecision, SourceMapKind } from '../core';
import type { InternalOptions, SourceMapDecision, SourceMapKind } from '../core';
import {
createSentryBuildPluginManager,
generateReleaseInjectorCode,
Expand Down Expand Up @@ -110,7 +110,7 @@ function getEsbuildMajorVersion(): string | undefined {
const pluginName = 'sentry-esbuild-plugin';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function sentryEsbuildPlugin(userOptions: Options = {}): any {
export function sentryEsbuildPlugin(userOptions: InternalOptions = {}): any {
const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, {
loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? `[${pluginName}]`,
buildTool: 'esbuild',
Expand Down Expand Up @@ -326,36 +326,17 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any {
try {
await sentryBuildPluginManager.createRelease();

if (sourcemapsEnabled) {
// The metafile lists outputs relative to `absWorkingDir`, not to the process.
const outputDir = initialOptions.absWorkingDir ?? process.cwd();
const buildArtifacts = result.metafile
? Object.keys(result.metafile.outputs).map(output => path.resolve(outputDir, output))
: [];

if (options.sourcemaps?.disable !== 'disable-upload') {
// The upload globs the paths, and glob reads backslashes as escapes.
const { uploaded, uploadedSourceMapPaths } = await upload(
buildArtifacts.map(artifact => artifact.split(path.sep).join('/')),
);

// esbuild writes before `onEnd` runs, so the maps are deleted from disk
// afterwards instead of being dropped from the output like in other bundlers.
// A map the plugin turned on never stays in the output. A map the user
// enabled is only deleted once it was uploaded.
if (sourceMapDecision.deleteAfterUpload) {
const sourceMaps =
sourceMapKind === 'unset'
? buildArtifacts.filter(artifact => artifact.endsWith('.map'))
: uploadedSourceMapPaths;

if (uploaded) {
await sentryBuildPluginManager.sourceMaps.deleteFromOutput(sourceMaps);
} else if (sourceMaps.length > 0) {
logger.error('The source maps were not uploaded, so they stay in the build output.');
}
}
} else if (initialOptions.write === false) {
// The metafile lists outputs relative to `absWorkingDir`, not to the process.
const outputDir = initialOptions.absWorkingDir ?? process.cwd();
const buildArtifacts = result.metafile
? Object.keys(result.metafile.outputs).map(output => path.resolve(outputDir, output))
: [];

const { sourceMaps } = sentryBuildPluginManager;
const allMaps = buildArtifacts.filter(artifact => artifact.endsWith('.map'));
const deleteWithoutUpload = sourceMapDecision.deleteAfterUpload && !sourceMaps.uploadPossible;
if (sourcemapsEnabled && options.sourcemaps?.disable === 'disable-upload') {
if (initialOptions.write === false) {
logger.debug('Build output is not written to disk. Skipping debug ID injection into build artifacts.');
} else {
// The upload routine (which stamps debug IDs into temp copies of the artifacts) is
Expand All @@ -367,6 +348,35 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any {
.map(bundle => addDebugIdToEmittedArtifacts(bundle, logger, options.sourcemaps?.resolveSourceMap)),
);
}
} else if (sourcemapsEnabled) {
// The upload globs the paths, and glob reads backslashes as escapes.
// When no upload can run, this logs why.
const { uploaded, uploadedSourceMapPaths } = await upload(
buildArtifacts.map(artifact => artifact.split(path.sep).join('/')),
);

// esbuild writes before `onEnd` runs, so the maps are deleted from disk
// afterwards instead of being dropped from the output like in other bundlers.
// A map the plugin turned on never stays in the output. A map the user
// enabled is only deleted once it was uploaded.
if (sourceMapDecision.deleteAfterUpload && !deleteWithoutUpload) {
const mapsToDelete = sourceMapDecision.removeAllSourceMaps ? allMaps : uploadedSourceMapPaths;

if (uploaded) {
await sourceMaps.deleteFromOutput(mapsToDelete);
} else if (mapsToDelete.length > 0) {
logger.error('The source maps were not uploaded, so they stay in the build output.');
}
}
}

if (deleteWithoutUpload) {
const bundleOf = (map: string): string => map.slice(0, -'.map'.length);
const isUploaded = (map: string): boolean =>
isJsFile(bundleOf(map)) && sourceMaps.shouldUpload(bundleOf(map));
await sourceMaps.deleteFromOutput(
sourceMapDecision.removeAllSourceMaps ? allMaps : allMaps.filter(isUploaded),
);
}
} finally {
freeGlobalDependencyOnBuildArtifacts();
Expand Down
4 changes: 2 additions & 2 deletions packages/bundler-plugins/src/rollup/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ChunkGraphNode, Options } from '../core';
import type { ChunkGraphNode, InternalOptions, Options } from '../core';
import {
createSentryBuildPluginManager,
generateReleaseInjectorCode,
Expand Down Expand Up @@ -91,7 +91,7 @@ function getViteParseAstAsync(): Promise<ViteParseAstAsync | null> {
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function _createRollupPluginInternals(
userOptions: Options = {},
userOptions: InternalOptions = {},
buildTool: 'rollup' | 'vite',
buildToolMajorVersion?: string,
) {
Expand Down
Loading
Loading