index.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. Object.defineProperty(exports, '__esModule', { value: true });
  2. const core = require('@sentry/core');
  3. const utils = require('@sentry/utils');
  4. const debugBuild = require('../../common/debug-build.js');
  5. const instrument = require('../instrument.js');
  6. const types = require('../types.js');
  7. const getVisibilityWatcher = require('../web-vitals/lib/getVisibilityWatcher.js');
  8. const utils$1 = require('./utils.js');
  9. const MAX_INT_AS_BYTES = 2147483647;
  10. /**
  11. * Converts from milliseconds to seconds
  12. * @param time time in ms
  13. */
  14. function msToSec(time) {
  15. return time / 1000;
  16. }
  17. function getBrowserPerformanceAPI() {
  18. // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are
  19. return types.WINDOW && types.WINDOW.addEventListener && types.WINDOW.performance;
  20. }
  21. let _performanceCursor = 0;
  22. let _measurements = {};
  23. let _lcpEntry;
  24. let _clsEntry;
  25. /**
  26. * Start tracking web vitals.
  27. * The callback returned by this function can be used to stop tracking & ensure all measurements are final & captured.
  28. *
  29. * @returns A function that forces web vitals collection
  30. */
  31. function startTrackingWebVitals() {
  32. const performance = getBrowserPerformanceAPI();
  33. if (performance && utils.browserPerformanceTimeOrigin) {
  34. // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are
  35. if (performance.mark) {
  36. types.WINDOW.performance.mark('sentry-tracing-init');
  37. }
  38. const fidCallback = _trackFID();
  39. const clsCallback = _trackCLS();
  40. const lcpCallback = _trackLCP();
  41. return () => {
  42. fidCallback();
  43. clsCallback();
  44. lcpCallback();
  45. };
  46. }
  47. return () => undefined;
  48. }
  49. /**
  50. * Start tracking long tasks.
  51. */
  52. function startTrackingLongTasks() {
  53. instrument.addPerformanceInstrumentationHandler('longtask', ({ entries }) => {
  54. for (const entry of entries) {
  55. // eslint-disable-next-line deprecation/deprecation
  56. const transaction = core.getActiveTransaction() ;
  57. if (!transaction) {
  58. return;
  59. }
  60. const startTime = msToSec((utils.browserPerformanceTimeOrigin ) + entry.startTime);
  61. const duration = msToSec(entry.duration);
  62. // eslint-disable-next-line deprecation/deprecation
  63. transaction.startChild({
  64. description: 'Main UI thread blocked',
  65. op: 'ui.long-task',
  66. origin: 'auto.ui.browser.metrics',
  67. startTimestamp: startTime,
  68. endTimestamp: startTime + duration,
  69. });
  70. }
  71. });
  72. }
  73. /**
  74. * Start tracking interaction events.
  75. */
  76. function startTrackingInteractions() {
  77. instrument.addPerformanceInstrumentationHandler('event', ({ entries }) => {
  78. for (const entry of entries) {
  79. // eslint-disable-next-line deprecation/deprecation
  80. const transaction = core.getActiveTransaction() ;
  81. if (!transaction) {
  82. return;
  83. }
  84. if (entry.name === 'click') {
  85. const startTime = msToSec((utils.browserPerformanceTimeOrigin ) + entry.startTime);
  86. const duration = msToSec(entry.duration);
  87. const span = {
  88. description: utils.htmlTreeAsString(entry.target),
  89. op: `ui.interaction.${entry.name}`,
  90. origin: 'auto.ui.browser.metrics',
  91. startTimestamp: startTime,
  92. endTimestamp: startTime + duration,
  93. };
  94. const componentName = utils.getComponentName(entry.target);
  95. if (componentName) {
  96. span.attributes = { 'ui.component_name': componentName };
  97. }
  98. // eslint-disable-next-line deprecation/deprecation
  99. transaction.startChild(span);
  100. }
  101. }
  102. });
  103. }
  104. /** Starts tracking the Cumulative Layout Shift on the current page. */
  105. function _trackCLS() {
  106. return instrument.addClsInstrumentationHandler(({ metric }) => {
  107. const entry = metric.entries[metric.entries.length - 1];
  108. if (!entry) {
  109. return;
  110. }
  111. debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding CLS');
  112. _measurements['cls'] = { value: metric.value, unit: '' };
  113. _clsEntry = entry ;
  114. }, true);
  115. }
  116. /** Starts tracking the Largest Contentful Paint on the current page. */
  117. function _trackLCP() {
  118. return instrument.addLcpInstrumentationHandler(({ metric }) => {
  119. const entry = metric.entries[metric.entries.length - 1];
  120. if (!entry) {
  121. return;
  122. }
  123. debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding LCP');
  124. _measurements['lcp'] = { value: metric.value, unit: 'millisecond' };
  125. _lcpEntry = entry ;
  126. }, true);
  127. }
  128. /** Starts tracking the First Input Delay on the current page. */
  129. function _trackFID() {
  130. return instrument.addFidInstrumentationHandler(({ metric }) => {
  131. const entry = metric.entries[metric.entries.length - 1];
  132. if (!entry) {
  133. return;
  134. }
  135. const timeOrigin = msToSec(utils.browserPerformanceTimeOrigin );
  136. const startTime = msToSec(entry.startTime);
  137. debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding FID');
  138. _measurements['fid'] = { value: metric.value, unit: 'millisecond' };
  139. _measurements['mark.fid'] = { value: timeOrigin + startTime, unit: 'second' };
  140. });
  141. }
  142. /** Add performance related spans to a transaction */
  143. function addPerformanceEntries(transaction) {
  144. const performance = getBrowserPerformanceAPI();
  145. if (!performance || !types.WINDOW.performance.getEntries || !utils.browserPerformanceTimeOrigin) {
  146. // Gatekeeper if performance API not available
  147. return;
  148. }
  149. debugBuild.DEBUG_BUILD && utils.logger.log('[Tracing] Adding & adjusting spans using Performance API');
  150. const timeOrigin = msToSec(utils.browserPerformanceTimeOrigin);
  151. const performanceEntries = performance.getEntries();
  152. let responseStartTimestamp;
  153. let requestStartTimestamp;
  154. const { op, start_timestamp: transactionStartTime } = core.spanToJSON(transaction);
  155. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  156. performanceEntries.slice(_performanceCursor).forEach((entry) => {
  157. const startTime = msToSec(entry.startTime);
  158. const duration = msToSec(entry.duration);
  159. // eslint-disable-next-line deprecation/deprecation
  160. if (transaction.op === 'navigation' && transactionStartTime && timeOrigin + startTime < transactionStartTime) {
  161. return;
  162. }
  163. switch (entry.entryType) {
  164. case 'navigation': {
  165. _addNavigationSpans(transaction, entry, timeOrigin);
  166. responseStartTimestamp = timeOrigin + msToSec(entry.responseStart);
  167. requestStartTimestamp = timeOrigin + msToSec(entry.requestStart);
  168. break;
  169. }
  170. case 'mark':
  171. case 'paint':
  172. case 'measure': {
  173. _addMeasureSpans(transaction, entry, startTime, duration, timeOrigin);
  174. // capture web vitals
  175. const firstHidden = getVisibilityWatcher.getVisibilityWatcher();
  176. // Only report if the page wasn't hidden prior to the web vital.
  177. const shouldRecord = entry.startTime < firstHidden.firstHiddenTime;
  178. if (entry.name === 'first-paint' && shouldRecord) {
  179. debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding FP');
  180. _measurements['fp'] = { value: entry.startTime, unit: 'millisecond' };
  181. }
  182. if (entry.name === 'first-contentful-paint' && shouldRecord) {
  183. debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding FCP');
  184. _measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' };
  185. }
  186. break;
  187. }
  188. case 'resource': {
  189. _addResourceSpans(transaction, entry, entry.name , startTime, duration, timeOrigin);
  190. break;
  191. }
  192. // Ignore other entry types.
  193. }
  194. });
  195. _performanceCursor = Math.max(performanceEntries.length - 1, 0);
  196. _trackNavigator(transaction);
  197. // Measurements are only available for pageload transactions
  198. if (op === 'pageload') {
  199. _addTtfbToMeasurements(_measurements, responseStartTimestamp, requestStartTimestamp, transactionStartTime);
  200. ['fcp', 'fp', 'lcp'].forEach(name => {
  201. if (!_measurements[name] || !transactionStartTime || timeOrigin >= transactionStartTime) {
  202. return;
  203. }
  204. // The web vitals, fcp, fp, lcp, and ttfb, all measure relative to timeOrigin.
  205. // Unfortunately, timeOrigin is not captured within the transaction span data, so these web vitals will need
  206. // to be adjusted to be relative to transaction.startTimestamp.
  207. const oldValue = _measurements[name].value;
  208. const measurementTimestamp = timeOrigin + msToSec(oldValue);
  209. // normalizedValue should be in milliseconds
  210. const normalizedValue = Math.abs((measurementTimestamp - transactionStartTime) * 1000);
  211. const delta = normalizedValue - oldValue;
  212. debugBuild.DEBUG_BUILD && utils.logger.log(`[Measurements] Normalized ${name} from ${oldValue} to ${normalizedValue} (${delta})`);
  213. _measurements[name].value = normalizedValue;
  214. });
  215. const fidMark = _measurements['mark.fid'];
  216. if (fidMark && _measurements['fid']) {
  217. // create span for FID
  218. utils$1._startChild(transaction, {
  219. description: 'first input delay',
  220. endTimestamp: fidMark.value + msToSec(_measurements['fid'].value),
  221. op: 'ui.action',
  222. origin: 'auto.ui.browser.metrics',
  223. startTimestamp: fidMark.value,
  224. });
  225. // Delete mark.fid as we don't want it to be part of final payload
  226. delete _measurements['mark.fid'];
  227. }
  228. // If FCP is not recorded we should not record the cls value
  229. // according to the new definition of CLS.
  230. if (!('fcp' in _measurements)) {
  231. delete _measurements.cls;
  232. }
  233. Object.keys(_measurements).forEach(measurementName => {
  234. core.setMeasurement(measurementName, _measurements[measurementName].value, _measurements[measurementName].unit);
  235. });
  236. _tagMetricInfo(transaction);
  237. }
  238. _lcpEntry = undefined;
  239. _clsEntry = undefined;
  240. _measurements = {};
  241. }
  242. /** Create measure related spans */
  243. function _addMeasureSpans(
  244. transaction,
  245. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  246. entry,
  247. startTime,
  248. duration,
  249. timeOrigin,
  250. ) {
  251. const measureStartTimestamp = timeOrigin + startTime;
  252. const measureEndTimestamp = measureStartTimestamp + duration;
  253. utils$1._startChild(transaction, {
  254. description: entry.name ,
  255. endTimestamp: measureEndTimestamp,
  256. op: entry.entryType ,
  257. origin: 'auto.resource.browser.metrics',
  258. startTimestamp: measureStartTimestamp,
  259. });
  260. return measureStartTimestamp;
  261. }
  262. /** Instrument navigation entries */
  263. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  264. function _addNavigationSpans(transaction, entry, timeOrigin) {
  265. ['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'].forEach(event => {
  266. _addPerformanceNavigationTiming(transaction, entry, event, timeOrigin);
  267. });
  268. _addPerformanceNavigationTiming(transaction, entry, 'secureConnection', timeOrigin, 'TLS/SSL', 'connectEnd');
  269. _addPerformanceNavigationTiming(transaction, entry, 'fetch', timeOrigin, 'cache', 'domainLookupStart');
  270. _addPerformanceNavigationTiming(transaction, entry, 'domainLookup', timeOrigin, 'DNS');
  271. _addRequest(transaction, entry, timeOrigin);
  272. }
  273. /** Create performance navigation related spans */
  274. function _addPerformanceNavigationTiming(
  275. transaction,
  276. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  277. entry,
  278. event,
  279. timeOrigin,
  280. description,
  281. eventEnd,
  282. ) {
  283. const end = eventEnd ? (entry[eventEnd] ) : (entry[`${event}End`] );
  284. const start = entry[`${event}Start`] ;
  285. if (!start || !end) {
  286. return;
  287. }
  288. utils$1._startChild(transaction, {
  289. op: 'browser',
  290. origin: 'auto.browser.browser.metrics',
  291. description: description || event,
  292. startTimestamp: timeOrigin + msToSec(start),
  293. endTimestamp: timeOrigin + msToSec(end),
  294. });
  295. }
  296. /** Create request and response related spans */
  297. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  298. function _addRequest(transaction, entry, timeOrigin) {
  299. if (entry.responseEnd) {
  300. // It is possible that we are collecting these metrics when the page hasn't finished loading yet, for example when the HTML slowly streams in.
  301. // In this case, ie. when the document request hasn't finished yet, `entry.responseEnd` will be 0.
  302. // In order not to produce faulty spans, where the end timestamp is before the start timestamp, we will only collect
  303. // these spans when the responseEnd value is available. The backend (Relay) would drop the entire transaction if it contained faulty spans.
  304. utils$1._startChild(transaction, {
  305. op: 'browser',
  306. origin: 'auto.browser.browser.metrics',
  307. description: 'request',
  308. startTimestamp: timeOrigin + msToSec(entry.requestStart ),
  309. endTimestamp: timeOrigin + msToSec(entry.responseEnd ),
  310. });
  311. utils$1._startChild(transaction, {
  312. op: 'browser',
  313. origin: 'auto.browser.browser.metrics',
  314. description: 'response',
  315. startTimestamp: timeOrigin + msToSec(entry.responseStart ),
  316. endTimestamp: timeOrigin + msToSec(entry.responseEnd ),
  317. });
  318. }
  319. }
  320. /** Create resource-related spans */
  321. function _addResourceSpans(
  322. transaction,
  323. entry,
  324. resourceUrl,
  325. startTime,
  326. duration,
  327. timeOrigin,
  328. ) {
  329. // we already instrument based on fetch and xhr, so we don't need to
  330. // duplicate spans here.
  331. if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
  332. return;
  333. }
  334. const parsedUrl = utils.parseUrl(resourceUrl);
  335. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  336. const data = {};
  337. setResourceEntrySizeData(data, entry, 'transferSize', 'http.response_transfer_size');
  338. setResourceEntrySizeData(data, entry, 'encodedBodySize', 'http.response_content_length');
  339. setResourceEntrySizeData(data, entry, 'decodedBodySize', 'http.decoded_response_content_length');
  340. if ('renderBlockingStatus' in entry) {
  341. data['resource.render_blocking_status'] = entry.renderBlockingStatus;
  342. }
  343. if (parsedUrl.protocol) {
  344. data['url.scheme'] = parsedUrl.protocol.split(':').pop(); // the protocol returned by parseUrl includes a :, but OTEL spec does not, so we remove it.
  345. }
  346. if (parsedUrl.host) {
  347. data['server.address'] = parsedUrl.host;
  348. }
  349. data['url.same_origin'] = resourceUrl.includes(types.WINDOW.location.origin);
  350. const startTimestamp = timeOrigin + startTime;
  351. const endTimestamp = startTimestamp + duration;
  352. utils$1._startChild(transaction, {
  353. description: resourceUrl.replace(types.WINDOW.location.origin, ''),
  354. endTimestamp,
  355. op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other',
  356. origin: 'auto.resource.browser.metrics',
  357. startTimestamp,
  358. data,
  359. });
  360. }
  361. /**
  362. * Capture the information of the user agent.
  363. */
  364. function _trackNavigator(transaction) {
  365. const navigator = types.WINDOW.navigator ;
  366. if (!navigator) {
  367. return;
  368. }
  369. // track network connectivity
  370. const connection = navigator.connection;
  371. if (connection) {
  372. if (connection.effectiveType) {
  373. // TODO: Can we rewrite this to an attribute?
  374. // eslint-disable-next-line deprecation/deprecation
  375. transaction.setTag('effectiveConnectionType', connection.effectiveType);
  376. }
  377. if (connection.type) {
  378. // TODO: Can we rewrite this to an attribute?
  379. // eslint-disable-next-line deprecation/deprecation
  380. transaction.setTag('connectionType', connection.type);
  381. }
  382. if (utils$1.isMeasurementValue(connection.rtt)) {
  383. _measurements['connection.rtt'] = { value: connection.rtt, unit: 'millisecond' };
  384. }
  385. }
  386. if (utils$1.isMeasurementValue(navigator.deviceMemory)) {
  387. // TODO: Can we rewrite this to an attribute?
  388. // eslint-disable-next-line deprecation/deprecation
  389. transaction.setTag('deviceMemory', `${navigator.deviceMemory} GB`);
  390. }
  391. if (utils$1.isMeasurementValue(navigator.hardwareConcurrency)) {
  392. // TODO: Can we rewrite this to an attribute?
  393. // eslint-disable-next-line deprecation/deprecation
  394. transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency));
  395. }
  396. }
  397. /** Add LCP / CLS data to transaction to allow debugging */
  398. function _tagMetricInfo(transaction) {
  399. if (_lcpEntry) {
  400. debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding LCP Data');
  401. // Capture Properties of the LCP element that contributes to the LCP.
  402. if (_lcpEntry.element) {
  403. // TODO: Can we rewrite this to an attribute?
  404. // eslint-disable-next-line deprecation/deprecation
  405. transaction.setTag('lcp.element', utils.htmlTreeAsString(_lcpEntry.element));
  406. }
  407. if (_lcpEntry.id) {
  408. // TODO: Can we rewrite this to an attribute?
  409. // eslint-disable-next-line deprecation/deprecation
  410. transaction.setTag('lcp.id', _lcpEntry.id);
  411. }
  412. if (_lcpEntry.url) {
  413. // Trim URL to the first 200 characters.
  414. // TODO: Can we rewrite this to an attribute?
  415. // eslint-disable-next-line deprecation/deprecation
  416. transaction.setTag('lcp.url', _lcpEntry.url.trim().slice(0, 200));
  417. }
  418. // TODO: Can we rewrite this to an attribute?
  419. // eslint-disable-next-line deprecation/deprecation
  420. transaction.setTag('lcp.size', _lcpEntry.size);
  421. }
  422. // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift
  423. if (_clsEntry && _clsEntry.sources) {
  424. debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding CLS Data');
  425. _clsEntry.sources.forEach((source, index) =>
  426. // TODO: Can we rewrite this to an attribute?
  427. // eslint-disable-next-line deprecation/deprecation
  428. transaction.setTag(`cls.source.${index + 1}`, utils.htmlTreeAsString(source.node)),
  429. );
  430. }
  431. }
  432. function setResourceEntrySizeData(
  433. data,
  434. entry,
  435. key,
  436. dataKey,
  437. ) {
  438. const entryVal = entry[key];
  439. if (entryVal != null && entryVal < MAX_INT_AS_BYTES) {
  440. data[dataKey] = entryVal;
  441. }
  442. }
  443. /**
  444. * Add ttfb information to measurements
  445. *
  446. * Exported for tests
  447. */
  448. function _addTtfbToMeasurements(
  449. _measurements,
  450. responseStartTimestamp,
  451. requestStartTimestamp,
  452. transactionStartTime,
  453. ) {
  454. // Generate TTFB (Time to First Byte), which measured as the time between the beginning of the transaction and the
  455. // start of the response in milliseconds
  456. if (typeof responseStartTimestamp === 'number' && transactionStartTime) {
  457. debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding TTFB');
  458. _measurements['ttfb'] = {
  459. // As per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStart,
  460. // responseStart can be 0 if the request is coming straight from the cache.
  461. // This might lead us to calculate a negative ttfb if we don't use Math.max here.
  462. //
  463. // This logic is the same as what is in the web-vitals library to calculate ttfb
  464. // https://github.com/GoogleChrome/web-vitals/blob/2301de5015e82b09925238a228a0893635854587/src/onTTFB.ts#L92
  465. // TODO(abhi): We should use the web-vitals library instead of this custom calculation.
  466. value: Math.max(responseStartTimestamp - transactionStartTime, 0) * 1000,
  467. unit: 'millisecond',
  468. };
  469. if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) {
  470. // Capture the time spent making the request and receiving the first byte of the response.
  471. // This is the time between the start of the request and the start of the response in milliseconds.
  472. _measurements['ttfb.requestTime'] = {
  473. value: (responseStartTimestamp - requestStartTimestamp) * 1000,
  474. unit: 'millisecond',
  475. };
  476. }
  477. }
  478. }
  479. exports._addMeasureSpans = _addMeasureSpans;
  480. exports._addResourceSpans = _addResourceSpans;
  481. exports._addTtfbToMeasurements = _addTtfbToMeasurements;
  482. exports.addPerformanceEntries = addPerformanceEntries;
  483. exports.startTrackingInteractions = startTrackingInteractions;
  484. exports.startTrackingLongTasks = startTrackingLongTasks;
  485. exports.startTrackingWebVitals = startTrackingWebVitals;
  486. //# sourceMappingURL=index.js.map