|
1 |
| -import { getCurrentHub, getMainCarrier } from '@sentry/core'; |
2 |
| -import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types'; |
| 1 | +import { getCurrentHub } from '@sentry/core'; |
| 2 | +import type { CustomSamplingContext, Transaction } from '@sentry/types'; |
3 | 3 | import { logger, uuid4 } from '@sentry/utils';
|
4 | 4 |
|
| 5 | +import type { BrowserClient } from '../client'; |
5 | 6 | import { WINDOW } from '../helpers';
|
6 | 7 | import type {
|
7 | 8 | JSSelfProfile,
|
8 | 9 | JSSelfProfiler,
|
9 | 10 | JSSelfProfilerConstructor,
|
10 | 11 | ProcessedJSSelfProfile,
|
11 | 12 | } from './jsSelfProfiling';
|
12 |
| -import { sendProfile } from './sendProfile'; |
| 13 | +import { isValidSampleRate } from './utils'; |
13 | 14 |
|
14 |
| -// Max profile duration. |
15 |
| -const MAX_PROFILE_DURATION_MS = 30_000; |
| 15 | +export const MAX_PROFILE_DURATION_MS = 30_000; |
16 | 16 | // Keep a flag value to avoid re-initializing the profiler constructor. If it fails
|
17 | 17 | // once, it will always fail and this allows us to early return.
|
18 | 18 | let PROFILING_CONSTRUCTOR_FAILED = false;
|
19 | 19 |
|
20 |
| -// While we experiment, per transaction sampling interval will be more flexible to work with. |
21 |
| -type StartTransaction = ( |
22 |
| - this: Hub, |
23 |
| - transactionContext: TransactionContext, |
| 20 | +// Takes a transaction and determines if it should be profiled or not. If it should be profiled, it returns the |
| 21 | +// profile_id, otherwise returns undefined. Takes care of setting profile context on transaction as well |
| 22 | +/** |
| 23 | + * |
| 24 | + */ |
| 25 | +export function maybeProfileTransaction( |
| 26 | + client: BrowserClient, |
| 27 | + transaction: Transaction, |
24 | 28 | customSamplingContext?: CustomSamplingContext,
|
25 |
| -) => Transaction | undefined; |
| 29 | +): string | undefined { |
| 30 | + // profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate. We dont perform |
| 31 | + // the actual multiplication to get the final rate, but we discard the profile if the transaction was sampled, |
| 32 | + // so anything after this block from here is based on the transaction sampling. |
| 33 | + if (!transaction.sampled) { |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + // Client and options are required for profiling |
| 38 | + if (!client) { |
| 39 | + __DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no client found.'); |
| 40 | + return; |
| 41 | + } |
| 42 | + |
| 43 | + const options = client.getOptions(); |
| 44 | + if (!options) { |
| 45 | + __DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.'); |
| 46 | + return; |
| 47 | + } |
| 48 | + |
| 49 | + // @ts-ignore profilesSampler is not part of the browser options yet |
| 50 | + const profilesSampler = options.profilesSampler; |
| 51 | + // @ts-ignore profilesSampleRate is not part of the browser options yet |
| 52 | + let profilesSampleRate: number | boolean | undefined = options.profilesSampleRate; |
| 53 | + |
| 54 | + // Prefer sampler to sample rate if both are provided. |
| 55 | + if (typeof profilesSampler === 'function') { |
| 56 | + profilesSampleRate = profilesSampler({ transactionContext: transaction.toContext(), ...customSamplingContext }); |
| 57 | + } |
| 58 | + |
| 59 | + // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The |
| 60 | + // only valid values are booleans or numbers between 0 and 1.) |
| 61 | + if (!isValidSampleRate(profilesSampleRate)) { |
| 62 | + __DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.'); |
| 63 | + return; |
| 64 | + } |
| 65 | + |
| 66 | + // if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped |
| 67 | + if (!profilesSampleRate) { |
| 68 | + __DEBUG_BUILD__ && |
| 69 | + logger.log( |
| 70 | + `[Profiling] Discarding profile because ${ |
| 71 | + typeof profilesSampler === 'function' |
| 72 | + ? 'profileSampler returned 0 or false' |
| 73 | + : 'a negative sampling decision was inherited or profileSampleRate is set to 0' |
| 74 | + }`, |
| 75 | + ); |
| 76 | + return; |
| 77 | + } |
| 78 | + |
| 79 | + // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is |
| 80 | + // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. |
| 81 | + const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate; |
| 82 | + // Check if we should sample this profile |
| 83 | + if (!sampled) { |
| 84 | + __DEBUG_BUILD__ && |
| 85 | + logger.log( |
| 86 | + `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number( |
| 87 | + profilesSampleRate, |
| 88 | + )})`, |
| 89 | + ); |
| 90 | + return; |
| 91 | + } |
| 92 | + |
| 93 | + const profile_id = uuid4(); |
| 94 | + CpuProfilerBindings.startProfiling(profile_id); |
| 95 | + |
| 96 | + __DEBUG_BUILD__ && logger.log(`[Profiling] started profiling transaction: ${transaction.name}`); |
| 97 | + |
| 98 | + // set transaction context - do this regardless if profiling fails down the line |
| 99 | + // so that we can still see the profile_id in the transaction context |
| 100 | + return profile_id; |
| 101 | +} |
| 102 | + |
| 103 | +/** |
| 104 | + * |
| 105 | + */ |
| 106 | +export function stopTransactionProfile( |
| 107 | + transaction: Transaction, |
| 108 | + profile_id: string | undefined, |
| 109 | +): ReturnType<(typeof CpuProfilerBindings)['stopProfiling']> | null { |
| 110 | + // Should not happen, but satisfy the type checker and be safe regardless. |
| 111 | + if (!profile_id) { |
| 112 | + return null; |
| 113 | + } |
| 114 | + |
| 115 | + const profile = CpuProfilerBindings.stopProfiling(profile_id); |
| 116 | + |
| 117 | + __DEBUG_BUILD__ && logger.log(`[Profiling] stopped profiling of transaction: ${transaction.name}`); |
| 118 | + |
| 119 | + // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile. |
| 120 | + if (!profile) { |
| 121 | + __DEBUG_BUILD__ && |
| 122 | + logger.log( |
| 123 | + `[Profiling] profiler returned null profile for: ${transaction.name}`, |
| 124 | + 'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started', |
| 125 | + ); |
| 126 | + return null; |
| 127 | + } |
| 128 | + |
| 129 | + // Assign profile_id to the profile |
| 130 | + profile.profile_id = profile_id; |
| 131 | + return profile; |
| 132 | +} |
26 | 133 |
|
27 | 134 | /**
|
28 | 135 | * Check if profiler constructor is available.
|
@@ -247,56 +354,3 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
|
247 | 354 | transaction.finish = profilingWrappedTransactionFinish;
|
248 | 355 | return transaction;
|
249 | 356 | }
|
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