summaryrefslogtreecommitdiff
path: root/shared/logger/node_modules/@sentry/browser/esm/profiling/hubextensions.js
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /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.js240
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