utils.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. Object.defineProperty(exports, '__esModule', { value: true });
  2. const core = require('@sentry/core');
  3. const utils = require('@sentry/utils');
  4. const debugBuild = require('../debug-build.js');
  5. const helpers = require('../helpers.js');
  6. /* eslint-disable max-lines */
  7. const 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
  10. const THREAD_ID_STRING = String(0);
  11. const THREAD_NAME = 'main';
  12. // Machine properties (eval only once)
  13. let OS_PLATFORM = '';
  14. let OS_PLATFORM_VERSION = '';
  15. let OS_ARCH = '';
  16. let OS_BROWSER = (helpers.WINDOW.navigator && helpers.WINDOW.navigator.userAgent) || '';
  17. let OS_MODEL = '';
  18. const OS_LOCALE =
  19. (helpers.WINDOW.navigator && helpers.WINDOW.navigator.language) ||
  20. (helpers.WINDOW.navigator && helpers.WINDOW.navigator.languages && helpers.WINDOW.navigator.languages[0]) ||
  21. '';
  22. function isUserAgentData(data) {
  23. return typeof data === 'object' && data !== null && 'getHighEntropyValues' in data;
  24. }
  25. // @ts-expect-error userAgentData is not part of the navigator interface yet
  26. const userAgentData = helpers.WINDOW.navigator && helpers.WINDOW.navigator.userAgentData;
  27. if (isUserAgentData(userAgentData)) {
  28. userAgentData
  29. .getHighEntropyValues(['architecture', 'model', 'platform', 'platformVersion', 'fullVersionList'])
  30. .then((ua) => {
  31. OS_PLATFORM = ua.platform || '';
  32. OS_ARCH = ua.architecture || '';
  33. OS_MODEL = ua.model || '';
  34. OS_PLATFORM_VERSION = ua.platformVersion || '';
  35. if (ua.fullVersionList && ua.fullVersionList.length > 0) {
  36. const firstUa = ua.fullVersionList[ua.fullVersionList.length - 1];
  37. OS_BROWSER = `${firstUa.brand} ${firstUa.version}`;
  38. }
  39. })
  40. .catch(e => void e);
  41. }
  42. function isProcessedJSSelfProfile(profile) {
  43. return !('thread_metadata' in profile);
  44. }
  45. // Enriches the profile with threadId of the current thread.
  46. // This is done in node as we seem to not be able to get the info from C native code.
  47. /**
  48. *
  49. */
  50. function enrichWithThreadInformation(profile) {
  51. if (!isProcessedJSSelfProfile(profile)) {
  52. return profile;
  53. }
  54. return convertJSSelfProfileToSampledFormat(profile);
  55. }
  56. // Profile is marked as optional because it is deleted from the metadata
  57. // by the integration before the event is processed by other integrations.
  58. function getTraceId(event) {
  59. const traceId = event && event.contexts && event.contexts['trace'] && event.contexts['trace']['trace_id'];
  60. // Log a warning if the profile has an invalid traceId (should be uuidv4).
  61. // All profiles and transactions are rejected if this is the case and we want to
  62. // warn users that this is happening if they enable debug flag
  63. if (typeof traceId === 'string' && traceId.length !== 32) {
  64. if (debugBuild.DEBUG_BUILD) {
  65. utils.logger.log(`[Profiling] Invalid traceId: ${traceId} on profiled event`);
  66. }
  67. }
  68. if (typeof traceId !== 'string') {
  69. return '';
  70. }
  71. return traceId;
  72. }
  73. /**
  74. * Creates a profiling event envelope from a Sentry event. If profile does not pass
  75. * validation, returns null.
  76. * @param event
  77. * @param dsn
  78. * @param metadata
  79. * @param tunnel
  80. * @returns {EventEnvelope | null}
  81. */
  82. /**
  83. * Creates a profiling event envelope from a Sentry event.
  84. */
  85. function createProfilePayload(
  86. profile_id,
  87. start_timestamp,
  88. processed_profile,
  89. event,
  90. ) {
  91. if (event.type !== 'transaction') {
  92. // createProfilingEventEnvelope should only be called for transactions,
  93. // we type guard this behavior with isProfiledTransactionEvent.
  94. throw new TypeError('Profiling events may only be attached to transactions, this should never occur.');
  95. }
  96. if (processed_profile === undefined || processed_profile === null) {
  97. throw new TypeError(
  98. `Cannot construct profiling event envelope without a valid profile. Got ${processed_profile} instead.`,
  99. );
  100. }
  101. const traceId = getTraceId(event);
  102. const enrichedThreadProfile = enrichWithThreadInformation(processed_profile);
  103. const transactionStartMs = start_timestamp
  104. ? start_timestamp
  105. : typeof event.start_timestamp === 'number'
  106. ? event.start_timestamp * 1000
  107. : Date.now();
  108. const transactionEndMs = typeof event.timestamp === 'number' ? event.timestamp * 1000 : Date.now();
  109. const profile = {
  110. event_id: profile_id,
  111. timestamp: new Date(transactionStartMs).toISOString(),
  112. platform: 'javascript',
  113. version: '1',
  114. release: event.release || '',
  115. environment: event.environment || core.DEFAULT_ENVIRONMENT,
  116. runtime: {
  117. name: 'javascript',
  118. version: helpers.WINDOW.navigator.userAgent,
  119. },
  120. os: {
  121. name: OS_PLATFORM,
  122. version: OS_PLATFORM_VERSION,
  123. build_number: OS_BROWSER,
  124. },
  125. device: {
  126. locale: OS_LOCALE,
  127. model: OS_MODEL,
  128. manufacturer: OS_BROWSER,
  129. architecture: OS_ARCH,
  130. is_emulator: false,
  131. },
  132. debug_meta: {
  133. images: applyDebugMetadata(processed_profile.resources),
  134. },
  135. profile: enrichedThreadProfile,
  136. transactions: [
  137. {
  138. name: event.transaction || '',
  139. id: event.event_id || utils.uuid4(),
  140. trace_id: traceId,
  141. active_thread_id: THREAD_ID_STRING,
  142. relative_start_ns: '0',
  143. relative_end_ns: ((transactionEndMs - transactionStartMs) * 1e6).toFixed(0),
  144. },
  145. ],
  146. };
  147. return profile;
  148. }
  149. /*
  150. See packages/tracing-internal/src/browser/router.ts
  151. */
  152. /**
  153. *
  154. */
  155. function isAutomatedPageLoadTransaction(transaction) {
  156. return transaction.op === 'pageload';
  157. }
  158. /**
  159. * Converts a JSSelfProfile to a our sampled format.
  160. * Does not currently perform stack indexing.
  161. */
  162. function convertJSSelfProfileToSampledFormat(input) {
  163. let EMPTY_STACK_ID = undefined;
  164. let STACK_ID = 0;
  165. // Initialize the profile that we will fill with data
  166. const profile = {
  167. samples: [],
  168. stacks: [],
  169. frames: [],
  170. thread_metadata: {
  171. [THREAD_ID_STRING]: { name: THREAD_NAME },
  172. },
  173. };
  174. if (!input.samples.length) {
  175. return profile;
  176. }
  177. // We assert samples.length > 0 above and timestamp should always be present
  178. const start = input.samples[0].timestamp;
  179. // The JS SDK might change it's time origin based on some heuristic (see See packages/utils/src/time.ts)
  180. // when that happens, we need to ensure we are correcting the profile timings so the two timelines stay in sync.
  181. // Since JS self profiling time origin is always initialized to performance.timeOrigin, we need to adjust for
  182. // the drift between the SDK selected value and our profile time origin.
  183. const origin =
  184. typeof performance.timeOrigin === 'number' ? performance.timeOrigin : utils.browserPerformanceTimeOrigin || 0;
  185. const adjustForOriginChange = origin - (utils.browserPerformanceTimeOrigin || origin);
  186. for (let i = 0; i < input.samples.length; i++) {
  187. const jsSample = input.samples[i];
  188. // If sample has no stack, add an empty sample
  189. if (jsSample.stackId === undefined) {
  190. if (EMPTY_STACK_ID === undefined) {
  191. EMPTY_STACK_ID = STACK_ID;
  192. profile.stacks[EMPTY_STACK_ID] = [];
  193. STACK_ID++;
  194. }
  195. profile['samples'][i] = {
  196. // convert ms timestamp to ns
  197. elapsed_since_start_ns: ((jsSample.timestamp + adjustForOriginChange - start) * MS_TO_NS).toFixed(0),
  198. stack_id: EMPTY_STACK_ID,
  199. thread_id: THREAD_ID_STRING,
  200. };
  201. continue;
  202. }
  203. let stackTop = input.stacks[jsSample.stackId];
  204. // Functions in top->down order (root is last)
  205. // We follow the stackTop.parentId trail and collect each visited frameId
  206. const stack = [];
  207. while (stackTop) {
  208. stack.push(stackTop.frameId);
  209. const frame = input.frames[stackTop.frameId];
  210. // If our frame has not been indexed yet, index it
  211. if (profile.frames[stackTop.frameId] === undefined) {
  212. profile.frames[stackTop.frameId] = {
  213. function: frame.name,
  214. abs_path: typeof frame.resourceId === 'number' ? input.resources[frame.resourceId] : undefined,
  215. lineno: frame.line,
  216. colno: frame.column,
  217. };
  218. }
  219. stackTop = stackTop.parentId === undefined ? undefined : input.stacks[stackTop.parentId];
  220. }
  221. const sample = {
  222. // convert ms timestamp to ns
  223. elapsed_since_start_ns: ((jsSample.timestamp + adjustForOriginChange - start) * MS_TO_NS).toFixed(0),
  224. stack_id: STACK_ID,
  225. thread_id: THREAD_ID_STRING,
  226. };
  227. profile['stacks'][STACK_ID] = stack;
  228. profile['samples'][i] = sample;
  229. STACK_ID++;
  230. }
  231. return profile;
  232. }
  233. /**
  234. * Adds items to envelope if they are not already present - mutates the envelope.
  235. * @param envelope
  236. */
  237. function addProfilesToEnvelope(envelope, profiles) {
  238. if (!profiles.length) {
  239. return envelope;
  240. }
  241. for (const profile of profiles) {
  242. envelope[1].push([{ type: 'profile' }, profile]);
  243. }
  244. return envelope;
  245. }
  246. /**
  247. * Finds transactions with profile_id context in the envelope
  248. * @param envelope
  249. * @returns
  250. */
  251. function findProfiledTransactionsFromEnvelope(envelope) {
  252. const events = [];
  253. utils.forEachEnvelopeItem(envelope, (item, type) => {
  254. if (type !== 'transaction') {
  255. return;
  256. }
  257. for (let j = 1; j < item.length; j++) {
  258. const event = item[j] ;
  259. if (event && event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id']) {
  260. events.push(item[j] );
  261. }
  262. }
  263. });
  264. return events;
  265. }
  266. const debugIdStackParserCache = new WeakMap();
  267. /**
  268. * Applies debug meta data to an event from a list of paths to resources (sourcemaps)
  269. */
  270. function applyDebugMetadata(resource_paths) {
  271. const debugIdMap = utils.GLOBAL_OBJ._sentryDebugIds;
  272. if (!debugIdMap) {
  273. return [];
  274. }
  275. const client = core.getClient();
  276. const options = client && client.getOptions();
  277. const stackParser = options && options.stackParser;
  278. if (!stackParser) {
  279. return [];
  280. }
  281. let debugIdStackFramesCache;
  282. const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(stackParser);
  283. if (cachedDebugIdStackFrameCache) {
  284. debugIdStackFramesCache = cachedDebugIdStackFrameCache;
  285. } else {
  286. debugIdStackFramesCache = new Map();
  287. debugIdStackParserCache.set(stackParser, debugIdStackFramesCache);
  288. }
  289. // Build a map of filename -> debug_id
  290. const filenameDebugIdMap = Object.keys(debugIdMap).reduce((acc, debugIdStackTrace) => {
  291. let parsedStack;
  292. const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace);
  293. if (cachedParsedStack) {
  294. parsedStack = cachedParsedStack;
  295. } else {
  296. parsedStack = stackParser(debugIdStackTrace);
  297. debugIdStackFramesCache.set(debugIdStackTrace, parsedStack);
  298. }
  299. for (let i = parsedStack.length - 1; i >= 0; i--) {
  300. const stackFrame = parsedStack[i];
  301. const file = stackFrame && stackFrame.filename;
  302. if (stackFrame && file) {
  303. acc[file] = debugIdMap[debugIdStackTrace] ;
  304. break;
  305. }
  306. }
  307. return acc;
  308. }, {});
  309. const images = [];
  310. for (const path of resource_paths) {
  311. if (path && filenameDebugIdMap[path]) {
  312. images.push({
  313. type: 'sourcemap',
  314. code_file: path,
  315. debug_id: filenameDebugIdMap[path] ,
  316. });
  317. }
  318. }
  319. return images;
  320. }
  321. /**
  322. * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
  323. */
  324. function isValidSampleRate(rate) {
  325. // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck
  326. if ((typeof rate !== 'number' && typeof rate !== 'boolean') || (typeof rate === 'number' && isNaN(rate))) {
  327. debugBuild.DEBUG_BUILD &&
  328. utils.logger.warn(
  329. `[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify(
  330. rate,
  331. )} of type ${JSON.stringify(typeof rate)}.`,
  332. );
  333. return false;
  334. }
  335. // Boolean sample rates are always valid
  336. if (rate === true || rate === false) {
  337. return true;
  338. }
  339. // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false
  340. if (rate < 0 || rate > 1) {
  341. debugBuild.DEBUG_BUILD && utils.logger.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`);
  342. return false;
  343. }
  344. return true;
  345. }
  346. function isValidProfile(profile) {
  347. if (profile.samples.length < 2) {
  348. if (debugBuild.DEBUG_BUILD) {
  349. // Log a warning if the profile has less than 2 samples so users can know why
  350. // they are not seeing any profiling data and we cant avoid the back and forth
  351. // of asking them to provide us with a dump of the profile data.
  352. utils.logger.log('[Profiling] Discarding profile because it contains less than 2 samples');
  353. }
  354. return false;
  355. }
  356. if (!profile.frames.length) {
  357. if (debugBuild.DEBUG_BUILD) {
  358. utils.logger.log('[Profiling] Discarding profile because it contains no frames');
  359. }
  360. return false;
  361. }
  362. return true;
  363. }
  364. // Keep a flag value to avoid re-initializing the profiler constructor. If it fails
  365. // once, it will always fail and this allows us to early return.
  366. let PROFILING_CONSTRUCTOR_FAILED = false;
  367. const MAX_PROFILE_DURATION_MS = 30000;
  368. /**
  369. * Check if profiler constructor is available.
  370. * @param maybeProfiler
  371. */
  372. function isJSProfilerSupported(maybeProfiler) {
  373. return typeof maybeProfiler === 'function';
  374. }
  375. /**
  376. * Starts the profiler and returns the profiler instance.
  377. */
  378. function startJSSelfProfile() {
  379. // Feature support check first
  380. const JSProfilerConstructor = helpers.WINDOW.Profiler;
  381. if (!isJSProfilerSupported(JSProfilerConstructor)) {
  382. if (debugBuild.DEBUG_BUILD) {
  383. utils.logger.log(
  384. '[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.',
  385. );
  386. }
  387. return;
  388. }
  389. // From initial testing, it seems that the minimum value for sampleInterval is 10ms.
  390. const samplingIntervalMS = 10;
  391. // Start the profiler
  392. const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingIntervalMS);
  393. // Attempt to initialize the profiler constructor, if it fails, we disable profiling for the current user session.
  394. // This is likely due to a missing 'Document-Policy': 'js-profiling' header. We do not want to throw an error if this happens
  395. // as we risk breaking the user's application, so just disable profiling and log an error.
  396. try {
  397. return new JSProfilerConstructor({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples });
  398. } catch (e) {
  399. if (debugBuild.DEBUG_BUILD) {
  400. utils.logger.log(
  401. "[Profiling] Failed to initialize the Profiling constructor, this is likely due to a missing 'Document-Policy': 'js-profiling' header.",
  402. );
  403. utils.logger.log('[Profiling] Disabling profiling for current user session.');
  404. }
  405. PROFILING_CONSTRUCTOR_FAILED = true;
  406. }
  407. return;
  408. }
  409. /**
  410. * Determine if a profile should be profiled.
  411. */
  412. function shouldProfileTransaction(transaction) {
  413. // If constructor failed once, it will always fail, so we can early return.
  414. if (PROFILING_CONSTRUCTOR_FAILED) {
  415. if (debugBuild.DEBUG_BUILD) {
  416. utils.logger.log('[Profiling] Profiling has been disabled for the duration of the current user session.');
  417. }
  418. return false;
  419. }
  420. if (!transaction.isRecording()) {
  421. if (debugBuild.DEBUG_BUILD) {
  422. utils.logger.log('[Profiling] Discarding profile because transaction was not sampled.');
  423. }
  424. return false;
  425. }
  426. const client = core.getClient();
  427. const options = client && client.getOptions();
  428. if (!options) {
  429. debugBuild.DEBUG_BUILD && utils.logger.log('[Profiling] Profiling disabled, no options found.');
  430. return false;
  431. }
  432. // @ts-expect-error profilesSampleRate is not part of the browser options yet
  433. const profilesSampleRate = options.profilesSampleRate;
  434. // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
  435. // only valid values are booleans or numbers between 0 and 1.)
  436. if (!isValidSampleRate(profilesSampleRate)) {
  437. debugBuild.DEBUG_BUILD && utils.logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
  438. return false;
  439. }
  440. // if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
  441. if (!profilesSampleRate) {
  442. debugBuild.DEBUG_BUILD &&
  443. utils.logger.log(
  444. '[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
  445. );
  446. return false;
  447. }
  448. // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
  449. // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
  450. const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
  451. // Check if we should sample this profile
  452. if (!sampled) {
  453. debugBuild.DEBUG_BUILD &&
  454. utils.logger.log(
  455. `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
  456. profilesSampleRate,
  457. )})`,
  458. );
  459. return false;
  460. }
  461. return true;
  462. }
  463. /**
  464. * Creates a profiling envelope item, if the profile does not pass validation, returns null.
  465. * @param event
  466. * @returns {Profile | null}
  467. */
  468. function createProfilingEvent(
  469. profile_id,
  470. start_timestamp,
  471. profile,
  472. event,
  473. ) {
  474. if (!isValidProfile(profile)) {
  475. return null;
  476. }
  477. return createProfilePayload(profile_id, start_timestamp, profile, event);
  478. }
  479. const PROFILE_MAP = new Map();
  480. /**
  481. *
  482. */
  483. function getActiveProfilesCount() {
  484. return PROFILE_MAP.size;
  485. }
  486. /**
  487. * Retrieves profile from global cache and removes it.
  488. */
  489. function takeProfileFromGlobalCache(profile_id) {
  490. const profile = PROFILE_MAP.get(profile_id);
  491. if (profile) {
  492. PROFILE_MAP.delete(profile_id);
  493. }
  494. return profile;
  495. }
  496. /**
  497. * Adds profile to global cache and evicts the oldest profile if the cache is full.
  498. */
  499. function addProfileToGlobalCache(profile_id, profile) {
  500. PROFILE_MAP.set(profile_id, profile);
  501. if (PROFILE_MAP.size > 30) {
  502. const last = PROFILE_MAP.keys().next().value;
  503. PROFILE_MAP.delete(last);
  504. }
  505. }
  506. exports.MAX_PROFILE_DURATION_MS = MAX_PROFILE_DURATION_MS;
  507. exports.addProfileToGlobalCache = addProfileToGlobalCache;
  508. exports.addProfilesToEnvelope = addProfilesToEnvelope;
  509. exports.applyDebugMetadata = applyDebugMetadata;
  510. exports.convertJSSelfProfileToSampledFormat = convertJSSelfProfileToSampledFormat;
  511. exports.createProfilePayload = createProfilePayload;
  512. exports.createProfilingEvent = createProfilingEvent;
  513. exports.enrichWithThreadInformation = enrichWithThreadInformation;
  514. exports.findProfiledTransactionsFromEnvelope = findProfiledTransactionsFromEnvelope;
  515. exports.getActiveProfilesCount = getActiveProfilesCount;
  516. exports.isAutomatedPageLoadTransaction = isAutomatedPageLoadTransaction;
  517. exports.isValidSampleRate = isValidSampleRate;
  518. exports.shouldProfileTransaction = shouldProfileTransaction;
  519. exports.startJSSelfProfile = startJSSelfProfile;
  520. exports.takeProfileFromGlobalCache = takeProfileFromGlobalCache;
  521. //# sourceMappingURL=utils.js.map