diff options
Diffstat (limited to 'shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics')
| -rw-r--r-- | shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/index.js | 484 | ||||
| -rw-r--r-- | shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/utils.js | 25 |
2 files changed, 509 insertions, 0 deletions
diff --git a/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/index.js b/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/index.js new file mode 100644 index 0000000..5bca7af --- /dev/null +++ b/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/index.js @@ -0,0 +1,484 @@ +import { getActiveTransaction } from '@sentry/core'; +import { browserPerformanceTimeOrigin, logger, htmlTreeAsString } from '@sentry/utils'; +import { WINDOW } from '../types.js'; +import { onCLS } from '../web-vitals/getCLS.js'; +import { onFID } from '../web-vitals/getFID.js'; +import { onLCP } from '../web-vitals/getLCP.js'; +import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher.js'; +import { observe } from '../web-vitals/lib/observe.js'; +import { _startChild, isMeasurementValue } from './utils.js'; + +/** + * Converts from milliseconds to seconds + * @param time time in ms + */ +function msToSec(time) { + return time / 1000; +} + +function getBrowserPerformanceAPI() { + // @ts-ignore we want to make sure all of these are available, even if TS is sure they are + return WINDOW && WINDOW.addEventListener && WINDOW.performance; +} + +let _performanceCursor = 0; + +let _measurements = {}; +let _lcpEntry; +let _clsEntry; + +/** + * Start tracking web vitals + * + * @returns A function that forces web vitals collection + */ +function startTrackingWebVitals() { + const performance = getBrowserPerformanceAPI(); + if (performance && browserPerformanceTimeOrigin) { + // @ts-ignore we want to make sure all of these are available, even if TS is sure they are + if (performance.mark) { + WINDOW.performance.mark('sentry-tracing-init'); + } + _trackFID(); + const clsCallback = _trackCLS(); + const lcpCallback = _trackLCP(); + + return () => { + if (clsCallback) { + clsCallback(); + } + if (lcpCallback) { + lcpCallback(); + } + }; + } + + return () => undefined; +} + +/** + * Start tracking long tasks. + */ +function startTrackingLongTasks() { + const entryHandler = (entries) => { + for (const entry of entries) { + const transaction = getActiveTransaction() ; + if (!transaction) { + return; + } + const startTime = msToSec((browserPerformanceTimeOrigin ) + entry.startTime); + const duration = msToSec(entry.duration); + + transaction.startChild({ + description: 'Main UI thread blocked', + op: 'ui.long-task', + startTimestamp: startTime, + endTimestamp: startTime + duration, + }); + } + }; + + observe('longtask', entryHandler); +} + +/** + * Start tracking interaction events. + */ +function startTrackingInteractions() { + const entryHandler = (entries) => { + for (const entry of entries) { + const transaction = getActiveTransaction() ; + if (!transaction) { + return; + } + + if (entry.name === 'click') { + const startTime = msToSec((browserPerformanceTimeOrigin ) + entry.startTime); + const duration = msToSec(entry.duration); + + transaction.startChild({ + description: htmlTreeAsString(entry.target), + op: `ui.interaction.${entry.name}`, + startTimestamp: startTime, + endTimestamp: startTime + duration, + }); + } + } + }; + + observe('event', entryHandler, { durationThreshold: 0 }); +} + +/** Starts tracking the Cumulative Layout Shift on the current page. */ +function _trackCLS() { + // See: + // https://web.dev/evolving-cls/ + // https://web.dev/cls-web-tooling/ + return onCLS(metric => { + const entry = metric.entries.pop(); + if (!entry) { + return; + } + + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding CLS'); + _measurements['cls'] = { value: metric.value, unit: '' }; + _clsEntry = entry ; + }); +} + +/** Starts tracking the Largest Contentful Paint on the current page. */ +function _trackLCP() { + return onLCP(metric => { + const entry = metric.entries.pop(); + if (!entry) { + return; + } + + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding LCP'); + _measurements['lcp'] = { value: metric.value, unit: 'millisecond' }; + _lcpEntry = entry ; + }); +} + +/** Starts tracking the First Input Delay on the current page. */ +function _trackFID() { + onFID(metric => { + const entry = metric.entries.pop(); + if (!entry) { + return; + } + + const timeOrigin = msToSec(browserPerformanceTimeOrigin ); + const startTime = msToSec(entry.startTime); + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding FID'); + _measurements['fid'] = { value: metric.value, unit: 'millisecond' }; + _measurements['mark.fid'] = { value: timeOrigin + startTime, unit: 'second' }; + }); +} + +/** Add performance related spans to a transaction */ +function addPerformanceEntries(transaction) { + const performance = getBrowserPerformanceAPI(); + if (!performance || !WINDOW.performance.getEntries || !browserPerformanceTimeOrigin) { + // Gatekeeper if performance API not available + return; + } + + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Tracing] Adding & adjusting spans using Performance API'); + const timeOrigin = msToSec(browserPerformanceTimeOrigin); + + const performanceEntries = performance.getEntries(); + + let responseStartTimestamp; + let requestStartTimestamp; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + performanceEntries.slice(_performanceCursor).forEach((entry) => { + const startTime = msToSec(entry.startTime); + const duration = msToSec(entry.duration); + + if (transaction.op === 'navigation' && timeOrigin + startTime < transaction.startTimestamp) { + return; + } + + switch (entry.entryType) { + case 'navigation': { + _addNavigationSpans(transaction, entry, timeOrigin); + responseStartTimestamp = timeOrigin + msToSec(entry.responseStart); + requestStartTimestamp = timeOrigin + msToSec(entry.requestStart); + break; + } + case 'mark': + case 'paint': + case 'measure': { + _addMeasureSpans(transaction, entry, startTime, duration, timeOrigin); + + // capture web vitals + const firstHidden = getVisibilityWatcher(); + // Only report if the page wasn't hidden prior to the web vital. + const shouldRecord = entry.startTime < firstHidden.firstHiddenTime; + + if (entry.name === 'first-paint' && shouldRecord) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding FP'); + _measurements['fp'] = { value: entry.startTime, unit: 'millisecond' }; + } + if (entry.name === 'first-contentful-paint' && shouldRecord) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding FCP'); + _measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' }; + } + break; + } + case 'resource': { + const resourceName = (entry.name ).replace(WINDOW.location.origin, ''); + _addResourceSpans(transaction, entry, resourceName, startTime, duration, timeOrigin); + break; + } + // Ignore other entry types. + } + }); + + _performanceCursor = Math.max(performanceEntries.length - 1, 0); + + _trackNavigator(transaction); + + // Measurements are only available for pageload transactions + if (transaction.op === 'pageload') { + // Generate TTFB (Time to First Byte), which measured as the time between the beginning of the transaction and the + // start of the response in milliseconds + if (typeof responseStartTimestamp === 'number') { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding TTFB'); + _measurements['ttfb'] = { + value: (responseStartTimestamp - transaction.startTimestamp) * 1000, + unit: 'millisecond', + }; + + if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) { + // Capture the time spent making the request and receiving the first byte of the response. + // This is the time between the start of the request and the start of the response in milliseconds. + _measurements['ttfb.requestTime'] = { + value: (responseStartTimestamp - requestStartTimestamp) * 1000, + unit: 'millisecond', + }; + } + } + + ['fcp', 'fp', 'lcp'].forEach(name => { + if (!_measurements[name] || timeOrigin >= transaction.startTimestamp) { + return; + } + // The web vitals, fcp, fp, lcp, and ttfb, all measure relative to timeOrigin. + // Unfortunately, timeOrigin is not captured within the transaction span data, so these web vitals will need + // to be adjusted to be relative to transaction.startTimestamp. + const oldValue = _measurements[name].value; + const measurementTimestamp = timeOrigin + msToSec(oldValue); + + // normalizedValue should be in milliseconds + const normalizedValue = Math.abs((measurementTimestamp - transaction.startTimestamp) * 1000); + const delta = normalizedValue - oldValue; + + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && + logger.log(`[Measurements] Normalized ${name} from ${oldValue} to ${normalizedValue} (${delta})`); + _measurements[name].value = normalizedValue; + }); + + const fidMark = _measurements['mark.fid']; + if (fidMark && _measurements['fid']) { + // create span for FID + _startChild(transaction, { + description: 'first input delay', + endTimestamp: fidMark.value + msToSec(_measurements['fid'].value), + op: 'ui.action', + startTimestamp: fidMark.value, + }); + + // Delete mark.fid as we don't want it to be part of final payload + delete _measurements['mark.fid']; + } + + // If FCP is not recorded we should not record the cls value + // according to the new definition of CLS. + if (!('fcp' in _measurements)) { + delete _measurements.cls; + } + + Object.keys(_measurements).forEach(measurementName => { + transaction.setMeasurement( + measurementName, + _measurements[measurementName].value, + _measurements[measurementName].unit, + ); + }); + + _tagMetricInfo(transaction); + } + + _lcpEntry = undefined; + _clsEntry = undefined; + _measurements = {}; +} + +/** Create measure related spans */ +function _addMeasureSpans( + transaction, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + entry, + startTime, + duration, + timeOrigin, +) { + const measureStartTimestamp = timeOrigin + startTime; + const measureEndTimestamp = measureStartTimestamp + duration; + + _startChild(transaction, { + description: entry.name , + endTimestamp: measureEndTimestamp, + op: entry.entryType , + startTimestamp: measureStartTimestamp, + }); + + return measureStartTimestamp; +} + +/** Instrument navigation entries */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function _addNavigationSpans(transaction, entry, timeOrigin) { + ['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'].forEach(event => { + _addPerformanceNavigationTiming(transaction, entry, event, timeOrigin); + }); + _addPerformanceNavigationTiming(transaction, entry, 'secureConnection', timeOrigin, 'TLS/SSL', 'connectEnd'); + _addPerformanceNavigationTiming(transaction, entry, 'fetch', timeOrigin, 'cache', 'domainLookupStart'); + _addPerformanceNavigationTiming(transaction, entry, 'domainLookup', timeOrigin, 'DNS'); + _addRequest(transaction, entry, timeOrigin); +} + +/** Create performance navigation related spans */ +function _addPerformanceNavigationTiming( + transaction, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + entry, + event, + timeOrigin, + description, + eventEnd, +) { + const end = eventEnd ? (entry[eventEnd] ) : (entry[`${event}End`] ); + const start = entry[`${event}Start`] ; + if (!start || !end) { + return; + } + _startChild(transaction, { + op: 'browser', + description: description || event, + startTimestamp: timeOrigin + msToSec(start), + endTimestamp: timeOrigin + msToSec(end), + }); +} + +/** Create request and response related spans */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function _addRequest(transaction, entry, timeOrigin) { + _startChild(transaction, { + op: 'browser', + description: 'request', + startTimestamp: timeOrigin + msToSec(entry.requestStart ), + endTimestamp: timeOrigin + msToSec(entry.responseEnd ), + }); + + _startChild(transaction, { + op: 'browser', + description: 'response', + startTimestamp: timeOrigin + msToSec(entry.responseStart ), + endTimestamp: timeOrigin + msToSec(entry.responseEnd ), + }); +} + +/** Create resource-related spans */ +function _addResourceSpans( + transaction, + entry, + resourceName, + startTime, + duration, + timeOrigin, +) { + // we already instrument based on fetch and xhr, so we don't need to + // duplicate spans here. + if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = {}; + if ('transferSize' in entry) { + data['http.response_transfer_size'] = entry.transferSize; + } + if ('encodedBodySize' in entry) { + data['http.response_content_length'] = entry.encodedBodySize; + } + if ('decodedBodySize' in entry) { + data['http.decoded_response_content_length'] = entry.decodedBodySize; + } + if ('renderBlockingStatus' in entry) { + data['resource.render_blocking_status'] = entry.renderBlockingStatus; + } + + const startTimestamp = timeOrigin + startTime; + const endTimestamp = startTimestamp + duration; + + _startChild(transaction, { + description: resourceName, + endTimestamp, + op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other', + startTimestamp, + data, + }); +} + +/** + * Capture the information of the user agent. + */ +function _trackNavigator(transaction) { + const navigator = WINDOW.navigator ; + if (!navigator) { + return; + } + + // track network connectivity + const connection = navigator.connection; + if (connection) { + if (connection.effectiveType) { + transaction.setTag('effectiveConnectionType', connection.effectiveType); + } + + if (connection.type) { + transaction.setTag('connectionType', connection.type); + } + + if (isMeasurementValue(connection.rtt)) { + _measurements['connection.rtt'] = { value: connection.rtt, unit: 'millisecond' }; + } + } + + if (isMeasurementValue(navigator.deviceMemory)) { + transaction.setTag('deviceMemory', `${navigator.deviceMemory} GB`); + } + + if (isMeasurementValue(navigator.hardwareConcurrency)) { + transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency)); + } +} + +/** Add LCP / CLS data to transaction to allow debugging */ +function _tagMetricInfo(transaction) { + if (_lcpEntry) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding LCP Data'); + + // Capture Properties of the LCP element that contributes to the LCP. + + if (_lcpEntry.element) { + transaction.setTag('lcp.element', htmlTreeAsString(_lcpEntry.element)); + } + + if (_lcpEntry.id) { + transaction.setTag('lcp.id', _lcpEntry.id); + } + + if (_lcpEntry.url) { + // Trim URL to the first 200 characters. + transaction.setTag('lcp.url', _lcpEntry.url.trim().slice(0, 200)); + } + + transaction.setTag('lcp.size', _lcpEntry.size); + } + + // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift + if (_clsEntry && _clsEntry.sources) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding CLS Data'); + _clsEntry.sources.forEach((source, index) => + transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), + ); + } +} + +export { _addMeasureSpans, _addResourceSpans, addPerformanceEntries, startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals }; +//# sourceMappingURL=index.js.map diff --git a/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/utils.js b/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/utils.js new file mode 100644 index 0000000..ecb4873 --- /dev/null +++ b/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/utils.js @@ -0,0 +1,25 @@ +/** + * Checks if a given value is a valid measurement value. + */ +function isMeasurementValue(value) { + return typeof value === 'number' && isFinite(value); +} + +/** + * Helper function to start child on transactions. This function will make sure that the transaction will + * use the start timestamp of the created child span if it is earlier than the transactions actual + * start timestamp. + */ +function _startChild(transaction, { startTimestamp, ...ctx }) { + if (startTimestamp && transaction.startTimestamp > startTimestamp) { + transaction.startTimestamp = startTimestamp; + } + + return transaction.startChild({ + startTimestamp, + ...ctx, + }); +} + +export { _startChild, isMeasurementValue }; +//# sourceMappingURL=utils.js.map |
