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