11/* eslint-disable complexity */
2- import { getCurrentHub } from '@sentry/core' ;
32import type { Transaction } from '@sentry/types' ;
4- import { logger , uuid4 } from '@sentry/utils' ;
3+ import { logger , timestampInSeconds , uuid4 } from '@sentry/utils' ;
54
65import { WINDOW } from '../helpers' ;
7- import type { JSSelfProfile , JSSelfProfiler , JSSelfProfilerConstructor } from './jsSelfProfiling' ;
8- import { addProfileToMap , isValidSampleRate } from './utils' ;
9-
10- export const MAX_PROFILE_DURATION_MS = 30_000 ;
11- // Keep a flag value to avoid re-initializing the profiler constructor. If it fails
12- // once, it will always fail and this allows us to early return.
13- let PROFILING_CONSTRUCTOR_FAILED = false ;
14-
15- /**
16- * Check if profiler constructor is available.
17- * @param maybeProfiler
18- */
19- function isJSProfilerSupported ( maybeProfiler : unknown ) : maybeProfiler is typeof JSSelfProfilerConstructor {
20- return typeof maybeProfiler === 'function' ;
21- }
6+ import type { JSSelfProfile } from './jsSelfProfiling' ;
7+ import {
8+ addProfileToGlobalCache ,
9+ isAutomatedPageLoadTransaction ,
10+ MAX_PROFILE_DURATION_MS ,
11+ shouldProfileTransaction ,
12+ startJSSelfProfile ,
13+ } from './utils' ;
2214
2315/**
2416 * Safety wrapper for startTransaction for the unlikely case that transaction starts before tracing is imported -
@@ -35,98 +27,29 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
3527 return transaction ;
3628 }
3729
38- return wrapTransactionWithProfiling ( transaction ) ;
30+ if ( shouldProfileTransaction ( transaction ) ) {
31+ return startProfileForTransaction ( transaction ) ;
32+ }
33+
34+ return transaction ;
3935}
4036
4137/**
4238 * Wraps startTransaction and stopTransaction with profiling related logic.
43- * startProfiling is called after the call to startTransaction in order to avoid our own code from
39+ * startProfileForTransaction is called after the call to startTransaction in order to avoid our own code from
4440 * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
4541 */
46- export function wrapTransactionWithProfiling ( transaction : Transaction ) : Transaction {
47- // Feature support check first
48- const JSProfilerConstructor = WINDOW . Profiler ;
49-
50- if ( ! isJSProfilerSupported ( JSProfilerConstructor ) ) {
51- if ( __DEBUG_BUILD__ ) {
52- logger . log (
53- '[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.' ,
54- ) ;
55- }
56- return transaction ;
57- }
58-
59- // If constructor failed once, it will always fail, so we can early return.
60- if ( PROFILING_CONSTRUCTOR_FAILED ) {
61- if ( __DEBUG_BUILD__ ) {
62- logger . log ( '[Profiling] Profiling has been disabled for the duration of the current user session.' ) ;
63- }
64- return transaction ;
65- }
66-
67- const client = getCurrentHub ( ) . getClient ( ) ;
68- const options = client && client . getOptions ( ) ;
69- if ( ! options ) {
70- __DEBUG_BUILD__ && logger . log ( '[Profiling] Profiling disabled, no options found.' ) ;
71- return transaction ;
42+ export function startProfileForTransaction ( transaction : Transaction ) : Transaction {
43+ // Start the profiler and get the profiler instance.
44+ let startTimestamp : number | undefined ;
45+ if ( isAutomatedPageLoadTransaction ( transaction ) ) {
46+ startTimestamp = timestampInSeconds ( ) * 1000 ;
7247 }
7348
74- // @ts -expect-error profilesSampleRate is not part of the browser options yet
75- const profilesSampleRate : number | boolean | undefined = options . profilesSampleRate ;
49+ const profiler = startJSSelfProfile ( ) ;
7650
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.' ) ;
81- return transaction ;
82- }
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 ;
96- // Check if we should sample this profile
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- return transaction ;
105- }
106-
107- // From initial testing, it seems that the minimum value for sampleInterval is 10ms.
108- const samplingIntervalMS = 10 ;
109- // Start the profiler
110- const maxSamples = Math . floor ( MAX_PROFILE_DURATION_MS / samplingIntervalMS ) ;
111- let profiler : JSSelfProfiler | undefined ;
112-
113- // Attempt to initialize the profiler constructor, if it fails, we disable profiling for the current user session.
114- // This is likely due to a missing 'Document-Policy': 'js-profiling' header. We do not want to throw an error if this happens
115- // as we risk breaking the user's application, so just disable profiling and log an error.
116- try {
117- profiler = new JSProfilerConstructor ( { sampleInterval : samplingIntervalMS , maxBufferSize : maxSamples } ) ;
118- } catch ( e ) {
119- if ( __DEBUG_BUILD__ ) {
120- logger . log (
121- "[Profiling] Failed to initialize the Profiling constructor, this is likely due to a missing 'Document-Policy': 'js-profiling' header." ,
122- ) ;
123- logger . log ( '[Profiling] Disabling profiling for current user session.' ) ;
124- }
125- PROFILING_CONSTRUCTOR_FAILED = true ;
126- }
127-
128- // We failed to construct the profiler, fallback to original transaction - there is no need to log
129- // anything as we already did that in the try/catch block.
51+ // We failed to construct the profiler, fallback to original transaction.
52+ // No need to log anything as this has already been logged in startProfile.
13053 if ( ! profiler ) {
13154 return transaction ;
13255 }
@@ -172,19 +95,9 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
17295 return null ;
17396 }
17497
175- // This is temporary - we will use the collected span data to evaluate
176- // if deferring txn.finish until profiler resolves is a viable approach.
177- const stopProfilerSpan = transaction . startChild ( {
178- description : 'profiler.stop' ,
179- op : 'profiler' ,
180- origin : 'auto.profiler.browser' ,
181- } ) ;
182-
18398 return profiler
18499 . stop ( )
185- . then ( ( p : JSSelfProfile ) : null => {
186- stopProfilerSpan . finish ( ) ;
187-
100+ . then ( ( profile : JSSelfProfile ) : null => {
188101 if ( maxDurationTimeoutID ) {
189102 WINDOW . clearTimeout ( maxDurationTimeoutID ) ;
190103 maxDurationTimeoutID = undefined ;
@@ -195,7 +108,7 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
195108 }
196109
197110 // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile.
198- if ( ! p ) {
111+ if ( ! profile ) {
199112 if ( __DEBUG_BUILD__ ) {
200113 logger . log (
201114 `[Profiling] profiler returned null profile for: ${ transaction . name || transaction . description } ` ,
@@ -205,11 +118,10 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
205118 return null ;
206119 }
207120
208- addProfileToMap ( profileId , p ) ;
121+ addProfileToGlobalCache ( profileId , profile ) ;
209122 return null ;
210123 } )
211124 . catch ( error => {
212- stopProfilerSpan . finish ( ) ;
213125 if ( __DEBUG_BUILD__ ) {
214126 logger . log ( '[Profiling] error while stopping profiler:' , error ) ;
215127 }
@@ -245,7 +157,7 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
245157 // Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
246158 void onProfileHandler ( ) . then (
247159 ( ) => {
248- transaction . setContext ( 'profile' , { profile_id : profileId } ) ;
160+ transaction . setContext ( 'profile' , { profile_id : profileId , start_timestamp : startTimestamp } ) ;
249161 originalFinish ( ) ;
250162 } ,
251163 ( ) => {
0 commit comments