1
1
/* eslint-disable complexity */
2
- import { getCurrentHub } from '@sentry/core';
3
2
import type { Transaction } from '@sentry/types';
4
- import { logger, uuid4 } from '@sentry/utils';
3
+ import { logger, timestampInSeconds, uuid4 } from '@sentry/utils';
5
4
6
5
import { 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';
22
14
23
15
/**
24
16
* 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
35
27
return transaction;
36
28
}
37
29
38
- return wrapTransactionWithProfiling(transaction);
30
+ if (shouldProfileTransaction(transaction)) {
31
+ return startProfileForTransaction(transaction);
32
+ }
33
+
34
+ return transaction;
39
35
}
40
36
41
37
/**
42
38
* 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
44
40
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
45
41
*/
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;
72
47
}
73
48
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();
76
50
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.
130
53
if (!profiler) {
131
54
return transaction;
132
55
}
@@ -172,19 +95,9 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
172
95
return null;
173
96
}
174
97
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
-
183
98
return profiler
184
99
.stop()
185
- .then((p: JSSelfProfile): null => {
186
- stopProfilerSpan.finish();
187
-
100
+ .then((profile: JSSelfProfile): null => {
188
101
if (maxDurationTimeoutID) {
189
102
WINDOW.clearTimeout(maxDurationTimeoutID);
190
103
maxDurationTimeoutID = undefined;
@@ -195,7 +108,7 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
195
108
}
196
109
197
110
// In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile.
198
- if (!p ) {
111
+ if (!profile ) {
199
112
if (__DEBUG_BUILD__) {
200
113
logger.log(
201
114
`[Profiling] profiler returned null profile for: ${transaction.name || transaction.description}`,
@@ -205,11 +118,10 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
205
118
return null;
206
119
}
207
120
208
- addProfileToMap (profileId, p );
121
+ addProfileToGlobalCache (profileId, profile );
209
122
return null;
210
123
})
211
124
.catch(error => {
212
- stopProfilerSpan.finish();
213
125
if (__DEBUG_BUILD__) {
214
126
logger.log('[Profiling] error while stopping profiler:', error);
215
127
}
@@ -245,7 +157,7 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
245
157
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
246
158
void onProfileHandler().then(
247
159
() => {
248
- transaction.setContext('profile', { profile_id: profileId });
160
+ transaction.setContext('profile', { profile_id: profileId, start_timestamp: startTimestamp });
249
161
originalFinish();
250
162
},
251
163
() => {
0 commit comments