Skip to content
Draft
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
23 changes: 18 additions & 5 deletions packages/bundler-plugins/src/core/build-plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ export function createSentryBuildPluginManager(
})),
);

await cliInstance.uploadSourcemaps(releaseName, uploadTargets);
warnAboutDirectoriesWithoutSourcemaps(logger, await cliInstance.uploadSourcemaps(releaseName, uploadTargets));
}

if (options.release.setCommits !== false) {
Expand Down Expand Up @@ -686,9 +686,9 @@ export function createSentryBuildPluginManager(
return;
}

await startSpan({ name: 'upload', scope: sentryScope }, async () => {
const emptyDirectories = await startSpan({ name: 'upload', scope: sentryScope }, async () => {
const cliInstance = new SentryCliAdapter(options);
await cliInstance.uploadSourcemaps(
return cliInstance.uploadSourcemaps(
options.release.name ?? 'undefined',
pathsToUpload.map(directory => ({
directory,
Expand All @@ -698,8 +698,11 @@ export function createSentryBuildPluginManager(
);
});

uploaded = true;
logger.info('Successfully uploaded source maps to Sentry');
warnAboutDirectoriesWithoutSourcemaps(logger, emptyDirectories);
if (emptyDirectories.length < pathsToUpload.length) {
uploaded = true;
logger.info('Successfully uploaded source maps to Sentry');
}
} else {
// Prepare artifacts in temp folder before uploading
let globAssets: string | string[];
Expand Down Expand Up @@ -1029,3 +1032,13 @@ function canUploadSourceMaps(options: NormalizedOptions, logger: Logger, isDevMo
return true;
}
}

function warnAboutDirectoriesWithoutSourcemaps(logger: Logger, directories: string[]): void {
for (const directory of directories) {
logger.warn(
`No source maps found in "${directory}", so nothing was uploaded from it. ` +
'This usually means source map generation is not enabled for this build output. ' +
'Enable it so Sentry can un-minify stack traces from these files.',
);
}
}
18 changes: 15 additions & 3 deletions packages/bundler-plugins/src/core/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,24 +120,36 @@ export class SentryCliAdapter {
* array; the new `sourcemap upload` command takes a single directory, so we upload each target
* separately. A client is created per project because project selection is bound at client
* creation time.
*
* Empty directories must not abort the loop: `@sentry/nextjs` uploads `.next/server` first, and
* a server build without source maps would otherwise skip every client directory after it.
*
* @returns The directories that had no source maps to upload.
*/
public async uploadSourcemaps(name: string, targets: UploadTarget[]): Promise<void> {
public async uploadSourcemaps(name: string, targets: UploadTarget[]): Promise<string[]> {
const projects = getProjects(this.#options.project) ?? [undefined];
const emptyDirectories = new Set<string>();

for (const project of projects) {
const sdk = this.#createClient(project);
for (const target of targets) {
await sdk.sourcemap.upload({
const result = (await sdk.sourcemap.upload({
directory: target.directory,
release: name,
dist: target.dist ?? this.#options.release.dist,
ext: target.ext?.join(','),
ignore: serializeIgnore(target.ignore),
ignoreFile: target.ignoreFile,
urlPrefix: target.urlPrefix,
});
allowEmpty: true,
})) as { filesUploaded?: number } | undefined;
if (result?.filesUploaded === 0) {
emptyDirectories.add(target.directory);
}
}
}

return [...emptyDirectories];
}

/** Inject debug IDs into the given build artifacts. */
Expand Down
45 changes: 45 additions & 0 deletions packages/bundler-plugins/test/core/build-plugin-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,50 @@ describe('createSentryBuildPluginManager', () => {
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

it('keeps uploading the other directories when one has no source maps', async () => {
mockSourcemapUpload.mockResolvedValueOnce({ filesUploaded: 0 }).mockResolvedValueOnce({ filesUploaded: 4 });

const manager = createSentryBuildPluginManager(
{ authToken: 't', org: 'o', project: 'p', release: { name: 'some-release-name' } },
{ buildTool: 'webpack', loggerPrefix: '[sentry-webpack-plugin]' },
);
const warnSpy = vi.spyOn(manager.logger, 'warn');
const infoSpy = vi.spyOn(manager.logger, 'info');

const result = await manager.uploadSourcemaps(['.next/server', '.next/static/chunks'], {
prepareArtifacts: false,
});

expect(mockSourcemapUpload).toHaveBeenCalledTimes(2);
expect(mockSourcemapUpload).toHaveBeenCalledWith(
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
expect.objectContaining({ directory: '.next/server', allowEmpty: true }),
);
expect(warnSpy).toHaveBeenCalledOnce();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No source maps found in ".next/server"'));
expect(infoSpy).toHaveBeenCalledWith('Successfully uploaded source maps to Sentry');
expect(result).toEqual({ uploaded: true });

vi.restoreAllMocks();
});

it('does not report success when no directory has source maps', async () => {
mockSourcemapUpload.mockResolvedValue({ filesUploaded: 0 });

const manager = createSentryBuildPluginManager(
{ authToken: 't', org: 'o', project: 'p', release: { name: 'some-release-name' } },
{ buildTool: 'webpack', loggerPrefix: '[sentry-webpack-plugin]' },
);
const infoSpy = vi.spyOn(manager.logger, 'info');

const result = await manager.uploadSourcemaps(['.next/server'], { prepareArtifacts: false });

expect(infoSpy).not.toHaveBeenCalledWith('Successfully uploaded source maps to Sentry');
expect(result).toEqual({ uploaded: false });

vi.restoreAllMocks();
});

it('exits early when assets is an empty array', async () => {
const manager = createSentryBuildPluginManager(
{
Expand Down Expand Up @@ -419,6 +463,7 @@ describe('createSentryBuildPluginManager', () => {
ignore: undefined,
ignoreFile: undefined,
urlPrefix: undefined,
allowEmpty: true,
});
});

Expand Down
15 changes: 15 additions & 0 deletions packages/bundler-plugins/test/core/cli-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,21 @@ describe('SentryCliAdapter against the real CLI', () => {
expect(requestsSince(before)).toContain('POST /api/0/organizations/test-org/artifactbundle/assemble/');
});

it('skips a directory without source maps and still uploads the next one', async () => {
const emptyDir = join(workDir, 'server');
mkdirSync(emptyDir);
writeFileSync(join(emptyDir, 'page.js'), 'console.log("no map");\n');
const before = recordedRequests();

const emptyDirectories = await createAdapter().uploadSourcemaps('1.0.0', [
{ directory: emptyDir },
{ directory: buildDir },
]);

expect(emptyDirectories).toEqual([emptyDir]);
expect(requestsSince(before)).toContain('POST /api/0/organizations/test-org/artifactbundle/assemble/');
});

it('resolves the server url, or gives up quietly', async () => {
const adapter = createAdapter();

Expand Down
Loading