Skip to content

Commit 07c25ce

Browse files
authored
feat(core): No asset globbing for direct upload (#800)
1 parent be45621 commit 07c25ce

File tree

2 files changed

+155
-63
lines changed

2 files changed

+155
-63
lines changed

packages/bundler-plugin-core/src/build-plugin-manager.ts

Lines changed: 74 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,15 @@ export function createSentryBuildPluginManager(
564564
return;
565565
}
566566

567+
// Early exit if assets is explicitly set to an empty array
568+
const assets = options.sourcemaps?.assets;
569+
if (Array.isArray(assets) && assets.length === 0) {
570+
logger.debug(
571+
"Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID."
572+
);
573+
return;
574+
}
575+
567576
await startSpan(
568577
// This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions.
569578
{ name: "debug-id-sourcemap-upload", scope: sentryScope, forceTransaction: true },
@@ -578,65 +587,77 @@ export function createSentryBuildPluginManager(
578587
const freeUploadDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts();
579588

580589
try {
581-
const assets = options.sourcemaps?.assets;
582-
583-
let globAssets: string | string[];
584-
if (assets) {
585-
globAssets = assets;
586-
} else {
587-
logger.debug(
588-
"No `sourcemaps.assets` option provided, falling back to uploading detected build artifacts."
589-
);
590-
globAssets = buildArtifactPaths;
591-
}
590+
if (!shouldPrepare) {
591+
// Direct CLI upload from existing artifact paths (no globbing, no preparation)
592+
let pathsToUpload: string[];
592593

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

604-
const debugIdChunkFilePaths = shouldPrepare
605-
? globResult.filter((debugIdChunkFilePath) => {
606-
return !!stripQueryAndHashFromPath(debugIdChunkFilePath).match(/\.(js|mjs|cjs)$/);
607-
})
608-
: globResult;
606+
const ignorePaths = options.sourcemaps?.ignore
607+
? Array.isArray(options.sourcemaps?.ignore)
608+
? options.sourcemaps?.ignore
609+
: [options.sourcemaps?.ignore]
610+
: [];
611+
await startSpan({ name: "upload", scope: sentryScope }, async () => {
612+
const cliInstance = createCliInstance(options);
613+
await cliInstance.releases.uploadSourceMaps(options.release.name ?? "undefined", {
614+
include: [
615+
{
616+
paths: pathsToUpload,
617+
rewrite: true,
618+
dist: options.release.dist,
619+
},
620+
],
621+
ignore: ignorePaths,
622+
live: "rejectOnError",
623+
});
624+
});
609625

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

614-
if (Array.isArray(assets) && assets.length === 0) {
615-
logger.debug(
616-
"Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID."
617-
);
618-
} else if (debugIdChunkFilePaths.length === 0) {
619-
logger.warn(
620-
"Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
639+
const globResult = await startSpan(
640+
{ name: "glob", scope: sentryScope },
641+
async () =>
642+
await glob(globAssets, {
643+
absolute: true,
644+
nodir: true, // We need individual files for preparation
645+
ignore: options.sourcemaps?.ignore,
646+
})
621647
);
622-
} else {
623-
if (!shouldPrepare) {
624-
// Direct CLI upload from existing artifact paths (no preparation or temp copies)
625-
await startSpan({ name: "upload", scope: sentryScope }, async () => {
626-
const cliInstance = createCliInstance(options);
627-
await cliInstance.releases.uploadSourceMaps(options.release.name ?? "undefined", {
628-
include: [
629-
{
630-
paths: debugIdChunkFilePaths,
631-
rewrite: false,
632-
dist: options.release.dist,
633-
},
634-
],
635-
live: "rejectOnError",
636-
});
637-
});
638648

639-
logger.info("Successfully uploaded source maps to Sentry");
649+
const debugIdChunkFilePaths = globResult.filter((debugIdChunkFilePath) => {
650+
return !!stripQueryAndHashFromPath(debugIdChunkFilePath).match(/\.(js|mjs|cjs)$/);
651+
});
652+
653+
// The order of the files output by glob() is not deterministic
654+
// Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent
655+
debugIdChunkFilePaths.sort();
656+
657+
if (debugIdChunkFilePaths.length === 0) {
658+
logger.warn(
659+
"Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
660+
);
640661
} else {
641662
const tmpUploadFolder = await startSpan(
642663
{ name: "mkdtemp", scope: sentryScope },

packages/bundler-plugin-core/test/build-plugin-manager.test.ts

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,6 @@ describe("createSentryBuildPluginManager", () => {
9898
it("uploads in-place when prepareArtifacts is false", async () => {
9999
mockCliUploadSourceMaps.mockResolvedValue(undefined);
100100

101-
// Return a mixture of files/dirs; in-place path should pass through as-is
102-
mockGlob.mockResolvedValue(["/app/dist/a.js", "/app/dist/dir", "/app/dist/a.js.map"]);
103-
104101
const manager = createSentryBuildPluginManager(
105102
{
106103
authToken: "t",
@@ -123,19 +120,93 @@ describe("createSentryBuildPluginManager", () => {
123120
include: expect.arrayContaining([
124121
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
125122
expect.objectContaining({
126-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
127-
paths: expect.arrayContaining([
128-
"/app/dist/a.js",
129-
"/app/dist/dir",
130-
"/app/dist/a.js.map",
131-
]),
132-
rewrite: false,
123+
// User-provided assets should be passed directly to CLI (no globbing)
124+
paths: ["/app/dist/**/*"],
125+
rewrite: true,
126+
dist: "1",
127+
}),
128+
]),
129+
live: "rejectOnError",
130+
})
131+
);
132+
// Should not glob when prepareArtifacts is false
133+
expect(mockGlob).not.toHaveBeenCalled();
134+
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
135+
});
136+
137+
it("uploads build artifact paths when prepareArtifacts is false and no assets provided", async () => {
138+
mockCliUploadSourceMaps.mockResolvedValue(undefined);
139+
140+
const manager = createSentryBuildPluginManager(
141+
{
142+
authToken: "t",
143+
org: "o",
144+
project: "p",
145+
release: { name: "some-release-name", dist: "1" },
146+
// No assets provided
147+
},
148+
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
149+
);
150+
151+
await manager.uploadSourcemaps([".next", "dist"], { prepareArtifacts: false });
152+
153+
expect(mockCliUploadSourceMaps).toHaveBeenCalledTimes(1);
154+
expect(mockCliUploadSourceMaps).toHaveBeenCalledWith(
155+
"some-release-name",
156+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
157+
expect.objectContaining({
158+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
159+
include: expect.arrayContaining([
160+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
161+
expect.objectContaining({
162+
// Should use buildArtifactPaths directly
163+
paths: [".next", "dist"],
164+
rewrite: true,
133165
dist: "1",
134166
}),
135167
]),
136168
live: "rejectOnError",
137169
})
138170
);
171+
expect(mockGlob).not.toHaveBeenCalled();
172+
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
173+
});
174+
175+
it("exits early when assets is an empty array", async () => {
176+
const manager = createSentryBuildPluginManager(
177+
{
178+
authToken: "t",
179+
org: "o",
180+
project: "p",
181+
release: { name: "some-release-name", dist: "1" },
182+
sourcemaps: { assets: [] },
183+
},
184+
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
185+
);
186+
187+
await manager.uploadSourcemaps([".next"], { prepareArtifacts: false });
188+
189+
expect(mockCliUploadSourceMaps).not.toHaveBeenCalled();
190+
expect(mockGlob).not.toHaveBeenCalled();
191+
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
192+
});
193+
194+
it("exits early when assets is an empty array even for default mode", async () => {
195+
const manager = createSentryBuildPluginManager(
196+
{
197+
authToken: "t",
198+
org: "o",
199+
project: "p",
200+
release: { name: "some-release-name", dist: "1" },
201+
sourcemaps: { assets: [] },
202+
},
203+
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
204+
);
205+
206+
await manager.uploadSourcemaps([".next"]);
207+
208+
expect(mockCliUploadSourceMaps).not.toHaveBeenCalled();
209+
expect(mockGlob).not.toHaveBeenCalled();
139210
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
140211
});
141212

0 commit comments

Comments
 (0)