@@ -5,13 +5,16 @@ import path from "path";
5
5
import * as util from "util" ;
6
6
import { Logger } from "./sentry/logger" ;
7
7
import { promisify } from "util" ;
8
- import { Hub , NodeClient } from "@sentry/node" ;
9
8
import SentryCli from "@sentry/cli" ;
10
9
import { dynamicSamplingContextToSentryBaggageHeader } from "@sentry/utils" ;
11
10
import { safeFlushTelemetry } from "./sentry/telemetry" ;
12
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" ;
13
15
14
16
interface RewriteSourcesHook {
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
18
( source : string , map : any ) : string ;
16
19
}
17
20
@@ -23,8 +26,8 @@ interface DebugIdUploadPluginOptions {
23
26
dist ?: string ;
24
27
rewriteSourcesHook ?: RewriteSourcesHook ;
25
28
handleRecoverableError : ( error : unknown ) => void ;
26
- sentryHub : Hub ;
27
- sentryClient : NodeClient ;
29
+ sentryScope : Scope ;
30
+ sentryClient : Client ;
28
31
sentryCliOptions : {
29
32
url : string ;
30
33
authToken : string ;
@@ -44,7 +47,7 @@ export function createDebugIdUploadFunction({
44
47
releaseName,
45
48
dist,
46
49
handleRecoverableError,
47
- sentryHub ,
50
+ sentryScope ,
48
51
sentryClient,
49
52
sentryCliOptions,
50
53
rewriteSourcesHook,
@@ -53,155 +56,152 @@ export function createDebugIdUploadFunction({
53
56
const freeGlobalDependencyOnSourcemapFiles = createDependencyOnSourcemapFiles ( ) ;
54
57
55
58
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
+ ) ;
59
78
60
- let folderToCleanUp : string | undefined ;
79
+ folderToCleanUp = tmpUploadFolder ;
61
80
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
+ }
65
90
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 ( ) ;
84
103
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."
124
107
) ;
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" ) ;
125
187
}
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
+ } ) ;
134
198
}
135
- } ;
136
- for ( let workerIndex = 0 ; workerIndex < 16 ; workerIndex ++ ) {
137
- workers . push ( worker ( ) ) ;
199
+ freeGlobalDependencyOnSourcemapFiles ( ) ;
200
+ freeUploadDependencyOnSourcemapFiles ( ) ;
201
+ await safeFlushTelemetry ( sentryClient ) ;
138
202
}
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 ( ) ;
199
203
}
200
- artifactBundleUploadTransaction . finish ( ) ;
201
- freeGlobalDependencyOnSourcemapFiles ( ) ;
202
- freeUploadDependencyOnSourcemapFiles ( ) ;
203
- await safeFlushTelemetry ( sentryClient ) ;
204
- }
204
+ ) ;
205
205
} ;
206
206
}
207
207
0 commit comments