From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../@sentry/browser/esm/profiling/hubextensions.js | 240 +++++++++++ .../@sentry/browser/esm/profiling/integration.js | 81 ++++ .../@sentry/browser/esm/profiling/utils.js | 438 +++++++++++++++++++++ 3 files changed, 759 insertions(+) create mode 100644 shared/logger/node_modules/@sentry/browser/esm/profiling/hubextensions.js create mode 100644 shared/logger/node_modules/@sentry/browser/esm/profiling/integration.js create mode 100644 shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js (limited to 'shared/logger/node_modules/@sentry/browser/esm/profiling') 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 diff --git a/shared/logger/node_modules/@sentry/browser/esm/profiling/integration.js b/shared/logger/node_modules/@sentry/browser/esm/profiling/integration.js new file mode 100644 index 0000000..f09e4ed --- /dev/null +++ b/shared/logger/node_modules/@sentry/browser/esm/profiling/integration.js @@ -0,0 +1,81 @@ +import { logger } from '@sentry/utils'; +import { wrapTransactionWithProfiling } from './hubextensions.js'; +import { PROFILE_MAP, findProfiledTransactionsFromEnvelope, createProfilingEvent, addProfilesToEnvelope } from './utils.js'; + +/** + * Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"] + * This exists because we do not want to await async profiler.stop calls as transaction.finish is called + * in a synchronous context. Instead, we handle sending the profile async from the promise callback and + * rely on being able to pull the event from the cache when we need to construct the envelope. This makes the + * integration less reliable as we might be dropping profiles when the cache is full. + * + * @experimental + */ +class BrowserProfilingIntegration {constructor() { BrowserProfilingIntegration.prototype.__init.call(this);BrowserProfilingIntegration.prototype.__init2.call(this); } + __init() {this.name = 'BrowserProfilingIntegration';} + __init2() {this.getCurrentHub = undefined;} + + /** + * @inheritDoc + */ + setupOnce(addGlobalEventProcessor, getCurrentHub) { + this.getCurrentHub = getCurrentHub; + const client = this.getCurrentHub().getClient() ; + + if (client && typeof client.on === 'function') { + client.on('startTransaction', (transaction) => { + wrapTransactionWithProfiling(transaction); + }); + + client.on('beforeEnvelope', (envelope) => { + // if not profiles are in queue, there is nothing to add to the envelope. + if (!PROFILE_MAP['size']) { + return; + } + + const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); + if (!profiledTransactionEvents.length) { + return; + } + + const profilesToAddToEnvelope = []; + + for (const profiledTransaction of profiledTransactionEvents) { + const context = profiledTransaction && profiledTransaction.contexts; + const profile_id = context && context['profile'] && (context['profile']['profile_id'] ); + + if (!profile_id) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && + logger.log('[Profiling] cannot find profile for a transaction without a profile context'); + continue; + } + + // Remove the profile from the transaction context before sending, relay will take care of the rest. + if (context && context['profile']) { + delete context.profile; + } + + const profile = PROFILE_MAP.get(profile_id); + if (!profile) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); + continue; + } + + PROFILE_MAP.delete(profile_id); + const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction ); + + if (profileEvent) { + profilesToAddToEnvelope.push(profileEvent); + } + } + + addProfilesToEnvelope(envelope, profilesToAddToEnvelope); + }); + } else { + logger.warn('[Profiling] Client does not support hooks, profiling will be disabled'); + } + } +} + +export { BrowserProfilingIntegration }; +//# sourceMappingURL=integration.js.map diff --git a/shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js b/shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js new file mode 100644 index 0000000..e8beae2 --- /dev/null +++ b/shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js @@ -0,0 +1,438 @@ +import { DEFAULT_ENVIRONMENT, getCurrentHub } from '@sentry/core'; +import { forEachEnvelopeItem, logger, uuid4, GLOBAL_OBJ } from '@sentry/utils'; +import { WINDOW } from '../helpers.js'; + +/* eslint-disable max-lines */ + +const MS_TO_NS = 1e6; +// Use 0 as main thread id which is identical to threadId in node:worker_threads +// where main logs 0 and workers seem to log in increments of 1 +const THREAD_ID_STRING = String(0); +const THREAD_NAME = 'main'; + +// Machine properties (eval only once) +let OS_PLATFORM = ''; +let OS_PLATFORM_VERSION = ''; +let OS_ARCH = ''; +let OS_BROWSER = (WINDOW.navigator && WINDOW.navigator.userAgent) || ''; +let OS_MODEL = ''; +const OS_LOCALE = + (WINDOW.navigator && WINDOW.navigator.language) || + (WINDOW.navigator && WINDOW.navigator.languages && WINDOW.navigator.languages[0]) || + ''; + +function isUserAgentData(data) { + return typeof data === 'object' && data !== null && 'getHighEntropyValues' in data; +} + +// @ts-ignore userAgentData is not part of the navigator interface yet +const userAgentData = WINDOW.navigator && WINDOW.navigator.userAgentData; + +if (isUserAgentData(userAgentData)) { + userAgentData + .getHighEntropyValues(['architecture', 'model', 'platform', 'platformVersion', 'fullVersionList']) + .then((ua) => { + OS_PLATFORM = ua.platform || ''; + OS_ARCH = ua.architecture || ''; + OS_MODEL = ua.model || ''; + OS_PLATFORM_VERSION = ua.platformVersion || ''; + + if (ua.fullVersionList && ua.fullVersionList.length > 0) { + const firstUa = ua.fullVersionList[ua.fullVersionList.length - 1]; + OS_BROWSER = `${firstUa.brand} ${firstUa.version}`; + } + }) + .catch(e => void e); +} + +function isProcessedJSSelfProfile(profile) { + return !('thread_metadata' in profile); +} + +// Enriches the profile with threadId of the current thread. +// This is done in node as we seem to not be able to get the info from C native code. +/** + * + */ +function enrichWithThreadInformation(profile) { + if (!isProcessedJSSelfProfile(profile)) { + return profile; + } + + return convertJSSelfProfileToSampledFormat(profile); +} + +// Profile is marked as optional because it is deleted from the metadata +// by the integration before the event is processed by other integrations. + +function getTraceId(event) { + const traceId = event && event.contexts && event.contexts['trace'] && event.contexts['trace']['trace_id']; + // Log a warning if the profile has an invalid traceId (should be uuidv4). + // All profiles and transactions are rejected if this is the case and we want to + // warn users that this is happening if they enable debug flag + if (typeof traceId === 'string' && traceId.length !== 32) { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log(`[Profiling] Invalid traceId: ${traceId} on profiled event`); + } + } + if (typeof traceId !== 'string') { + return ''; + } + + return traceId; +} +/** + * Creates a profiling event envelope from a Sentry event. If profile does not pass + * validation, returns null. + * @param event + * @param dsn + * @param metadata + * @param tunnel + * @returns {EventEnvelope | null} + */ + +/** + * Creates a profiling event envelope from a Sentry event. + */ +function createProfilePayload( + event, + processedProfile, + profile_id, +) { + if (event.type !== 'transaction') { + // createProfilingEventEnvelope should only be called for transactions, + // we type guard this behavior with isProfiledTransactionEvent. + throw new TypeError('Profiling events may only be attached to transactions, this should never occur.'); + } + + if (processedProfile === undefined || processedProfile === null) { + throw new TypeError( + `Cannot construct profiling event envelope without a valid profile. Got ${processedProfile} instead.`, + ); + } + + const traceId = getTraceId(event); + const enrichedThreadProfile = enrichWithThreadInformation(processedProfile); + const transactionStartMs = typeof event.start_timestamp === 'number' ? event.start_timestamp * 1000 : Date.now(); + const transactionEndMs = typeof event.timestamp === 'number' ? event.timestamp * 1000 : Date.now(); + + const profile = { + event_id: profile_id, + timestamp: new Date(transactionStartMs).toISOString(), + platform: 'javascript', + version: '1', + release: event.release || '', + environment: event.environment || DEFAULT_ENVIRONMENT, + runtime: { + name: 'javascript', + version: WINDOW.navigator.userAgent, + }, + os: { + name: OS_PLATFORM, + version: OS_PLATFORM_VERSION, + build_number: OS_BROWSER, + }, + device: { + locale: OS_LOCALE, + model: OS_MODEL, + manufacturer: OS_BROWSER, + architecture: OS_ARCH, + is_emulator: false, + }, + debug_meta: { + images: applyDebugMetadata(processedProfile.resources), + }, + profile: enrichedThreadProfile, + transactions: [ + { + name: event.transaction || '', + id: event.event_id || uuid4(), + trace_id: traceId, + active_thread_id: THREAD_ID_STRING, + relative_start_ns: '0', + relative_end_ns: ((transactionEndMs - transactionStartMs) * 1e6).toFixed(0), + }, + ], + }; + + return profile; +} + +/** + * Converts a JSSelfProfile to a our sampled format. + * Does not currently perform stack indexing. + */ +function convertJSSelfProfileToSampledFormat(input) { + let EMPTY_STACK_ID = undefined; + let STACK_ID = 0; + + // Initialize the profile that we will fill with data + const profile = { + samples: [], + stacks: [], + frames: [], + thread_metadata: { + [THREAD_ID_STRING]: { name: THREAD_NAME }, + }, + }; + + if (!input.samples.length) { + return profile; + } + + // We assert samples.length > 0 above and timestamp should always be present + const start = input.samples[0].timestamp; + + for (let i = 0; i < input.samples.length; i++) { + const jsSample = input.samples[i]; + + // If sample has no stack, add an empty sample + if (jsSample.stackId === undefined) { + if (EMPTY_STACK_ID === undefined) { + EMPTY_STACK_ID = STACK_ID; + profile.stacks[EMPTY_STACK_ID] = []; + STACK_ID++; + } + + profile['samples'][i] = { + // convert ms timestamp to ns + elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0), + stack_id: EMPTY_STACK_ID, + thread_id: THREAD_ID_STRING, + }; + continue; + } + + let stackTop = input.stacks[jsSample.stackId]; + + // Functions in top->down order (root is last) + // We follow the stackTop.parentId trail and collect each visited frameId + const stack = []; + + while (stackTop) { + stack.push(stackTop.frameId); + + const frame = input.frames[stackTop.frameId]; + + // If our frame has not been indexed yet, index it + if (profile.frames[stackTop.frameId] === undefined) { + profile.frames[stackTop.frameId] = { + function: frame.name, + file: frame.resourceId ? input.resources[frame.resourceId] : undefined, + line: frame.line, + column: frame.column, + }; + } + + stackTop = stackTop.parentId === undefined ? undefined : input.stacks[stackTop.parentId]; + } + + const sample = { + // convert ms timestamp to ns + elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0), + stack_id: STACK_ID, + thread_id: THREAD_ID_STRING, + }; + + profile['stacks'][STACK_ID] = stack; + profile['samples'][i] = sample; + STACK_ID++; + } + + return profile; +} + +/** + * Adds items to envelope if they are not already present - mutates the envelope. + * @param envelope + */ +function addProfilesToEnvelope(envelope, profiles) { + if (!profiles.length) { + return envelope; + } + + for (const profile of profiles) { + // @ts-ignore untyped envelope + envelope[1].push([{ type: 'profile' }, profile]); + } + return envelope; +} + +/** + * Finds transactions with profile_id context in the envelope + * @param envelope + * @returns + */ +function findProfiledTransactionsFromEnvelope(envelope) { + const events = []; + + forEachEnvelopeItem(envelope, (item, type) => { + if (type !== 'transaction') { + return; + } + + for (let j = 1; j < item.length; j++) { + const event = item[j] ; + + if (event && event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id']) { + events.push(item[j] ); + } + } + }); + + return events; +} + +const debugIdStackParserCache = new WeakMap(); +/** + * Applies debug meta data to an event from a list of paths to resources (sourcemaps) + */ +function applyDebugMetadata(resource_paths) { + const debugIdMap = GLOBAL_OBJ._sentryDebugIds; + + if (!debugIdMap) { + return []; + } + + const hub = getCurrentHub(); + if (!hub) { + return []; + } + const client = hub.getClient(); + if (!client) { + return []; + } + const options = client.getOptions(); + if (!options) { + return []; + } + const stackParser = options.stackParser; + if (!stackParser) { + return []; + } + + let debugIdStackFramesCache; + const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(stackParser); + if (cachedDebugIdStackFrameCache) { + debugIdStackFramesCache = cachedDebugIdStackFrameCache; + } else { + debugIdStackFramesCache = new Map(); + debugIdStackParserCache.set(stackParser, debugIdStackFramesCache); + } + + // Build a map of filename -> debug_id + const filenameDebugIdMap = Object.keys(debugIdMap).reduce((acc, debugIdStackTrace) => { + let parsedStack; + + const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace); + if (cachedParsedStack) { + parsedStack = cachedParsedStack; + } else { + parsedStack = stackParser(debugIdStackTrace); + debugIdStackFramesCache.set(debugIdStackTrace, parsedStack); + } + + for (let i = parsedStack.length - 1; i >= 0; i--) { + const stackFrame = parsedStack[i]; + const file = stackFrame && stackFrame.filename; + + if (stackFrame && file) { + acc[file] = debugIdMap[debugIdStackTrace] ; + break; + } + } + return acc; + }, {}); + + const images = []; + for (const path of resource_paths) { + if (path && filenameDebugIdMap[path]) { + images.push({ + type: 'sourcemap', + code_file: path, + debug_id: filenameDebugIdMap[path] , + }); + } + } + + return images; +} + +/** + * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). + */ +function isValidSampleRate(rate) { + // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck + if ((typeof rate !== 'number' && typeof rate !== 'boolean') || (typeof rate === 'number' && isNaN(rate))) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && + logger.warn( + `[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( + rate, + )} of type ${JSON.stringify(typeof rate)}.`, + ); + return false; + } + + // Boolean sample rates are always valid + if (rate === true || rate === false) { + return true; + } + + // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false + if (rate < 0 || rate > 1) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && + logger.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`); + return false; + } + return true; +} + +function isValidProfile(profile) { + if (profile.samples.length < 2) { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + // Log a warning if the profile has less than 2 samples so users can know why + // they are not seeing any profiling data and we cant avoid the back and forth + // of asking them to provide us with a dump of the profile data. + logger.log('[Profiling] Discarding profile because it contains less than 2 samples'); + } + return false; + } + + if (!profile.frames.length) { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + logger.log('[Profiling] Discarding profile because it contains no frames'); + } + return false; + } + + return true; +} + +/** + * Creates a profiling envelope item, if the profile does not pass validation, returns null. + * @param event + * @returns {Profile | null} + */ +function createProfilingEvent(profile_id, profile, event) { + if (!isValidProfile(profile)) { + return null; + } + + return createProfilePayload(event, profile, profile_id); +} + +const PROFILE_MAP = new Map(); +/** + * + */ +function addProfileToMap(profile_id, profile) { + PROFILE_MAP.set(profile_id, profile); + + if (PROFILE_MAP.size > 30) { + const last = PROFILE_MAP.keys().next().value; + PROFILE_MAP.delete(last); + } +} + +export { PROFILE_MAP, addProfileToMap, addProfilesToEnvelope, applyDebugMetadata, convertJSSelfProfileToSampledFormat, createProfilePayload, createProfilingEvent, enrichWithThreadInformation, findProfiledTransactionsFromEnvelope, isValidSampleRate }; +//# sourceMappingURL=utils.js.map -- cgit v1.2.3