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' ;
34import { logger , uuid4 } from '@sentry/utils' ;
45
56import { 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' ;
139
14- // Max profile duration.
15- const MAX_PROFILE_DURATION_MS = 30_000 ;
10+ export const MAX_PROFILE_DURATION_MS = 30_000 ;
1611// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
1712// once, it will always fail and this allows us to early return.
1813let PROFILING_CONSTRUCTOR_FAILED = false ;
1914
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-
2715/**
2816 * Check if profiler constructor is available.
2917 * @param maybeProfiler
@@ -55,7 +43,7 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
5543 * startProfiling is called after the call to startTransaction in order to avoid our own code from
5644 * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
5745 */
58- function wrapTransactionWithProfiling ( transaction : Transaction ) : Transaction {
46+ export function wrapTransactionWithProfiling ( transaction : Transaction ) : Transaction {
5947 // Feature support check first
6048 const JSProfilerConstructor = WINDOW . Profiler ;
6149
@@ -68,14 +56,6 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
6856 return transaction ;
6957 }
7058
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-
7959 // If constructor failed once, it will always fail, so we can early return.
8060 if ( PROFILING_CONSTRUCTOR_FAILED ) {
8161 if ( __DEBUG_BUILD__ ) {
@@ -86,21 +66,41 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
8666
8767 const client = getCurrentHub ( ) . getClient ( ) ;
8868 const options = client && client . getOptions ( ) ;
69+ if ( ! options ) {
70+ __DEBUG_BUILD__ && logger . log ( '[Profiling] Profiling disabled, no options found.' ) ;
71+ return transaction ;
72+ }
8973
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.' ) ;
9681 return transaction ;
9782 }
9883
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 ;
9996 // 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+ ) ;
104104 return transaction ;
105105 }
106106
@@ -147,19 +147,19 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
147147 // event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
148148 // is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
149149 // 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 ;
151151
152152 /**
153153 * Idempotent handler for profile stop
154154 */
155- function onProfileHandler ( ) : void {
155+ async function onProfileHandler ( ) : Promise < null > {
156156 // Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
157157 if ( ! transaction ) {
158- return ;
158+ return null ;
159159 }
160160 // Satisfy the type checker, but profiler will always be defined here.
161161 if ( ! profiler ) {
162- return ;
162+ return null ;
163163 }
164164 if ( processedProfile ) {
165165 if ( __DEBUG_BUILD__ ) {
@@ -169,12 +169,12 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
169169 'already exists, returning early' ,
170170 ) ;
171171 }
172- return ;
172+ return null ;
173173 }
174174
175- profiler
175+ return profiler
176176 . stop ( )
177- . then ( ( p : JSSelfProfile ) : void => {
177+ . then ( ( p : JSSelfProfile ) : null => {
178178 if ( maxDurationTimeoutID ) {
179179 WINDOW . clearTimeout ( maxDurationTimeoutID ) ;
180180 maxDurationTimeoutID = undefined ;
@@ -192,16 +192,11 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
192192 'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started' ,
193193 ) ;
194194 }
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 ;
201196 }
202197
203- processedProfile = { ... p , profile_id : profileId } ;
204- sendProfile ( profileId , processedProfile ) ;
198+ addProfileToMap ( profileId , p ) ;
199+ return null ;
205200 } )
206201 . catch ( error => {
207202 if ( __DEBUG_BUILD__ ) {
@@ -219,6 +214,7 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
219214 transaction . name || transaction . description ,
220215 ) ;
221216 }
217+ // If the timeout exceeds, we want to stop profiling, but not finish the transaction
222218 void onProfileHandler ( ) ;
223219 } , MAX_PROFILE_DURATION_MS ) ;
224220
@@ -230,73 +226,26 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
230226 * startProfiling is called after the call to startTransaction in order to avoid our own code from
231227 * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
232228 */
233- function profilingWrappedTransactionFinish ( ) : Promise < Transaction > {
229+ function profilingWrappedTransactionFinish ( ) : Transaction {
234230 if ( ! transaction ) {
235231 return originalFinish ( ) ;
236232 }
237233 // onProfileHandler should always return the same profile even if this is called multiple times.
238234 // 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+ ) ;
243245
244- return originalFinish ( ) ;
246+ return transaction ;
245247 }
246248
247249 transaction . finish = profilingWrappedTransactionFinish ;
248250 return transaction ;
249251}
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