summaryrefslogtreecommitdiff
path: root/shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/index.js
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/index.js
init commit
Diffstat (limited to 'shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/index.js')
-rw-r--r--shared/logger/node_modules/@sentry-internal/tracing/esm/browser/metrics/index.js484
1 files changed, 484 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