this repo has no description
1import { DEFAULT_ENVIRONMENT, getCurrentHub } from '@sentry/core'; 2import { forEachEnvelopeItem, logger, uuid4, GLOBAL_OBJ } from '@sentry/utils'; 3import { WINDOW } from '../helpers.js'; 4 5/* eslint-disable max-lines */ 6 7const MS_TO_NS = 1e6; 8// Use 0 as main thread id which is identical to threadId in node:worker_threads 9// where main logs 0 and workers seem to log in increments of 1 10const THREAD_ID_STRING = String(0); 11const THREAD_NAME = 'main'; 12 13// Machine properties (eval only once) 14let OS_PLATFORM = ''; 15let OS_PLATFORM_VERSION = ''; 16let OS_ARCH = ''; 17let OS_BROWSER = (WINDOW.navigator && WINDOW.navigator.userAgent) || ''; 18let OS_MODEL = ''; 19const OS_LOCALE = 20 (WINDOW.navigator && WINDOW.navigator.language) || 21 (WINDOW.navigator && WINDOW.navigator.languages && WINDOW.navigator.languages[0]) || 22 ''; 23 24function isUserAgentData(data) { 25 return typeof data === 'object' && data !== null && 'getHighEntropyValues' in data; 26} 27 28// @ts-ignore userAgentData is not part of the navigator interface yet 29const userAgentData = WINDOW.navigator && WINDOW.navigator.userAgentData; 30 31if (isUserAgentData(userAgentData)) { 32 userAgentData 33 .getHighEntropyValues(['architecture', 'model', 'platform', 'platformVersion', 'fullVersionList']) 34 .then((ua) => { 35 OS_PLATFORM = ua.platform || ''; 36 OS_ARCH = ua.architecture || ''; 37 OS_MODEL = ua.model || ''; 38 OS_PLATFORM_VERSION = ua.platformVersion || ''; 39 40 if (ua.fullVersionList && ua.fullVersionList.length > 0) { 41 const firstUa = ua.fullVersionList[ua.fullVersionList.length - 1]; 42 OS_BROWSER = `${firstUa.brand} ${firstUa.version}`; 43 } 44 }) 45 .catch(e => void e); 46} 47 48function isProcessedJSSelfProfile(profile) { 49 return !('thread_metadata' in profile); 50} 51 52// Enriches the profile with threadId of the current thread. 53// This is done in node as we seem to not be able to get the info from C native code. 54/** 55 * 56 */ 57function enrichWithThreadInformation(profile) { 58 if (!isProcessedJSSelfProfile(profile)) { 59 return profile; 60 } 61 62 return convertJSSelfProfileToSampledFormat(profile); 63} 64 65// Profile is marked as optional because it is deleted from the metadata 66// by the integration before the event is processed by other integrations. 67 68function getTraceId(event) { 69 const traceId = event && event.contexts && event.contexts['trace'] && event.contexts['trace']['trace_id']; 70 // Log a warning if the profile has an invalid traceId (should be uuidv4). 71 // All profiles and transactions are rejected if this is the case and we want to 72 // warn users that this is happening if they enable debug flag 73 if (typeof traceId === 'string' && traceId.length !== 32) { 74 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { 75 logger.log(`[Profiling] Invalid traceId: ${traceId} on profiled event`); 76 } 77 } 78 if (typeof traceId !== 'string') { 79 return ''; 80 } 81 82 return traceId; 83} 84/** 85 * Creates a profiling event envelope from a Sentry event. If profile does not pass 86 * validation, returns null. 87 * @param event 88 * @param dsn 89 * @param metadata 90 * @param tunnel 91 * @returns {EventEnvelope | null} 92 */ 93 94/** 95 * Creates a profiling event envelope from a Sentry event. 96 */ 97function createProfilePayload( 98 event, 99 processedProfile, 100 profile_id, 101) { 102 if (event.type !== 'transaction') { 103 // createProfilingEventEnvelope should only be called for transactions, 104 // we type guard this behavior with isProfiledTransactionEvent. 105 throw new TypeError('Profiling events may only be attached to transactions, this should never occur.'); 106 } 107 108 if (processedProfile === undefined || processedProfile === null) { 109 throw new TypeError( 110 `Cannot construct profiling event envelope without a valid profile. Got ${processedProfile} instead.`, 111 ); 112 } 113 114 const traceId = getTraceId(event); 115 const enrichedThreadProfile = enrichWithThreadInformation(processedProfile); 116 const transactionStartMs = typeof event.start_timestamp === 'number' ? event.start_timestamp * 1000 : Date.now(); 117 const transactionEndMs = typeof event.timestamp === 'number' ? event.timestamp * 1000 : Date.now(); 118 119 const profile = { 120 event_id: profile_id, 121 timestamp: new Date(transactionStartMs).toISOString(), 122 platform: 'javascript', 123 version: '1', 124 release: event.release || '', 125 environment: event.environment || DEFAULT_ENVIRONMENT, 126 runtime: { 127 name: 'javascript', 128 version: WINDOW.navigator.userAgent, 129 }, 130 os: { 131 name: OS_PLATFORM, 132 version: OS_PLATFORM_VERSION, 133 build_number: OS_BROWSER, 134 }, 135 device: { 136 locale: OS_LOCALE, 137 model: OS_MODEL, 138 manufacturer: OS_BROWSER, 139 architecture: OS_ARCH, 140 is_emulator: false, 141 }, 142 debug_meta: { 143 images: applyDebugMetadata(processedProfile.resources), 144 }, 145 profile: enrichedThreadProfile, 146 transactions: [ 147 { 148 name: event.transaction || '', 149 id: event.event_id || uuid4(), 150 trace_id: traceId, 151 active_thread_id: THREAD_ID_STRING, 152 relative_start_ns: '0', 153 relative_end_ns: ((transactionEndMs - transactionStartMs) * 1e6).toFixed(0), 154 }, 155 ], 156 }; 157 158 return profile; 159} 160 161/** 162 * Converts a JSSelfProfile to a our sampled format. 163 * Does not currently perform stack indexing. 164 */ 165function convertJSSelfProfileToSampledFormat(input) { 166 let EMPTY_STACK_ID = undefined; 167 let STACK_ID = 0; 168 169 // Initialize the profile that we will fill with data 170 const profile = { 171 samples: [], 172 stacks: [], 173 frames: [], 174 thread_metadata: { 175 [THREAD_ID_STRING]: { name: THREAD_NAME }, 176 }, 177 }; 178 179 if (!input.samples.length) { 180 return profile; 181 } 182 183 // We assert samples.length > 0 above and timestamp should always be present 184 const start = input.samples[0].timestamp; 185 186 for (let i = 0; i < input.samples.length; i++) { 187 const jsSample = input.samples[i]; 188 189 // If sample has no stack, add an empty sample 190 if (jsSample.stackId === undefined) { 191 if (EMPTY_STACK_ID === undefined) { 192 EMPTY_STACK_ID = STACK_ID; 193 profile.stacks[EMPTY_STACK_ID] = []; 194 STACK_ID++; 195 } 196 197 profile['samples'][i] = { 198 // convert ms timestamp to ns 199 elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0), 200 stack_id: EMPTY_STACK_ID, 201 thread_id: THREAD_ID_STRING, 202 }; 203 continue; 204 } 205 206 let stackTop = input.stacks[jsSample.stackId]; 207 208 // Functions in top->down order (root is last) 209 // We follow the stackTop.parentId trail and collect each visited frameId 210 const stack = []; 211 212 while (stackTop) { 213 stack.push(stackTop.frameId); 214 215 const frame = input.frames[stackTop.frameId]; 216 217 // If our frame has not been indexed yet, index it 218 if (profile.frames[stackTop.frameId] === undefined) { 219 profile.frames[stackTop.frameId] = { 220 function: frame.name, 221 file: frame.resourceId ? input.resources[frame.resourceId] : undefined, 222 line: frame.line, 223 column: frame.column, 224 }; 225 } 226 227 stackTop = stackTop.parentId === undefined ? undefined : input.stacks[stackTop.parentId]; 228 } 229 230 const sample = { 231 // convert ms timestamp to ns 232 elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0), 233 stack_id: STACK_ID, 234 thread_id: THREAD_ID_STRING, 235 }; 236 237 profile['stacks'][STACK_ID] = stack; 238 profile['samples'][i] = sample; 239 STACK_ID++; 240 } 241 242 return profile; 243} 244 245/** 246 * Adds items to envelope if they are not already present - mutates the envelope. 247 * @param envelope 248 */ 249function addProfilesToEnvelope(envelope, profiles) { 250 if (!profiles.length) { 251 return envelope; 252 } 253 254 for (const profile of profiles) { 255 // @ts-ignore untyped envelope 256 envelope[1].push([{ type: 'profile' }, profile]); 257 } 258 return envelope; 259} 260 261/** 262 * Finds transactions with profile_id context in the envelope 263 * @param envelope 264 * @returns 265 */ 266function findProfiledTransactionsFromEnvelope(envelope) { 267 const events = []; 268 269 forEachEnvelopeItem(envelope, (item, type) => { 270 if (type !== 'transaction') { 271 return; 272 } 273 274 for (let j = 1; j < item.length; j++) { 275 const event = item[j] ; 276 277 if (event && event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id']) { 278 events.push(item[j] ); 279 } 280 } 281 }); 282 283 return events; 284} 285 286const debugIdStackParserCache = new WeakMap(); 287/** 288 * Applies debug meta data to an event from a list of paths to resources (sourcemaps) 289 */ 290function applyDebugMetadata(resource_paths) { 291 const debugIdMap = GLOBAL_OBJ._sentryDebugIds; 292 293 if (!debugIdMap) { 294 return []; 295 } 296 297 const hub = getCurrentHub(); 298 if (!hub) { 299 return []; 300 } 301 const client = hub.getClient(); 302 if (!client) { 303 return []; 304 } 305 const options = client.getOptions(); 306 if (!options) { 307 return []; 308 } 309 const stackParser = options.stackParser; 310 if (!stackParser) { 311 return []; 312 } 313 314 let debugIdStackFramesCache; 315 const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(stackParser); 316 if (cachedDebugIdStackFrameCache) { 317 debugIdStackFramesCache = cachedDebugIdStackFrameCache; 318 } else { 319 debugIdStackFramesCache = new Map(); 320 debugIdStackParserCache.set(stackParser, debugIdStackFramesCache); 321 } 322 323 // Build a map of filename -> debug_id 324 const filenameDebugIdMap = Object.keys(debugIdMap).reduce((acc, debugIdStackTrace) => { 325 let parsedStack; 326 327 const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace); 328 if (cachedParsedStack) { 329 parsedStack = cachedParsedStack; 330 } else { 331 parsedStack = stackParser(debugIdStackTrace); 332 debugIdStackFramesCache.set(debugIdStackTrace, parsedStack); 333 } 334 335 for (let i = parsedStack.length - 1; i >= 0; i--) { 336 const stackFrame = parsedStack[i]; 337 const file = stackFrame && stackFrame.filename; 338 339 if (stackFrame && file) { 340 acc[file] = debugIdMap[debugIdStackTrace] ; 341 break; 342 } 343 } 344 return acc; 345 }, {}); 346 347 const images = []; 348 for (const path of resource_paths) { 349 if (path && filenameDebugIdMap[path]) { 350 images.push({ 351 type: 'sourcemap', 352 code_file: path, 353 debug_id: filenameDebugIdMap[path] , 354 }); 355 } 356 } 357 358 return images; 359} 360 361/** 362 * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). 363 */ 364function isValidSampleRate(rate) { 365 // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck 366 if ((typeof rate !== 'number' && typeof rate !== 'boolean') || (typeof rate === 'number' && isNaN(rate))) { 367 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && 368 logger.warn( 369 `[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( 370 rate, 371 )} of type ${JSON.stringify(typeof rate)}.`, 372 ); 373 return false; 374 } 375 376 // Boolean sample rates are always valid 377 if (rate === true || rate === false) { 378 return true; 379 } 380 381 // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false 382 if (rate < 0 || rate > 1) { 383 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && 384 logger.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`); 385 return false; 386 } 387 return true; 388} 389 390function isValidProfile(profile) { 391 if (profile.samples.length < 2) { 392 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { 393 // Log a warning if the profile has less than 2 samples so users can know why 394 // they are not seeing any profiling data and we cant avoid the back and forth 395 // of asking them to provide us with a dump of the profile data. 396 logger.log('[Profiling] Discarding profile because it contains less than 2 samples'); 397 } 398 return false; 399 } 400 401 if (!profile.frames.length) { 402 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { 403 logger.log('[Profiling] Discarding profile because it contains no frames'); 404 } 405 return false; 406 } 407 408 return true; 409} 410 411/** 412 * Creates a profiling envelope item, if the profile does not pass validation, returns null. 413 * @param event 414 * @returns {Profile | null} 415 */ 416function createProfilingEvent(profile_id, profile, event) { 417 if (!isValidProfile(profile)) { 418 return null; 419 } 420 421 return createProfilePayload(event, profile, profile_id); 422} 423 424const PROFILE_MAP = new Map(); 425/** 426 * 427 */ 428function addProfileToMap(profile_id, profile) { 429 PROFILE_MAP.set(profile_id, profile); 430 431 if (PROFILE_MAP.size > 30) { 432 const last = PROFILE_MAP.keys().next().value; 433 PROFILE_MAP.delete(last); 434 } 435} 436 437export { PROFILE_MAP, addProfileToMap, addProfilesToEnvelope, applyDebugMetadata, convertJSSelfProfileToSampledFormat, createProfilePayload, createProfilingEvent, enrichWithThreadInformation, findProfiledTransactionsFromEnvelope, isValidSampleRate }; 438//# sourceMappingURL=utils.js.map