Skip to content

Commit 75ca15e

Browse files
authored
fix: Wait for tasks depending on sourcemaps before deleting (#557)
1 parent 62de37e commit 75ca15e

File tree

4 files changed

+98
-45
lines changed

4 files changed

+98
-45
lines changed

Diff for: packages/bundler-plugin-core/src/debug-id-upload.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ interface DebugIdUploadPluginOptions {
2424
handleRecoverableError: (error: unknown) => void;
2525
sentryHub: Hub;
2626
sentryClient: NodeClient;
27-
deleteFilesUpForDeletion: () => Promise<void>;
2827
sentryCliOptions: {
2928
url: string;
3029
authToken: string;
@@ -34,6 +33,7 @@ interface DebugIdUploadPluginOptions {
3433
silent: boolean;
3534
headers?: Record<string, string>;
3635
};
36+
freeDependencyOnSourcemapFiles: () => void;
3737
}
3838

3939
export function createDebugIdUploadFunction({
@@ -47,7 +47,7 @@ export function createDebugIdUploadFunction({
4747
sentryClient,
4848
sentryCliOptions,
4949
rewriteSourcesHook,
50-
deleteFilesUpForDeletion,
50+
freeDependencyOnSourcemapFiles,
5151
}: DebugIdUploadPluginOptions) {
5252
return async (buildArtifactPaths: string[]) => {
5353
const artifactBundleUploadTransaction = sentryHub.startTransaction({
@@ -179,8 +179,6 @@ export function createDebugIdUploadFunction({
179179
uploadSpan.finish();
180180
logger.info("Successfully uploaded source maps to Sentry");
181181
}
182-
183-
await deleteFilesUpForDeletion();
184182
} catch (e) {
185183
sentryHub.withScope((scope) => {
186184
scope.setSpan(artifactBundleUploadTransaction);
@@ -196,6 +194,7 @@ export function createDebugIdUploadFunction({
196194
cleanupSpan.finish();
197195
}
198196
artifactBundleUploadTransaction.finish();
197+
freeDependencyOnSourcemapFiles();
199198
await safeFlushTelemetry(sentryClient);
200199
}
201200
};

Diff for: packages/bundler-plugin-core/src/index.ts

+54-34
Original file line numberDiff line numberDiff line change
@@ -185,32 +185,46 @@ export function sentryUnpluginFactory({
185185
})
186186
);
187187

188-
async function deleteFilesUpForDeletion() {
189-
const filesToDeleteAfterUpload =
190-
options.sourcemaps?.filesToDeleteAfterUpload ?? options.sourcemaps?.deleteFilesAfterUpload;
191-
192-
if (filesToDeleteAfterUpload) {
193-
const filePathsToDelete = await glob(filesToDeleteAfterUpload, {
194-
absolute: true,
195-
nodir: true,
196-
});
188+
// We have multiple plugins depending on generated source map files. (debug ID upload, legacy upload)
189+
// Additionally, we also want to have the functionality to delete files after uploading sourcemaps.
190+
// All of these plugins and the delete functionality need to run in the same hook (`writeBundle`).
191+
// Since the plugins among themselves are not aware of when they run and finish, we need a system to
192+
// track their dependencies on the generated files, so that we can initiate the file deletion only after
193+
// nothing depends on the files anymore.
194+
const dependenciesOnSourcemapFiles = new Set<symbol>();
195+
const sourcemapFileDependencySubscribers: (() => void)[] = [];
196+
197+
function notifySourcemapFileDependencySubscribers() {
198+
sourcemapFileDependencySubscribers.forEach((subscriber) => {
199+
subscriber();
200+
});
201+
}
202+
203+
function createDependencyOnSourcemapFiles() {
204+
const dependencyIdentifier = Symbol();
205+
dependenciesOnSourcemapFiles.add(dependencyIdentifier);
197206

198-
filePathsToDelete.forEach((filePathToDelete) => {
199-
logger.debug(`Deleting asset after upload: ${filePathToDelete}`);
207+
return function freeDependencyOnSourcemapFiles() {
208+
dependenciesOnSourcemapFiles.delete(dependencyIdentifier);
209+
notifySourcemapFileDependencySubscribers();
210+
};
211+
}
212+
213+
/**
214+
* Returns a Promise that resolves when all the currently active dependencies are freed again.
215+
*/
216+
function waitUntilSourcemapFileDependenciesAreFreed() {
217+
return new Promise<void>((resolve) => {
218+
sourcemapFileDependencySubscribers.push(() => {
219+
if (dependenciesOnSourcemapFiles.size === 0) {
220+
resolve();
221+
}
200222
});
201223

202-
await Promise.all(
203-
filePathsToDelete.map((filePathToDelete) =>
204-
fs.promises.rm(filePathToDelete, { force: true }).catch((e) => {
205-
// This is allowed to fail - we just don't do anything
206-
logger.debug(
207-
`An error occurred while attempting to delete asset: ${filePathToDelete}`,
208-
e
209-
);
210-
})
211-
)
212-
);
213-
}
224+
if (dependenciesOnSourcemapFiles.size === 0) {
225+
resolve();
226+
}
227+
});
214228
}
215229

216230
if (options.bundleSizeOptimizations) {
@@ -326,22 +340,13 @@ export function sentryUnpluginFactory({
326340
vcsRemote: options.release.vcsRemote,
327341
headers: options.headers,
328342
},
329-
deleteFilesUpForDeletion,
343+
freeDependencyOnSourcemapFiles: createDependencyOnSourcemapFiles(),
330344
})
331345
);
332346
}
333347

334348
plugins.push(debugIdInjectionPlugin(logger));
335349

336-
plugins.push(
337-
fileDeletionPlugin({
338-
deleteFilesUpForDeletion,
339-
handleRecoverableError,
340-
sentryHub,
341-
sentryClient,
342-
})
343-
);
344-
345350
if (!options.authToken) {
346351
logger.warn(
347352
"No auth token provided. Will not upload source maps. Please set the `authToken` option. You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/"
@@ -360,7 +365,7 @@ export function sentryUnpluginFactory({
360365
createDebugIdUploadFunction({
361366
assets: options.sourcemaps?.assets,
362367
ignore: options.sourcemaps?.ignore,
363-
deleteFilesUpForDeletion,
368+
freeDependencyOnSourcemapFiles: createDependencyOnSourcemapFiles(),
364369
dist: options.release.dist,
365370
releaseName: options.release.name,
366371
logger: logger,
@@ -396,6 +401,21 @@ export function sentryUnpluginFactory({
396401
}
397402
}
398403

404+
plugins.push(
405+
fileDeletionPlugin({
406+
// It is very important that this is only called after all other dependencies have been created with `createDependencyOnSourcemapFiles`.
407+
// Ideally, we always register this plugin last.
408+
dependenciesAreFreedPromise: waitUntilSourcemapFileDependenciesAreFreed(),
409+
filesToDeleteAfterUpload:
410+
options.sourcemaps?.filesToDeleteAfterUpload ??
411+
options.sourcemaps?.deleteFilesAfterUpload,
412+
logger,
413+
handleRecoverableError,
414+
sentryHub,
415+
sentryClient,
416+
})
417+
);
418+
399419
return plugins;
400420
});
401421
}

Diff for: packages/bundler-plugin-core/src/plugins/release-management.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface ReleaseManagementPluginOptions {
2727
silent: boolean;
2828
headers?: Record<string, string>;
2929
};
30-
deleteFilesUpForDeletion: () => Promise<void>;
30+
freeDependencyOnSourcemapFiles: () => void;
3131
}
3232

3333
export function releaseManagementPlugin({
@@ -42,7 +42,7 @@ export function releaseManagementPlugin({
4242
sentryHub,
4343
sentryClient,
4444
sentryCliOptions,
45-
deleteFilesUpForDeletion,
45+
freeDependencyOnSourcemapFiles,
4646
}: ReleaseManagementPluginOptions): UnpluginOptions {
4747
return {
4848
name: "sentry-debug-id-upload-plugin",
@@ -85,12 +85,12 @@ export function releaseManagementPlugin({
8585
if (deployOptions) {
8686
await cliInstance.releases.newDeploy(releaseName, deployOptions);
8787
}
88-
89-
await deleteFilesUpForDeletion();
9088
} catch (e) {
9189
sentryHub.captureException('Error in "releaseManagementPlugin" writeBundle hook');
9290
await safeFlushTelemetry(sentryClient);
9391
handleRecoverableError(e);
92+
} finally {
93+
freeDependencyOnSourcemapFiles();
9494
}
9595
},
9696
};

Diff for: packages/bundler-plugin-core/src/plugins/sourcemap-deletion.ts

+37-3
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,59 @@
11
import { Hub, NodeClient } from "@sentry/node";
2+
import { glob } from "glob";
23
import { UnpluginOptions } from "unplugin";
4+
import { Logger } from "../sentry/logger";
35
import { safeFlushTelemetry } from "../sentry/telemetry";
6+
import fs from "fs";
47

58
interface FileDeletionPlugin {
69
handleRecoverableError: (error: unknown) => void;
7-
deleteFilesUpForDeletion: () => Promise<void>;
10+
dependenciesAreFreedPromise: Promise<void>;
811
sentryHub: Hub;
912
sentryClient: NodeClient;
13+
filesToDeleteAfterUpload: string | string[] | undefined;
14+
logger: Logger;
1015
}
1116

1217
export function fileDeletionPlugin({
1318
handleRecoverableError,
1419
sentryHub,
1520
sentryClient,
16-
deleteFilesUpForDeletion,
21+
filesToDeleteAfterUpload,
22+
dependenciesAreFreedPromise,
23+
logger,
1724
}: FileDeletionPlugin): UnpluginOptions {
1825
return {
1926
name: "sentry-file-deletion-plugin",
2027
async writeBundle() {
2128
try {
22-
await deleteFilesUpForDeletion();
29+
if (filesToDeleteAfterUpload !== undefined) {
30+
const filePathsToDelete = await glob(filesToDeleteAfterUpload, {
31+
absolute: true,
32+
nodir: true,
33+
});
34+
35+
logger.debug(
36+
"Waiting for dependencies on generated files to be freed before deleting..."
37+
);
38+
39+
await dependenciesAreFreedPromise;
40+
41+
filePathsToDelete.forEach((filePathToDelete) => {
42+
logger.debug(`Deleting asset after upload: ${filePathToDelete}`);
43+
});
44+
45+
await Promise.all(
46+
filePathsToDelete.map((filePathToDelete) =>
47+
fs.promises.rm(filePathToDelete, { force: true }).catch((e) => {
48+
// This is allowed to fail - we just don't do anything
49+
logger.debug(
50+
`An error occurred while attempting to delete asset: ${filePathToDelete}`,
51+
e
52+
);
53+
})
54+
)
55+
);
56+
}
2357
} catch (e) {
2458
sentryHub.captureException('Error in "sentry-file-deletion-plugin" buildEnd hook');
2559
await safeFlushTelemetry(sentryClient);

0 commit comments

Comments
 (0)