1
- import { getCurrentHub , getMainCarrier } from '@sentry/core' ;
2
- import type { CustomSamplingContext , Hub , Transaction , TransactionContext } from '@sentry/types' ;
1
+ /* eslint-disable complexity */
2
+ import { getCurrentHub } from '@sentry/core' ;
3
+ import type { Transaction } from '@sentry/types' ;
3
4
import { logger , uuid4 } from '@sentry/utils' ;
4
5
5
6
import { WINDOW } from '../helpers' ;
6
- import type {
7
- JSSelfProfile ,
8
- JSSelfProfiler ,
9
- JSSelfProfilerConstructor ,
10
- ProcessedJSSelfProfile ,
11
- } from './jsSelfProfiling' ;
12
- import { sendProfile } from './sendProfile' ;
7
+ import type { JSSelfProfile , JSSelfProfiler , JSSelfProfilerConstructor } from './jsSelfProfiling' ;
8
+ import { addProfileToMap , isValidSampleRate } from './utils' ;
13
9
14
- // Max profile duration.
15
- const MAX_PROFILE_DURATION_MS = 30_000 ;
10
+ export const MAX_PROFILE_DURATION_MS = 30_000 ;
16
11
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
17
12
// once, it will always fail and this allows us to early return.
18
13
let PROFILING_CONSTRUCTOR_FAILED = false ;
19
14
20
- // While we experiment, per transaction sampling interval will be more flexible to work with.
21
- type StartTransaction = (
22
- this : Hub ,
23
- transactionContext : TransactionContext ,
24
- customSamplingContext ?: CustomSamplingContext ,
25
- ) => Transaction | undefined ;
26
-
27
15
/**
28
16
* Check if profiler constructor is available.
29
17
* @param maybeProfiler
@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
55
43
* startProfiling is called after the call to startTransaction in order to avoid our own code from
56
44
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
57
45
*/
58
- function wrapTransactionWithProfiling ( transaction : Transaction ) : Transaction {
46
+ export function wrapTransactionWithProfiling ( transaction : Transaction ) : Transaction {
59
47
// Feature support check first
60
48
const JSProfilerConstructor = WINDOW . Profiler ;
61
49
@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
68
56
return transaction ;
69
57
}
70
58
71
- // profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
72
- if ( ! transaction . sampled ) {
73
- if ( __DEBUG_BUILD__ ) {
74
- logger . log ( '[Profiling] Transaction is not sampled, skipping profiling' ) ;
75
- }
76
- return transaction ;
77
- }
78
-
79
59
// If constructor failed once, it will always fail, so we can early return.
80
60
if ( PROFILING_CONSTRUCTOR_FAILED ) {
81
61
if ( __DEBUG_BUILD__ ) {
@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
86
66
87
67
const client = getCurrentHub ( ) . getClient ( ) ;
88
68
const options = client && client . getOptions ( ) ;
69
+ if ( ! options ) {
70
+ __DEBUG_BUILD__ && logger . log ( '[Profiling] Profiling disabled, no options found.' ) ;
71
+ return transaction ;
72
+ }
89
73
90
- // @ts -ignore not part of the browser options yet
91
- const profilesSampleRate = ( options && options . profilesSampleRate ) || 0 ;
92
- if ( profilesSampleRate === undefined ) {
93
- if ( __DEBUG_BUILD__ ) {
94
- logger . log ( '[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.' ) ;
95
- }
74
+ // @ts -ignore profilesSampleRate is not part of the browser options yet
75
+ const profilesSampleRate : number | boolean | undefined = options . profilesSampleRate ;
76
+
77
+ // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
78
+ // only valid values are booleans or numbers between 0 and 1.)
79
+ if ( ! isValidSampleRate ( profilesSampleRate ) ) {
80
+ __DEBUG_BUILD__ && logger . warn ( '[Profiling] Discarding profile because of invalid sample rate.' ) ;
96
81
return transaction ;
97
82
}
98
83
84
+ // if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
85
+ if ( ! profilesSampleRate ) {
86
+ __DEBUG_BUILD__ &&
87
+ logger . log (
88
+ '[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0' ,
89
+ ) ;
90
+ return transaction ;
91
+ }
92
+
93
+ // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
94
+ // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
95
+ const sampled = profilesSampleRate === true ? true : Math . random ( ) < profilesSampleRate ;
99
96
// Check if we should sample this profile
100
- if ( Math . random ( ) > profilesSampleRate ) {
101
- if ( __DEBUG_BUILD__ ) {
102
- logger . log ( '[Profiling] Skip profiling transaction due to sampling.' ) ;
103
- }
97
+ if ( ! sampled ) {
98
+ __DEBUG_BUILD__ &&
99
+ logger . log (
100
+ `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${ Number (
101
+ profilesSampleRate ,
102
+ ) } )`,
103
+ ) ;
104
104
return transaction ;
105
105
}
106
106
@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
147
147
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
148
148
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
149
149
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
150
- let processedProfile : ProcessedJSSelfProfile | null = null ;
150
+ const processedProfile : JSSelfProfile | null = null ;
151
151
152
152
/**
153
153
* Idempotent handler for profile stop
154
154
*/
155
- function onProfileHandler ( ) : void {
155
+ async function onProfileHandler ( ) : Promise < null > {
156
156
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
157
157
if ( ! transaction ) {
158
- return ;
158
+ return null ;
159
159
}
160
160
// Satisfy the type checker, but profiler will always be defined here.
161
161
if ( ! profiler ) {
162
- return ;
162
+ return null ;
163
163
}
164
164
if ( processedProfile ) {
165
165
if ( __DEBUG_BUILD__ ) {
@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
169
169
'already exists, returning early' ,
170
170
) ;
171
171
}
172
- return ;
172
+ return null ;
173
173
}
174
174
175
- profiler
175
+ return profiler
176
176
. stop ( )
177
- . then ( ( p : JSSelfProfile ) : void => {
177
+ . then ( ( p : JSSelfProfile ) : null => {
178
178
if ( maxDurationTimeoutID ) {
179
179
WINDOW . clearTimeout ( maxDurationTimeoutID ) ;
180
180
maxDurationTimeoutID = undefined ;
@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
192
192
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started' ,
193
193
) ;
194
194
}
195
- return ;
196
- }
197
-
198
- // If a profile has less than 2 samples, it is not useful and should be discarded.
199
- if ( p . samples . length < 2 ) {
200
- return ;
195
+ return null ;
201
196
}
202
197
203
- processedProfile = { ... p , profile_id : profileId } ;
204
- sendProfile ( profileId , processedProfile ) ;
198
+ addProfileToMap ( profileId , p ) ;
199
+ return null ;
205
200
} )
206
201
. catch ( error => {
207
202
if ( __DEBUG_BUILD__ ) {
@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
219
214
transaction . name || transaction . description ,
220
215
) ;
221
216
}
217
+ // If the timeout exceeds, we want to stop profiling, but not finish the transaction
222
218
void onProfileHandler ( ) ;
223
219
} , MAX_PROFILE_DURATION_MS ) ;
224
220
@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
230
226
* startProfiling is called after the call to startTransaction in order to avoid our own code from
231
227
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
232
228
*/
233
- function profilingWrappedTransactionFinish ( ) : Promise < Transaction > {
229
+ function profilingWrappedTransactionFinish ( ) : Transaction {
234
230
if ( ! transaction ) {
235
231
return originalFinish ( ) ;
236
232
}
237
233
// onProfileHandler should always return the same profile even if this is called multiple times.
238
234
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
239
- onProfileHandler ( ) ;
240
-
241
- // Set profile context
242
- transaction . setContext ( 'profile' , { profile_id : profileId } ) ;
235
+ void onProfileHandler ( ) . then (
236
+ ( ) => {
237
+ transaction . setContext ( 'profile' , { profile_id : profileId } ) ;
238
+ originalFinish ( ) ;
239
+ } ,
240
+ ( ) => {
241
+ // If onProfileHandler fails, we still want to call the original finish method.
242
+ originalFinish ( ) ;
243
+ } ,
244
+ ) ;
243
245
244
- return originalFinish ( ) ;
246
+ return transaction ;
245
247
}
246
248
247
249
transaction . finish = profilingWrappedTransactionFinish ;
248
250
return transaction ;
249
251
}
250
-
251
- /**
252
- * Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
253
- */
254
- function __PRIVATE__wrapStartTransactionWithProfiling ( startTransaction : StartTransaction ) : StartTransaction {
255
- return function wrappedStartTransaction (
256
- this : Hub ,
257
- transactionContext : TransactionContext ,
258
- customSamplingContext ?: CustomSamplingContext ,
259
- ) : Transaction | undefined {
260
- const transaction : Transaction | undefined = startTransaction . call ( this , transactionContext , customSamplingContext ) ;
261
- if ( transaction === undefined ) {
262
- if ( __DEBUG_BUILD__ ) {
263
- logger . log ( '[Profiling] Transaction is undefined, skipping profiling' ) ;
264
- }
265
- return transaction ;
266
- }
267
-
268
- return wrapTransactionWithProfiling ( transaction ) ;
269
- } ;
270
- }
271
-
272
- /**
273
- * Patches startTransaction and stopTransaction with profiling logic.
274
- */
275
- export function addProfilingExtensionMethods ( ) : void {
276
- const carrier = getMainCarrier ( ) ;
277
- if ( ! carrier . __SENTRY__ ) {
278
- if ( __DEBUG_BUILD__ ) {
279
- logger . log ( "[Profiling] Can't find main carrier, profiling won't work." ) ;
280
- }
281
- return ;
282
- }
283
- carrier . __SENTRY__ . extensions = carrier . __SENTRY__ . extensions || { } ;
284
-
285
- if ( ! carrier . __SENTRY__ . extensions [ 'startTransaction' ] ) {
286
- if ( __DEBUG_BUILD__ ) {
287
- logger . log (
288
- '[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.' ,
289
- ) ;
290
- }
291
- return ;
292
- }
293
-
294
- if ( __DEBUG_BUILD__ ) {
295
- logger . log ( '[Profiling] startTransaction exists, patching it with profiling functionality...' ) ;
296
- }
297
-
298
- carrier . __SENTRY__ . extensions [ 'startTransaction' ] = __PRIVATE__wrapStartTransactionWithProfiling (
299
- // This is already patched by sentry/tracing, we are going to re-patch it...
300
- carrier . __SENTRY__ . extensions [ 'startTransaction' ] as StartTransaction ,
301
- ) ;
302
- }
0 commit comments