diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /shared/logger/node_modules/@sentry/browser/esm/profiling/hubextensions.js | |
init commit
Diffstat (limited to 'shared/logger/node_modules/@sentry/browser/esm/profiling/hubextensions.js')
| -rw-r--r-- | shared/logger/node_modules/@sentry/browser/esm/profiling/hubextensions.js | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/shared/logger/node_modules/@sentry/browser/esm/profiling/hubextensions.js b/shared/logger/node_modules/@sentry/browser/esm/profiling/hubextensions.js new file mode 100644 index 0000000..c1cd92d --- /dev/null +++ b/shared/logger/node_modules/@sentry/browser/esm/profiling/hubextensions.js @@ -0,0 +1,240 @@ +import { getCurrentHub } from '@sentry/core'; +import { logger, uuid4 } from '@sentry/utils'; +import { WINDOW } from '../helpers.js'; +import { isValidSampleRate, addProfileToMap } from './utils.js'; + +/* eslint-disable complexity */ + +const MAX_PROFILE_DURATION_MS = 30000; +// Keep a flag value to avoid re-initializing the profiler constructor. If it fails +// once, it will always fail and this allows us to early return. +let PROFILING_CONSTRUCTOR_FAILED = false; + +/** + * Check if profiler constructor is available. + * @param maybeProfiler + */ +function isJSProfilerSupported(maybeProfiler) { + return typeof maybeProfiler === 'function'; +} + +/** + * Safety wrapper for startTransaction for the unlikely case that transaction starts before tracing is imported - + * if that happens we want to avoid throwing an error from profiling code. + * see https://github.com/getsentry/sentry-javascript/issues/4731. + * + * @experimental + */ +function onProfilingStartRouteTransaction(transaction) { + if (!transaction) { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log('[Profiling] Transaction is undefined, skipping profiling'); + } + return transaction; + } + + return wrapTransactionWithProfiling(transaction); +} + +/** + * Wraps startTransaction and stopTransaction with profiling related logic. + * startProfiling is called after the call to startTransaction in order to avoid our own code from + * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction. + */ +function wrapTransactionWithProfiling(transaction) { + // Feature support check first + const JSProfilerConstructor = WINDOW.Profiler; + + if (!isJSProfilerSupported(JSProfilerConstructor)) { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log( + '[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.', + ); + } + return transaction; + } + + // If constructor failed once, it will always fail, so we can early return. + if (PROFILING_CONSTRUCTOR_FAILED) { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log('[Profiling] Profiling has been disabled for the duration of the current user session.'); + } + return transaction; + } + + const client = getCurrentHub().getClient(); + const options = client && client.getOptions(); + if (!options) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Profiling] Profiling disabled, no options found.'); + return transaction; + } + + // @ts-ignore profilesSampleRate is not part of the browser options yet + const profilesSampleRate = options.profilesSampleRate; + + // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The + // only valid values are booleans or numbers between 0 and 1.) + if (!isValidSampleRate(profilesSampleRate)) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('[Profiling] Discarding profile because of invalid sample rate.'); + return transaction; + } + + // if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped + if (!profilesSampleRate) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && + logger.log( + '[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0', + ); + return transaction; + } + + // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is + // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. + const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate; + // Check if we should sample this profile + if (!sampled) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && + logger.log( + `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number( + profilesSampleRate, + )})`, + ); + return transaction; + } + + // From initial testing, it seems that the minimum value for sampleInterval is 10ms. + const samplingIntervalMS = 10; + // Start the profiler + const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingIntervalMS); + let profiler; + + // Attempt to initialize the profiler constructor, if it fails, we disable profiling for the current user session. + // This is likely due to a missing 'Document-Policy': 'js-profiling' header. We do not want to throw an error if this happens + // as we risk breaking the user's application, so just disable profiling and log an error. + try { + profiler = new JSProfilerConstructor({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples }); + } catch (e) { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log( + "[Profiling] Failed to initialize the Profiling constructor, this is likely due to a missing 'Document-Policy': 'js-profiling' header.", + ); + logger.log('[Profiling] Disabling profiling for current user session.'); + } + PROFILING_CONSTRUCTOR_FAILED = true; + } + + // We failed to construct the profiler, fallback to original transaction - there is no need to log + // anything as we already did that in the try/catch block. + if (!profiler) { + return transaction; + } + + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log(`[Profiling] started profiling transaction: ${transaction.name || transaction.description}`); + } + + // We create "unique" transaction names to avoid concurrent transactions with same names + // from being ignored by the profiler. From here on, only this transaction name should be used when + // calling the profiler methods. Note: we log the original name to the user to avoid confusion. + const profileId = uuid4(); + + /** + * Idempotent handler for profile stop + */ + async function onProfileHandler() { + // Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times. + if (!transaction) { + return null; + } + // Satisfy the type checker, but profiler will always be defined here. + if (!profiler) { + return null; + } + + // This is temporary - we will use the collected span data to evaluate + // if deferring txn.finish until profiler resolves is a viable approach. + const stopProfilerSpan = transaction.startChild({ description: 'profiler.stop', op: 'profiler' }); + + return profiler + .stop() + .then((p) => { + stopProfilerSpan.finish(); + + if (maxDurationTimeoutID) { + WINDOW.clearTimeout(maxDurationTimeoutID); + maxDurationTimeoutID = undefined; + } + + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log(`[Profiling] stopped profiling of transaction: ${transaction.name || transaction.description}`); + } + + // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile. + if (!p) { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log( + `[Profiling] profiler returned null profile for: ${transaction.name || transaction.description}`, + 'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started', + ); + } + return null; + } + + addProfileToMap(profileId, p); + return null; + }) + .catch(error => { + stopProfilerSpan.finish(); + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log('[Profiling] error while stopping profiler:', error); + } + return null; + }); + } + + // Enqueue a timeout to prevent profiles from running over max duration. + let maxDurationTimeoutID = WINDOW.setTimeout(() => { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log( + '[Profiling] max profile duration elapsed, stopping profiling for:', + transaction.name || transaction.description, + ); + } + // If the timeout exceeds, we want to stop profiling, but not finish the transaction + void onProfileHandler(); + }, MAX_PROFILE_DURATION_MS); + + // We need to reference the original finish call to avoid creating an infinite loop + const originalFinish = transaction.finish.bind(transaction); + + /** + * Wraps startTransaction and stopTransaction with profiling related logic. + * startProfiling is called after the call to startTransaction in order to avoid our own code from + * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction. + */ + function profilingWrappedTransactionFinish() { + if (!transaction) { + return originalFinish(); + } + // onProfileHandler should always return the same profile even if this is called multiple times. + // Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared. + void onProfileHandler().then( + () => { + transaction.setContext('profile', { profile_id: profileId }); + originalFinish(); + }, + () => { + // If onProfileHandler fails, we still want to call the original finish method. + originalFinish(); + }, + ); + + return transaction; + } + + transaction.finish = profilingWrappedTransactionFinish; + return transaction; +} + +export { MAX_PROFILE_DURATION_MS, onProfilingStartRouteTransaction, wrapTransactionWithProfiling }; +//# sourceMappingURL=hubextensions.js.map |
