Skip to content

Commit d5ada3c

Browse files
authored
feat(core): Expose bundler plugin primitives via createSentryBuildPluginManager (#713)
1 parent 95e5cca commit d5ada3c

File tree

14 files changed

+824
-915
lines changed

14 files changed

+824
-915
lines changed

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

+591
Large diffs are not rendered by default.

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

+6-189
Original file line numberDiff line numberDiff line change
@@ -1,207 +1,24 @@
11
import fs from "fs";
2-
import { glob } from "glob";
3-
import os from "os";
42
import path from "path";
53
import * as util from "util";
6-
import { Logger } from "./sentry/logger";
74
import { promisify } from "util";
8-
import SentryCli from "@sentry/cli";
9-
import { dynamicSamplingContextToSentryBaggageHeader } from "@sentry/utils";
10-
import { safeFlushTelemetry } from "./sentry/telemetry";
11-
import { stripQueryAndHashFromPath } from "./utils";
12-
import { setMeasurement, spanToTraceHeader, startSpan } from "@sentry/core";
13-
import { getDynamicSamplingContextFromSpan, Scope } from "@sentry/core";
14-
import { Client } from "@sentry/types";
15-
import { HandleRecoverableErrorFn } from "./types";
5+
import { SentryBuildPluginManager } from "./api-primitives";
6+
import { Logger } from "./sentry/logger";
167

178
interface RewriteSourcesHook {
189
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1910
(source: string, map: any): string;
2011
}
2112

2213
interface DebugIdUploadPluginOptions {
23-
logger: Logger;
24-
assets?: string | string[];
25-
ignore?: string | string[];
26-
releaseName?: string;
27-
dist?: string;
28-
rewriteSourcesHook?: RewriteSourcesHook;
29-
handleRecoverableError: HandleRecoverableErrorFn;
30-
sentryScope: Scope;
31-
sentryClient: Client;
32-
sentryCliOptions: {
33-
url: string;
34-
authToken: string;
35-
org?: string;
36-
project: string;
37-
vcsRemote: string;
38-
silent: boolean;
39-
headers?: Record<string, string>;
40-
};
41-
createDependencyOnSourcemapFiles: () => () => void;
14+
sentryBuildPluginManager: SentryBuildPluginManager;
4215
}
4316

4417
export function createDebugIdUploadFunction({
45-
assets,
46-
ignore,
47-
logger,
48-
releaseName,
49-
dist,
50-
handleRecoverableError,
51-
sentryScope,
52-
sentryClient,
53-
sentryCliOptions,
54-
rewriteSourcesHook,
55-
createDependencyOnSourcemapFiles,
18+
sentryBuildPluginManager,
5619
}: DebugIdUploadPluginOptions) {
57-
const freeGlobalDependencyOnSourcemapFiles = createDependencyOnSourcemapFiles();
58-
5920
return async (buildArtifactPaths: string[]) => {
60-
await startSpan(
61-
// This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions.
62-
{ name: "debug-id-sourcemap-upload", scope: sentryScope, forceTransaction: true },
63-
async () => {
64-
let folderToCleanUp: string | undefined;
65-
66-
// It is possible that this writeBundle hook (which calls this function) is called multiple times in one build (for example when reusing the plugin, or when using build tooling like `@vitejs/plugin-legacy`)
67-
// Therefore we need to actually register the execution of this hook as dependency on the sourcemap files.
68-
const freeUploadDependencyOnSourcemapFiles = createDependencyOnSourcemapFiles();
69-
70-
try {
71-
const tmpUploadFolder = await startSpan(
72-
{ name: "mkdtemp", scope: sentryScope },
73-
async () => {
74-
return await fs.promises.mkdtemp(
75-
path.join(os.tmpdir(), "sentry-bundler-plugin-upload-")
76-
);
77-
}
78-
);
79-
80-
folderToCleanUp = tmpUploadFolder;
81-
82-
let globAssets: string | string[];
83-
if (assets) {
84-
globAssets = assets;
85-
} else {
86-
logger.debug(
87-
"No `sourcemaps.assets` option provided, falling back to uploading detected build artifacts."
88-
);
89-
globAssets = buildArtifactPaths;
90-
}
91-
92-
const globResult = await startSpan(
93-
{ name: "glob", scope: sentryScope },
94-
async () => await glob(globAssets, { absolute: true, nodir: true, ignore: ignore })
95-
);
96-
97-
const debugIdChunkFilePaths = globResult.filter((debugIdChunkFilePath) => {
98-
return !!stripQueryAndHashFromPath(debugIdChunkFilePath).match(/\.(js|mjs|cjs)$/);
99-
});
100-
101-
// The order of the files output by glob() is not deterministic
102-
// Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent
103-
debugIdChunkFilePaths.sort();
104-
105-
if (Array.isArray(assets) && assets.length === 0) {
106-
logger.debug(
107-
"Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID."
108-
);
109-
} else if (debugIdChunkFilePaths.length === 0) {
110-
logger.warn(
111-
"Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
112-
);
113-
} else {
114-
await startSpan(
115-
{ name: "prepare-bundles", scope: sentryScope },
116-
async (prepBundlesSpan) => {
117-
// Preparing the bundles can be a lot of work and doing it all at once has the potential of nuking the heap so
118-
// instead we do it with a maximum of 16 concurrent workers
119-
const preparationTasks = debugIdChunkFilePaths.map(
120-
(chunkFilePath, chunkIndex) => async () => {
121-
await prepareBundleForDebugIdUpload(
122-
chunkFilePath,
123-
tmpUploadFolder,
124-
chunkIndex,
125-
logger,
126-
rewriteSourcesHook ?? defaultRewriteSourcesHook
127-
);
128-
}
129-
);
130-
const workers: Promise<void>[] = [];
131-
const worker = async () => {
132-
while (preparationTasks.length > 0) {
133-
const task = preparationTasks.shift();
134-
if (task) {
135-
await task();
136-
}
137-
}
138-
};
139-
for (let workerIndex = 0; workerIndex < 16; workerIndex++) {
140-
workers.push(worker());
141-
}
142-
143-
await Promise.all(workers);
144-
145-
const files = await fs.promises.readdir(tmpUploadFolder);
146-
const stats = files.map((file) =>
147-
fs.promises.stat(path.join(tmpUploadFolder, file))
148-
);
149-
const uploadSize = (await Promise.all(stats)).reduce(
150-
(accumulator, { size }) => accumulator + size,
151-
0
152-
);
153-
154-
setMeasurement("files", files.length, "none", prepBundlesSpan);
155-
setMeasurement("upload_size", uploadSize, "byte", prepBundlesSpan);
156-
157-
await startSpan({ name: "upload", scope: sentryScope }, async (uploadSpan) => {
158-
const cliInstance = new SentryCli(null, {
159-
...sentryCliOptions,
160-
headers: {
161-
"sentry-trace": spanToTraceHeader(uploadSpan),
162-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
163-
baggage: dynamicSamplingContextToSentryBaggageHeader(
164-
getDynamicSamplingContextFromSpan(uploadSpan)
165-
)!,
166-
...sentryCliOptions.headers,
167-
},
168-
});
169-
170-
await cliInstance.releases.uploadSourceMaps(
171-
releaseName ?? "undefined", // unfortunately this needs a value for now but it will not matter since debug IDs overpower releases anyhow
172-
{
173-
include: [
174-
{
175-
paths: [tmpUploadFolder],
176-
rewrite: false,
177-
dist: dist,
178-
},
179-
],
180-
}
181-
);
182-
});
183-
}
184-
);
185-
186-
logger.info("Successfully uploaded source maps to Sentry");
187-
}
188-
} catch (e) {
189-
sentryScope.captureException('Error in "debugIdUploadPlugin" writeBundle hook');
190-
handleRecoverableError(e, false);
191-
} finally {
192-
if (folderToCleanUp) {
193-
void startSpan({ name: "cleanup", scope: sentryScope }, async () => {
194-
if (folderToCleanUp) {
195-
await fs.promises.rm(folderToCleanUp, { recursive: true, force: true });
196-
}
197-
});
198-
}
199-
freeGlobalDependencyOnSourcemapFiles();
200-
freeUploadDependencyOnSourcemapFiles();
201-
await safeFlushTelemetry(sentryClient);
202-
}
203-
}
204-
);
21+
await sentryBuildPluginManager.uploadSourcemaps(buildArtifactPaths);
20522
};
20623
}
20724

@@ -388,7 +205,7 @@ async function prepareSourceMapForDebugIdUpload(
388205
}
389206

390207
const PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//;
391-
function defaultRewriteSourcesHook(source: string): string {
208+
export function defaultRewriteSourcesHook(source: string): string {
392209
if (source.match(PROTOCOL_REGEX)) {
393210
return source.replace(PROTOCOL_REGEX, "");
394211
} else {

0 commit comments

Comments
 (0)