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
127 changes: 74 additions & 53 deletions packages/bundler-plugin-core/src/build-plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,15 @@ export function createSentryBuildPluginManager(
return;
}

// Early exit if assets is explicitly set to an empty array
const assets = options.sourcemaps?.assets;
if (Array.isArray(assets) && assets.length === 0) {
logger.debug(
"Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID."
);
return;
}

await startSpan(
// This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions.
{ name: "debug-id-sourcemap-upload", scope: sentryScope, forceTransaction: true },
Expand All @@ -578,65 +587,77 @@ export function createSentryBuildPluginManager(
const freeUploadDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts();

try {
const assets = options.sourcemaps?.assets;

let globAssets: string | string[];
if (assets) {
globAssets = assets;
} else {
logger.debug(
"No `sourcemaps.assets` option provided, falling back to uploading detected build artifacts."
);
globAssets = buildArtifactPaths;
}
if (!shouldPrepare) {
// Direct CLI upload from existing artifact paths (no globbing, no preparation)
let pathsToUpload: string[];

const globResult = await startSpan(
{ name: "glob", scope: sentryScope },
async () =>
await glob(globAssets, {
absolute: true,
// If we do not use a temp folder, we allow directories and files; CLI will traverse as needed when given paths.
nodir: shouldPrepare,
ignore: options.sourcemaps?.ignore,
})
);
if (assets) {
pathsToUpload = Array.isArray(assets) ? assets : [assets];
logger.debug(
`Direct upload mode: passing user-provided assets directly to CLI: ${pathsToUpload.join(
", "
)}`
);
} else {
// Use original paths e.g. like ['.next/server'] directly –> preferred way when no globbing is done
pathsToUpload = buildArtifactPaths;
}

const debugIdChunkFilePaths = shouldPrepare
? globResult.filter((debugIdChunkFilePath) => {
return !!stripQueryAndHashFromPath(debugIdChunkFilePath).match(/\.(js|mjs|cjs)$/);
})
: globResult;
const ignorePaths = options.sourcemaps?.ignore
? Array.isArray(options.sourcemaps?.ignore)
? options.sourcemaps?.ignore
: [options.sourcemaps?.ignore]
: [];
await startSpan({ name: "upload", scope: sentryScope }, async () => {
const cliInstance = createCliInstance(options);
await cliInstance.releases.uploadSourceMaps(options.release.name ?? "undefined", {
include: [
{
paths: pathsToUpload,
rewrite: true,
dist: options.release.dist,
},
],
ignore: ignorePaths,
live: "rejectOnError",
});
});

// The order of the files output by glob() is not deterministic
// Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent
debugIdChunkFilePaths.sort();
logger.info("Successfully uploaded source maps to Sentry");
} else {
// Prepare artifacts in temp folder before uploading
let globAssets: string | string[];
if (assets) {
globAssets = assets;
} else {
logger.debug(
"No `sourcemaps.assets` option provided, falling back to uploading detected build artifacts."
);
globAssets = buildArtifactPaths;
}

if (Array.isArray(assets) && assets.length === 0) {
logger.debug(
"Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID."
);
} else if (debugIdChunkFilePaths.length === 0) {
logger.warn(
"Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
const globResult = await startSpan(
{ name: "glob", scope: sentryScope },
async () =>
await glob(globAssets, {
absolute: true,
nodir: true, // We need individual files for preparation
ignore: options.sourcemaps?.ignore,
})
);
} else {
if (!shouldPrepare) {
// Direct CLI upload from existing artifact paths (no preparation or temp copies)
await startSpan({ name: "upload", scope: sentryScope }, async () => {
const cliInstance = createCliInstance(options);
await cliInstance.releases.uploadSourceMaps(options.release.name ?? "undefined", {
include: [
{
paths: debugIdChunkFilePaths,
rewrite: false,
dist: options.release.dist,
},
],
live: "rejectOnError",
});
});

logger.info("Successfully uploaded source maps to Sentry");
const debugIdChunkFilePaths = globResult.filter((debugIdChunkFilePath) => {
return !!stripQueryAndHashFromPath(debugIdChunkFilePath).match(/\.(js|mjs|cjs)$/);
});

// The order of the files output by glob() is not deterministic
// Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent
debugIdChunkFilePaths.sort();

if (debugIdChunkFilePaths.length === 0) {
logger.warn(
"Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
);
} else {
const tmpUploadFolder = await startSpan(
{ name: "mkdtemp", scope: sentryScope },
Expand Down
91 changes: 81 additions & 10 deletions packages/bundler-plugin-core/test/build-plugin-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,6 @@ describe("createSentryBuildPluginManager", () => {
it("uploads in-place when prepareArtifacts is false", async () => {
mockCliUploadSourceMaps.mockResolvedValue(undefined);

// Return a mixture of files/dirs; in-place path should pass through as-is
mockGlob.mockResolvedValue(["/app/dist/a.js", "/app/dist/dir", "/app/dist/a.js.map"]);

const manager = createSentryBuildPluginManager(
{
authToken: "t",
Expand All @@ -123,19 +120,93 @@ describe("createSentryBuildPluginManager", () => {
include: expect.arrayContaining([
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
expect.objectContaining({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
paths: expect.arrayContaining([
"/app/dist/a.js",
"/app/dist/dir",
"/app/dist/a.js.map",
]),
rewrite: false,
// User-provided assets should be passed directly to CLI (no globbing)
paths: ["/app/dist/**/*"],
rewrite: true,
dist: "1",
}),
]),
live: "rejectOnError",
})
);
// Should not glob when prepareArtifacts is false
expect(mockGlob).not.toHaveBeenCalled();
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

it("uploads build artifact paths when prepareArtifacts is false and no assets provided", async () => {
mockCliUploadSourceMaps.mockResolvedValue(undefined);

const manager = createSentryBuildPluginManager(
{
authToken: "t",
org: "o",
project: "p",
release: { name: "some-release-name", dist: "1" },
// No assets provided
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

await manager.uploadSourcemaps([".next", "dist"], { prepareArtifacts: false });

expect(mockCliUploadSourceMaps).toHaveBeenCalledTimes(1);
expect(mockCliUploadSourceMaps).toHaveBeenCalledWith(
"some-release-name",
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
expect.objectContaining({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
include: expect.arrayContaining([
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
expect.objectContaining({
// Should use buildArtifactPaths directly
paths: [".next", "dist"],
rewrite: true,
dist: "1",
}),
]),
live: "rejectOnError",
})
);
expect(mockGlob).not.toHaveBeenCalled();
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

it("exits early when assets is an empty array", async () => {
const manager = createSentryBuildPluginManager(
{
authToken: "t",
org: "o",
project: "p",
release: { name: "some-release-name", dist: "1" },
sourcemaps: { assets: [] },
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

await manager.uploadSourcemaps([".next"], { prepareArtifacts: false });

expect(mockCliUploadSourceMaps).not.toHaveBeenCalled();
expect(mockGlob).not.toHaveBeenCalled();
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

it("exits early when assets is an empty array even for default mode", async () => {
const manager = createSentryBuildPluginManager(
{
authToken: "t",
org: "o",
project: "p",
release: { name: "some-release-name", dist: "1" },
sourcemaps: { assets: [] },
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

await manager.uploadSourcemaps([".next"]);

expect(mockCliUploadSourceMaps).not.toHaveBeenCalled();
expect(mockGlob).not.toHaveBeenCalled();
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

Expand Down
Loading