@@ -5,13 +5,16 @@ import path from "path";
55import * as util from "util" ;
66import { Logger } from "./sentry/logger" ;
77import { promisify } from "util" ;
8- import { Hub , NodeClient } from "@sentry/node" ;
98import SentryCli from "@sentry/cli" ;
109import { dynamicSamplingContextToSentryBaggageHeader } from "@sentry/utils" ;
1110import { safeFlushTelemetry } from "./sentry/telemetry" ;
1211import { stripQueryAndHashFromPath } from "./utils" ;
12+ import { setMeasurement , spanToTraceHeader , startSpan } from "@sentry/core" ;
13+ import { getDynamicSamplingContextFromSpan , Scope } from "@sentry/core" ;
14+ import { Client } from "@sentry/types" ;
1315
1416interface RewriteSourcesHook {
17+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1518 ( source : string , map : any ) : string ;
1619}
1720
@@ -23,8 +26,8 @@ interface DebugIdUploadPluginOptions {
2326 dist ?: string ;
2427 rewriteSourcesHook ?: RewriteSourcesHook ;
2528 handleRecoverableError : ( error : unknown ) => void ;
26- sentryHub : Hub ;
27- sentryClient : NodeClient ;
29+ sentryScope : Scope ;
30+ sentryClient : Client ;
2831 sentryCliOptions : {
2932 url : string ;
3033 authToken : string ;
@@ -44,7 +47,7 @@ export function createDebugIdUploadFunction({
4447 releaseName,
4548 dist,
4649 handleRecoverableError,
47- sentryHub ,
50+ sentryScope ,
4851 sentryClient,
4952 sentryCliOptions,
5053 rewriteSourcesHook,
@@ -53,155 +56,152 @@ export function createDebugIdUploadFunction({
5356 const freeGlobalDependencyOnSourcemapFiles = createDependencyOnSourcemapFiles ( ) ;
5457
5558 return async ( buildArtifactPaths : string [ ] ) => {
56- const artifactBundleUploadTransaction = sentryHub . startTransaction ( {
57- name : "debug-id-sourcemap-upload" ,
58- } ) ;
59+ await startSpan (
60+ // This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions.
61+ { name : "debug-id-sourcemap-upload" , scope : sentryScope , forceTransaction : true } ,
62+ async ( ) => {
63+ let folderToCleanUp : string | undefined ;
64+
65+ // 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`)
66+ // Therefore we need to actually register the execution of this hook as dependency on the sourcemap files.
67+ const freeUploadDependencyOnSourcemapFiles = createDependencyOnSourcemapFiles ( ) ;
68+
69+ try {
70+ const tmpUploadFolder = await startSpan (
71+ { name : "mkdtemp" , scope : sentryScope } ,
72+ async ( ) => {
73+ return await fs . promises . mkdtemp (
74+ path . join ( os . tmpdir ( ) , "sentry-bundler-plugin-upload-" )
75+ ) ;
76+ }
77+ ) ;
5978
60- let folderToCleanUp : string | undefined ;
79+ folderToCleanUp = tmpUploadFolder ;
6180
62- // 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`)
63- // Therefore we need to actually register the execution of this hook as dependency on the sourcemap files.
64- const freeUploadDependencyOnSourcemapFiles = createDependencyOnSourcemapFiles ( ) ;
81+ let globAssets : string | string [ ] ;
82+ if ( assets ) {
83+ globAssets = assets ;
84+ } else {
85+ logger . debug (
86+ "No `sourcemaps.assets` option provided, falling back to uploading detected build artifacts."
87+ ) ;
88+ globAssets = buildArtifactPaths ;
89+ }
6590
66- try {
67- const mkdtempSpan = artifactBundleUploadTransaction . startChild ( { description : "mkdtemp" } ) ;
68- const tmpUploadFolder = await fs . promises . mkdtemp (
69- path . join ( os . tmpdir ( ) , "sentry-bundler-plugin-upload-" )
70- ) ;
71- mkdtempSpan . finish ( ) ;
72-
73- folderToCleanUp = tmpUploadFolder ;
74-
75- let globAssets ;
76- if ( assets ) {
77- globAssets = assets ;
78- } else {
79- logger . debug (
80- "No `sourcemaps.assets` option provided, falling back to uploading detected build artifacts."
81- ) ;
82- globAssets = buildArtifactPaths ;
83- }
91+ const globResult = await startSpan (
92+ { name : "glob" , scope : sentryScope } ,
93+ async ( ) => await glob ( globAssets , { absolute : true , nodir : true , ignore : ignore } )
94+ ) ;
95+
96+ const debugIdChunkFilePaths = globResult . filter ( ( debugIdChunkFilePath ) => {
97+ return ! ! stripQueryAndHashFromPath ( debugIdChunkFilePath ) . match ( / \. ( j s | m j s | c j s ) $ / ) ;
98+ } ) ;
99+
100+ // The order of the files output by glob() is not deterministic
101+ // Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent
102+ debugIdChunkFilePaths . sort ( ) ;
84103
85- const globSpan = artifactBundleUploadTransaction . startChild ( { description : "glob" } ) ;
86- const globResult = await glob ( globAssets , {
87- absolute : true ,
88- nodir : true ,
89- ignore : ignore ,
90- } ) ;
91- globSpan . finish ( ) ;
92-
93- const debugIdChunkFilePaths = globResult . filter ( ( debugIdChunkFilePath ) => {
94- return ! ! stripQueryAndHashFromPath ( debugIdChunkFilePath ) . match ( / \. ( j s | m j s | c j s ) $ / ) ;
95- } ) ;
96-
97- // The order of the files output by glob() is not deterministic
98- // Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent
99- debugIdChunkFilePaths . sort ( ) ;
100-
101- if ( Array . isArray ( assets ) && assets . length === 0 ) {
102- logger . debug (
103- "Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID."
104- ) ;
105- } else if ( debugIdChunkFilePaths . length === 0 ) {
106- logger . warn (
107- "Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
108- ) ;
109- } else {
110- const prepareSpan = artifactBundleUploadTransaction . startChild ( {
111- description : "prepare-bundles" ,
112- } ) ;
113-
114- // Preparing the bundles can be a lot of work and doing it all at once has the potential of nuking the heap so
115- // instead we do it with a maximum of 16 concurrent workers
116- const preparationTasks = debugIdChunkFilePaths . map (
117- ( chunkFilePath , chunkIndex ) => async ( ) => {
118- await prepareBundleForDebugIdUpload (
119- chunkFilePath ,
120- tmpUploadFolder ,
121- chunkIndex ,
122- logger ,
123- rewriteSourcesHook ?? defaultRewriteSourcesHook
104+ if ( Array . isArray ( assets ) && assets . length === 0 ) {
105+ logger . debug (
106+ "Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID."
124107 ) ;
108+ } else if ( debugIdChunkFilePaths . length === 0 ) {
109+ logger . warn (
110+ "Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
111+ ) ;
112+ } else {
113+ await startSpan (
114+ { name : "prepare-bundles" , scope : sentryScope } ,
115+ async ( prepBundlesSpan ) => {
116+ // Preparing the bundles can be a lot of work and doing it all at once has the potential of nuking the heap so
117+ // instead we do it with a maximum of 16 concurrent workers
118+ const preparationTasks = debugIdChunkFilePaths . map (
119+ ( chunkFilePath , chunkIndex ) => async ( ) => {
120+ await prepareBundleForDebugIdUpload (
121+ chunkFilePath ,
122+ tmpUploadFolder ,
123+ chunkIndex ,
124+ logger ,
125+ rewriteSourcesHook ?? defaultRewriteSourcesHook
126+ ) ;
127+ }
128+ ) ;
129+ const workers : Promise < void > [ ] = [ ] ;
130+ const worker = async ( ) => {
131+ while ( preparationTasks . length > 0 ) {
132+ const task = preparationTasks . shift ( ) ;
133+ if ( task ) {
134+ await task ( ) ;
135+ }
136+ }
137+ } ;
138+ for ( let workerIndex = 0 ; workerIndex < 16 ; workerIndex ++ ) {
139+ workers . push ( worker ( ) ) ;
140+ }
141+
142+ await Promise . all ( workers ) ;
143+
144+ const files = await fs . promises . readdir ( tmpUploadFolder ) ;
145+ const stats = files . map ( ( file ) =>
146+ fs . promises . stat ( path . join ( tmpUploadFolder , file ) )
147+ ) ;
148+ const uploadSize = ( await Promise . all ( stats ) ) . reduce (
149+ ( accumulator , { size } ) => accumulator + size ,
150+ 0
151+ ) ;
152+
153+ setMeasurement ( "files" , files . length , "none" , prepBundlesSpan ) ;
154+ setMeasurement ( "upload_size" , uploadSize , "byte" , prepBundlesSpan ) ;
155+
156+ await startSpan ( { name : "upload" , scope : sentryScope } , async ( uploadSpan ) => {
157+ const cliInstance = new SentryCli ( null , {
158+ ...sentryCliOptions ,
159+ headers : {
160+ "sentry-trace" : spanToTraceHeader ( uploadSpan ) ,
161+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
162+ baggage : dynamicSamplingContextToSentryBaggageHeader (
163+ getDynamicSamplingContextFromSpan ( uploadSpan )
164+ ) ! ,
165+ ...sentryCliOptions . headers ,
166+ } ,
167+ } ) ;
168+
169+ await cliInstance . releases . uploadSourceMaps (
170+ releaseName ?? "undefined" , // unfortunetly this needs a value for now but it will not matter since debug IDs overpower releases anyhow
171+ {
172+ include : [
173+ {
174+ paths : [ tmpUploadFolder ] ,
175+ rewrite : false ,
176+ dist : dist ,
177+ } ,
178+ ] ,
179+ useArtifactBundle : true ,
180+ }
181+ ) ;
182+ } ) ;
183+ }
184+ ) ;
185+
186+ logger . info ( "Successfully uploaded source maps to Sentry" ) ;
125187 }
126- ) ;
127- const workers : Promise < void > [ ] = [ ] ;
128- const worker = async ( ) => {
129- while ( preparationTasks . length > 0 ) {
130- const task = preparationTasks . shift ( ) ;
131- if ( task ) {
132- await task ( ) ;
133- }
188+ } catch ( e ) {
189+ sentryScope . captureException ( 'Error in "debugIdUploadPlugin" writeBundle hook' ) ;
190+ handleRecoverableError ( e ) ;
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+ } ) ;
134198 }
135- } ;
136- for ( let workerIndex = 0 ; workerIndex < 16 ; workerIndex ++ ) {
137- workers . push ( worker ( ) ) ;
199+ freeGlobalDependencyOnSourcemapFiles ( ) ;
200+ freeUploadDependencyOnSourcemapFiles ( ) ;
201+ await safeFlushTelemetry ( sentryClient ) ;
138202 }
139- await Promise . all ( workers ) ;
140-
141- prepareSpan . finish ( ) ;
142-
143- const files = await fs . promises . readdir ( tmpUploadFolder ) ;
144- const stats = files . map ( ( file ) => fs . promises . stat ( path . join ( tmpUploadFolder , file ) ) ) ;
145- const uploadSize = ( await Promise . all ( stats ) ) . reduce (
146- ( accumulator , { size } ) => accumulator + size ,
147- 0
148- ) ;
149-
150- artifactBundleUploadTransaction . setMeasurement ( "files" , files . length , "none" ) ;
151- artifactBundleUploadTransaction . setMeasurement ( "upload_size" , uploadSize , "byte" ) ;
152-
153- const uploadSpan = artifactBundleUploadTransaction . startChild ( {
154- description : "upload" ,
155- } ) ;
156-
157- const cliInstance = new SentryCli ( null , {
158- ...sentryCliOptions ,
159- headers : {
160- "sentry-trace" : uploadSpan . toTraceparent ( ) ,
161- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
162- baggage : dynamicSamplingContextToSentryBaggageHeader (
163- artifactBundleUploadTransaction . getDynamicSamplingContext ( )
164- ) ! ,
165- ...sentryCliOptions . headers ,
166- } ,
167- } ) ;
168-
169- await cliInstance . releases . uploadSourceMaps (
170- releaseName ?? "undefined" , // unfortunetly this needs a value for now but it will not matter since debug IDs overpower releases anyhow
171- {
172- include : [
173- {
174- paths : [ tmpUploadFolder ] ,
175- rewrite : false ,
176- dist : dist ,
177- } ,
178- ] ,
179- useArtifactBundle : true ,
180- }
181- ) ;
182-
183- uploadSpan . finish ( ) ;
184- logger . info ( "Successfully uploaded source maps to Sentry" ) ;
185- }
186- } catch ( e ) {
187- sentryHub . withScope ( ( scope ) => {
188- scope . setSpan ( artifactBundleUploadTransaction ) ;
189- sentryHub . captureException ( 'Error in "debugIdUploadPlugin" writeBundle hook' ) ;
190- } ) ;
191- handleRecoverableError ( e ) ;
192- } finally {
193- if ( folderToCleanUp ) {
194- const cleanupSpan = artifactBundleUploadTransaction . startChild ( {
195- description : "cleanup" ,
196- } ) ;
197- void fs . promises . rm ( folderToCleanUp , { recursive : true , force : true } ) ;
198- cleanupSpan . finish ( ) ;
199203 }
200- artifactBundleUploadTransaction . finish ( ) ;
201- freeGlobalDependencyOnSourcemapFiles ( ) ;
202- freeUploadDependencyOnSourcemapFiles ( ) ;
203- await safeFlushTelemetry ( sentryClient ) ;
204- }
204+ ) ;
205205 } ;
206206}
207207
0 commit comments