|
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