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