123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554 |
- import { getActiveTransaction, spanToJSON, setMeasurement } from '@sentry/core';
- import { browserPerformanceTimeOrigin, htmlTreeAsString, getComponentName, logger, parseUrl } from '@sentry/utils';
- import { DEBUG_BUILD } from '../../common/debug-build.js';
- import { addPerformanceInstrumentationHandler, addClsInstrumentationHandler, addLcpInstrumentationHandler, addFidInstrumentationHandler } from '../instrument.js';
- import { WINDOW } from '../types.js';
- import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher.js';
- import { _startChild, isMeasurementValue } from './utils.js';
- const MAX_INT_AS_BYTES = 2147483647;
- /**
- * Converts from milliseconds to seconds
- * @param time time in ms
- */
- function msToSec(time) {
- return time / 1000;
- }
- function getBrowserPerformanceAPI() {
- // @ts-expect-error 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.
- * The callback returned by this function can be used to stop tracking & ensure all measurements are final & captured.
- *
- * @returns A function that forces web vitals collection
- */
- function startTrackingWebVitals() {
- const performance = getBrowserPerformanceAPI();
- if (performance && browserPerformanceTimeOrigin) {
- // @ts-expect-error 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');
- }
- const fidCallback = _trackFID();
- const clsCallback = _trackCLS();
- const lcpCallback = _trackLCP();
- return () => {
- fidCallback();
- clsCallback();
- lcpCallback();
- };
- }
- return () => undefined;
- }
- /**
- * Start tracking long tasks.
- */
- function startTrackingLongTasks() {
- addPerformanceInstrumentationHandler('longtask', ({ entries }) => {
- for (const entry of entries) {
- // eslint-disable-next-line deprecation/deprecation
- const transaction = getActiveTransaction() ;
- if (!transaction) {
- return;
- }
- const startTime = msToSec((browserPerformanceTimeOrigin ) + entry.startTime);
- const duration = msToSec(entry.duration);
- // eslint-disable-next-line deprecation/deprecation
- transaction.startChild({
- description: 'Main UI thread blocked',
- op: 'ui.long-task',
- origin: 'auto.ui.browser.metrics',
- startTimestamp: startTime,
- endTimestamp: startTime + duration,
- });
- }
- });
- }
- /**
- * Start tracking interaction events.
- */
- function startTrackingInteractions() {
- addPerformanceInstrumentationHandler('event', ({ entries }) => {
- for (const entry of entries) {
- // eslint-disable-next-line deprecation/deprecation
- const transaction = getActiveTransaction() ;
- if (!transaction) {
- return;
- }
- if (entry.name === 'click') {
- const startTime = msToSec((browserPerformanceTimeOrigin ) + entry.startTime);
- const duration = msToSec(entry.duration);
- const span = {
- description: htmlTreeAsString(entry.target),
- op: `ui.interaction.${entry.name}`,
- origin: 'auto.ui.browser.metrics',
- startTimestamp: startTime,
- endTimestamp: startTime + duration,
- };
- const componentName = getComponentName(entry.target);
- if (componentName) {
- span.attributes = { 'ui.component_name': componentName };
- }
- // eslint-disable-next-line deprecation/deprecation
- transaction.startChild(span);
- }
- }
- });
- }
- /** Starts tracking the Cumulative Layout Shift on the current page. */
- function _trackCLS() {
- return addClsInstrumentationHandler(({ metric }) => {
- const entry = metric.entries[metric.entries.length - 1];
- if (!entry) {
- return;
- }
- DEBUG_BUILD && logger.log('[Measurements] Adding CLS');
- _measurements['cls'] = { value: metric.value, unit: '' };
- _clsEntry = entry ;
- }, true);
- }
- /** Starts tracking the Largest Contentful Paint on the current page. */
- function _trackLCP() {
- return addLcpInstrumentationHandler(({ metric }) => {
- const entry = metric.entries[metric.entries.length - 1];
- if (!entry) {
- return;
- }
- DEBUG_BUILD && logger.log('[Measurements] Adding LCP');
- _measurements['lcp'] = { value: metric.value, unit: 'millisecond' };
- _lcpEntry = entry ;
- }, true);
- }
- /** Starts tracking the First Input Delay on the current page. */
- function _trackFID() {
- return addFidInstrumentationHandler(({ metric }) => {
- const entry = metric.entries[metric.entries.length - 1];
- if (!entry) {
- return;
- }
- const timeOrigin = msToSec(browserPerformanceTimeOrigin );
- const startTime = msToSec(entry.startTime);
- DEBUG_BUILD && 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;
- }
- DEBUG_BUILD && logger.log('[Tracing] Adding & adjusting spans using Performance API');
- const timeOrigin = msToSec(browserPerformanceTimeOrigin);
- const performanceEntries = performance.getEntries();
- let responseStartTimestamp;
- let requestStartTimestamp;
- const { op, start_timestamp: transactionStartTime } = spanToJSON(transaction);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- performanceEntries.slice(_performanceCursor).forEach((entry) => {
- const startTime = msToSec(entry.startTime);
- const duration = msToSec(entry.duration);
- // eslint-disable-next-line deprecation/deprecation
- if (transaction.op === 'navigation' && transactionStartTime && timeOrigin + startTime < transactionStartTime) {
- 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) {
- DEBUG_BUILD && logger.log('[Measurements] Adding FP');
- _measurements['fp'] = { value: entry.startTime, unit: 'millisecond' };
- }
- if (entry.name === 'first-contentful-paint' && shouldRecord) {
- DEBUG_BUILD && logger.log('[Measurements] Adding FCP');
- _measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' };
- }
- break;
- }
- case 'resource': {
- _addResourceSpans(transaction, entry, entry.name , 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 (op === 'pageload') {
- _addTtfbToMeasurements(_measurements, responseStartTimestamp, requestStartTimestamp, transactionStartTime);
- ['fcp', 'fp', 'lcp'].forEach(name => {
- if (!_measurements[name] || !transactionStartTime || timeOrigin >= transactionStartTime) {
- 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 - transactionStartTime) * 1000);
- const delta = normalizedValue - oldValue;
- DEBUG_BUILD && 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',
- origin: 'auto.ui.browser.metrics',
- 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 => {
- 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 ,
- origin: 'auto.resource.browser.metrics',
- 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',
- origin: 'auto.browser.browser.metrics',
- 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) {
- if (entry.responseEnd) {
- // 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.
- // In this case, ie. when the document request hasn't finished yet, `entry.responseEnd` will be 0.
- // In order not to produce faulty spans, where the end timestamp is before the start timestamp, we will only collect
- // these spans when the responseEnd value is available. The backend (Relay) would drop the entire transaction if it contained faulty spans.
- _startChild(transaction, {
- op: 'browser',
- origin: 'auto.browser.browser.metrics',
- description: 'request',
- startTimestamp: timeOrigin + msToSec(entry.requestStart ),
- endTimestamp: timeOrigin + msToSec(entry.responseEnd ),
- });
- _startChild(transaction, {
- op: 'browser',
- origin: 'auto.browser.browser.metrics',
- description: 'response',
- startTimestamp: timeOrigin + msToSec(entry.responseStart ),
- endTimestamp: timeOrigin + msToSec(entry.responseEnd ),
- });
- }
- }
- /** Create resource-related spans */
- function _addResourceSpans(
- transaction,
- entry,
- resourceUrl,
- 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;
- }
- const parsedUrl = parseUrl(resourceUrl);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const data = {};
- setResourceEntrySizeData(data, entry, 'transferSize', 'http.response_transfer_size');
- setResourceEntrySizeData(data, entry, 'encodedBodySize', 'http.response_content_length');
- setResourceEntrySizeData(data, entry, 'decodedBodySize', 'http.decoded_response_content_length');
- if ('renderBlockingStatus' in entry) {
- data['resource.render_blocking_status'] = entry.renderBlockingStatus;
- }
- if (parsedUrl.protocol) {
- data['url.scheme'] = parsedUrl.protocol.split(':').pop(); // the protocol returned by parseUrl includes a :, but OTEL spec does not, so we remove it.
- }
- if (parsedUrl.host) {
- data['server.address'] = parsedUrl.host;
- }
- data['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
- const startTimestamp = timeOrigin + startTime;
- const endTimestamp = startTimestamp + duration;
- _startChild(transaction, {
- description: resourceUrl.replace(WINDOW.location.origin, ''),
- endTimestamp,
- op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other',
- origin: 'auto.resource.browser.metrics',
- 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) {
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag('effectiveConnectionType', connection.effectiveType);
- }
- if (connection.type) {
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag('connectionType', connection.type);
- }
- if (isMeasurementValue(connection.rtt)) {
- _measurements['connection.rtt'] = { value: connection.rtt, unit: 'millisecond' };
- }
- }
- if (isMeasurementValue(navigator.deviceMemory)) {
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag('deviceMemory', `${navigator.deviceMemory} GB`);
- }
- if (isMeasurementValue(navigator.hardwareConcurrency)) {
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency));
- }
- }
- /** Add LCP / CLS data to transaction to allow debugging */
- function _tagMetricInfo(transaction) {
- if (_lcpEntry) {
- DEBUG_BUILD && logger.log('[Measurements] Adding LCP Data');
- // Capture Properties of the LCP element that contributes to the LCP.
- if (_lcpEntry.element) {
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag('lcp.element', htmlTreeAsString(_lcpEntry.element));
- }
- if (_lcpEntry.id) {
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag('lcp.id', _lcpEntry.id);
- }
- if (_lcpEntry.url) {
- // Trim URL to the first 200 characters.
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag('lcp.url', _lcpEntry.url.trim().slice(0, 200));
- }
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag('lcp.size', _lcpEntry.size);
- }
- // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift
- if (_clsEntry && _clsEntry.sources) {
- DEBUG_BUILD && logger.log('[Measurements] Adding CLS Data');
- _clsEntry.sources.forEach((source, index) =>
- // TODO: Can we rewrite this to an attribute?
- // eslint-disable-next-line deprecation/deprecation
- transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)),
- );
- }
- }
- function setResourceEntrySizeData(
- data,
- entry,
- key,
- dataKey,
- ) {
- const entryVal = entry[key];
- if (entryVal != null && entryVal < MAX_INT_AS_BYTES) {
- data[dataKey] = entryVal;
- }
- }
- /**
- * Add ttfb information to measurements
- *
- * Exported for tests
- */
- function _addTtfbToMeasurements(
- _measurements,
- responseStartTimestamp,
- requestStartTimestamp,
- transactionStartTime,
- ) {
- // 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' && transactionStartTime) {
- DEBUG_BUILD && logger.log('[Measurements] Adding TTFB');
- _measurements['ttfb'] = {
- // As per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStart,
- // responseStart can be 0 if the request is coming straight from the cache.
- // This might lead us to calculate a negative ttfb if we don't use Math.max here.
- //
- // This logic is the same as what is in the web-vitals library to calculate ttfb
- // https://github.com/GoogleChrome/web-vitals/blob/2301de5015e82b09925238a228a0893635854587/src/onTTFB.ts#L92
- // TODO(abhi): We should use the web-vitals library instead of this custom calculation.
- value: Math.max(responseStartTimestamp - transactionStartTime, 0) * 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',
- };
- }
- }
- }
- export { _addMeasureSpans, _addResourceSpans, _addTtfbToMeasurements, addPerformanceEntries, startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals };
- //# sourceMappingURL=index.js.map
|