|
1 | 1 | import fs from "fs";
|
2 |
| -import { glob } from "glob"; |
3 |
| -import os from "os"; |
4 | 2 | import path from "path";
|
5 | 3 | import * as util from "util";
|
6 |
| -import { Logger } from "./sentry/logger"; |
7 | 4 | 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"; |
16 | 7 |
|
17 | 8 | interface RewriteSourcesHook {
|
18 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
19 | 10 | (source: string, map: any): string;
|
20 | 11 | }
|
21 | 12 |
|
22 | 13 | 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; |
42 | 15 | }
|
43 | 16 |
|
44 | 17 | 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, |
56 | 19 | }: DebugIdUploadPluginOptions) {
|
57 |
| - const freeGlobalDependencyOnSourcemapFiles = createDependencyOnSourcemapFiles(); |
58 |
| - |
59 | 20 | 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); |
205 | 22 | };
|
206 | 23 | }
|
207 | 24 |
|
@@ -388,7 +205,7 @@ async function prepareSourceMapForDebugIdUpload(
|
388 | 205 | }
|
389 | 206 |
|
390 | 207 | const PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//;
|
391 |
| -function defaultRewriteSourcesHook(source: string): string { |
| 208 | +export function defaultRewriteSourcesHook(source: string): string { |
392 | 209 | if (source.match(PROTOCOL_REGEX)) {
|
393 | 210 | return source.replace(PROTOCOL_REGEX, "");
|
394 | 211 | } else {
|
|
0 commit comments