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 @@ -15,6 +15,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"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { sentryWebpackPlugin } from "@sentry/bundler-plugins/webpack";
import { sentryConfig } from "../configs/sourcemaps-assets-auto.config.js";
import { resolve } from "node:path";

export default {
cache: false,
entry: {
entry1: "./src/entry1.js",
entry2: "./src/entry2.js",
},
output: {
path: resolve("./out/sourcemaps-assets-auto"),
filename: "[name].js",
},
optimization: {
minimize: false,
},
mode: "production",
plugins: [sentryWebpackPlugin(sentryConfig)],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect } from "vitest";
import { test } from "./utils";

test(import.meta.url, ({ runBundler, readOutputFiles }) => {
runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" });
const files = readOutputFiles();

expect(Object.keys(files).sort()).toMatchInlineSnapshot(`
[
"entry1.js",
"entry2.js",
"entry2.js.map",
"sentry-cli-mock.json",
"sentry-uploaded-sources.json",
]
`);
expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(`
[
[
"webpack5-integration-tests/./src/common.js",
"webpack5-integration-tests/./src/entry1.js",
],
]
`);
expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(`
"["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"],
"
`);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { sentryWebpackPlugin } from "@sentry/bundler-plugins/webpack";
import { sentryConfig } from "../configs/sourcemaps-ignore-auto.config.js";
import { resolve } from "node:path";

export default {
cache: false,
entry: {
entry1: "./src/entry1.js",
entry2: "./src/entry2.js",
},
output: {
path: resolve("./out/sourcemaps-ignore-auto"),
filename: "[name].js",
},
optimization: {
minimize: false,
},
mode: "production",
plugins: [sentryWebpackPlugin(sentryConfig)],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect } from "vitest";
import { test } from "./utils";

test(import.meta.url, ({ runBundler, readOutputFiles }) => {
runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" });
const files = readOutputFiles();

expect(Object.keys(files).sort()).toMatchInlineSnapshot(`
[
"entry1.js",
"entry2.js",
"entry2.js.map",
"sentry-cli-mock.json",
"sentry-uploaded-sources.json",
]
`);
expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(`
[
[
"webpack5-integration-tests/./src/common.js",
"webpack5-integration-tests/./src/entry1.js",
],
]
`);
expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(`
"["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"],
"
`);
});
157 changes: 155 additions & 2 deletions packages/bundler-plugins/src/webpack/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* oxlint-disable max-lines */

import type { Options } from '../core/index';
import type { Options, SourceMapDecision, SourceMapKind, UploadStaging } from '../core/index';
import {
createSentryBuildPluginManager,
generateReleaseInjectorCode,
Expand Down Expand Up @@ -71,6 +71,7 @@ type WebpackCompiler = {
options: {
plugins?: unknown[];
mode?: string;
devtool?: string | false;
module?: {
rules?: unknown[];
};
Expand All @@ -82,18 +83,25 @@ type WebpackCompiler = {
thisCompilation: {
tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void;
};
emit: {
tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void;
};
afterEmit: {
tapAsync: (name: string, callback: (compilation: WebpackCompilation, cb: () => void) => void) => void;
};
done: {
tap: (name: string, callback: () => void) => void;
};
failed?: {
tap: (name: string, callback: () => void) => void;
};
};
webpack?: {
DefinePlugin?: PluginClass;
Compilation?: {
PROCESS_ASSETS_STAGE_ADDITIONS?: number;
PROCESS_ASSETS_STAGE_DEV_TOOLING?: number;
PROCESS_ASSETS_STAGE_REPORT?: number;
};
sources?: {
RawSource?: WebpackRawSource;
Expand All @@ -116,16 +124,44 @@ type WebpackCompilation = {
getAssets: () => WebpackAsset[];
getAsset: (name: string) => WebpackAsset | undefined;
updateAsset: (name: string, source: WebpackSource) => void;
deleteAsset: (name: string) => void;
hooks: {
processAssets: {
tap: (
options: { name: string; stage: number },
callback: (assets: Record<string, WebpackSource>) => void,
) => void;
tapPromise: (options: { name: string; stage: number }, callback: () => Promise<void>) => void;
};
};
};

/**
* Maps webpack's `devtool` onto the plugin's source map kinds. A user who sets `devtool: false`
* and adds a `SourceMapDevToolPlugin` manages source maps themselves.
*/
function sourceMapKindFromDevtool(compiler: WebpackCompiler): SourceMapKind | undefined {
const devtool = compiler.options.devtool;

if (devtool === undefined) {
return 'unset';
}
if (devtool === false) {
const hasSourceMapPlugin = compiler.options.plugins?.some(
plugin =>
(plugin as { constructor?: { name?: string } } | undefined)?.constructor?.name === 'SourceMapDevToolPlugin',
);
return hasSourceMapPlugin ? undefined : 'off';
}
if (devtool.includes('inline') || devtool.includes('eval')) {
return 'inline';
}
if (devtool.includes('hidden')) {
return 'hidden';
}
return 'visible';
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}

type WebpackModule = {
version?: string;
default?: { version?: string };
Expand Down Expand Up @@ -171,6 +207,54 @@ function addDebugIdsToAssets(compilation: WebpackCompilation, RawSource: Webpack
}
}

/**
* Copies each JS asset and its source map into the upload staging folder and drops the source map
* assets (and their compressed variants) from the compilation, so webpack never emits them.
*/
async function stageSourceMaps(
compilation: WebpackCompilation,
staging: UploadStaging,
logger: { warn: (message: string) => void },
): Promise<void> {
const outputPath = compilation.outputOptions.path ?? path.resolve();

for (const asset of compilation.getAssets()) {
if (!isJsFile(asset.name)) {
continue;
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

const relatedSourceMap = asset.info.related?.sourceMap;
const sourceMapName = typeof relatedSourceMap === 'string' ? relatedSourceMap : `${asset.name}.map`;
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
const sourceMapAsset = compilation.getAsset(sourceMapName);
if (!sourceMapAsset) {
continue;
}

const result = await staging.stage({
code: asset.source.source().toString(),
mapSource: sourceMapAsset.source.source().toString(),
mapDir: path.join(outputPath, path.dirname(sourceMapName)),
chunkPath: path.join(outputPath, asset.name),
});

if (result === 'excluded') {
continue;
}
if (result !== 'staged') {
logger.warn(
`Could not prepare ${asset.name} for upload, so its source map ${sourceMapName} stays in the build output.`,
);
continue;
}

for (const candidate of compilation.getAssets()) {
if (candidate.name === sourceMapName || candidate.name.startsWith(`${sourceMapName}.`)) {
compilation.deleteAsset(candidate.name);
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}
}

function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) {
const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, {
loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? '[sentry-webpack-plugin]',
Expand Down Expand Up @@ -231,6 +315,21 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {})

const transformReplace = Object.keys(replacementValues).length > 0;

function decideSourceMaps(compiler: WebpackCompiler): SourceMapDecision {
const kind = compiler.options.mode === 'development' ? undefined : sourceMapKindFromDevtool(compiler);
if (kind === undefined) {
return { enableHidden: false, deleteAfterUpload: false };
}

const decision = sentryBuildPluginManager.sourceMaps.decide(kind);

if (decision.enableHidden) {
compiler.options.devtool = kind === 'visible' ? `hidden-${compiler.options.devtool}` : 'hidden-source-map';
}

return decision;
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

function addCodeInjection(compiler: WebpackCompiler): void {
if (releaseInjectionCode.isEmpty() && chunkInjectionCode.isEmpty() && !sourcemapsEnabled) {
return;
Expand Down Expand Up @@ -299,6 +398,47 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {})

const { DefinePlugin } = compiler.webpack ?? {};

// One plugin instance can serve several compilers (`MultiCompiler`) and
// a compiler several compilations (watch mode), so nothing is shared.
const sourceMapDecision = decideSourceMaps(compiler);
const pendingStagings = new Map<WebpackCompilation, UploadStaging>();

if (sourceMapDecision.deleteAfterUpload) {
// After `RealContentHashPlugin` rewrote the hash placeholders, so the staged
// JS equals the emitted file and `[contenthash]` names are final.
const stage = (compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_REPORT ?? 5000) + 1;

compiler.hooks.thisCompilation.tap('sentry-webpack-plugin', compilation => {
compilation.hooks.processAssets.tapPromise({ name: 'sentry-webpack-plugin', stage }, async () => {
const staging = await sentryBuildPluginManager.createUploadStaging();
pendingStagings.set(compilation, staging);
await stageSourceMaps(compilation, staging, logger);
});
});

// A staging that `afterEmit` never took belongs to a compilation that
// failed before emitting, or to one whose assets were emitted without
// their maps because an earlier `afterEmit` tap failed. Only the
// second kind has no other copy of the maps.
const emittedCompilations = new Set<WebpackCompilation>();
compiler.hooks.emit.tap('sentry-webpack-plugin', compilation => {
emittedCompilations.add(compilation);
});
const settlePendingStagings = (): void => {
for (const [compilation, staging] of pendingStagings) {
if (emittedCompilations.has(compilation)) {
void staging.keep('The source map upload did not run.');
} else {
void staging.discard();
}
}
pendingStagings.clear();
emittedCompilations.clear();
};
compiler.hooks.done.tap('sentry-webpack-plugin', settlePendingStagings);
compiler.hooks.failed?.tap('sentry-webpack-plugin', settlePendingStagings);
}

// BannerPlugin prepends executable code before directive prologues.
addCodeInjection(compiler);

Expand Down Expand Up @@ -354,14 +494,27 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {})
const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts();
const upload = createDebugIdUploadFunction({ sentryBuildPluginManager });

const staging = pendingStagings.get(compilation);
pendingStagings.delete(compilation);
let stagingHandedOver = false;

const run = async (): Promise<void> => {
try {
await sentryBuildPluginManager.createRelease();
if (sourcemapsEnabled && options.sourcemaps?.disable !== 'disable-upload') {
const outputPath = compilation.outputOptions.path ?? path.resolve();
const buildArtifacts = Object.keys(compilation.assets).map(asset => path.join(outputPath, asset));
await upload(buildArtifacts);
stagingHandedOver = true;
await upload(buildArtifacts, { staging });
} else {
stagingHandedOver = true;
await staging?.discard();
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}
} catch (e) {
if (!stagingHandedOver) {
await staging?.keep('The source map upload did not run.');
}
throw e;
} finally {
freeGlobalDependencyOnBuildArtifacts();
await sentryBuildPluginManager.deleteArtifacts();
Expand Down
Loading
Loading