707 lines
23 KiB
JavaScript
707 lines
23 KiB
JavaScript
import { getActiveTransaction, spanToJSON, setMeasurement, getClient, Span, createSpanEnvelope, hasTracingEnabled, isValidSampleRate } 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, addTtfbInstrumentationHandler, addInpInstrumentationHandler } from '../instrument.js';
|
|
import { WINDOW } from '../types.js';
|
|
import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher.js';
|
|
import { _startChild, isMeasurementValue } from './utils.js';
|
|
import { getNavigationEntry } from '../web-vitals/lib/getNavigationEntry.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();
|
|
const ttfbCallback = _trackTtfb();
|
|
|
|
return () => {
|
|
fidCallback();
|
|
clsCallback();
|
|
lcpCallback();
|
|
ttfbCallback();
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start tracking INP webvital events.
|
|
*/
|
|
function startTrackingINP(
|
|
interactionIdtoRouteNameMapping,
|
|
interactionsSampleRate,
|
|
) {
|
|
const performance = getBrowserPerformanceAPI();
|
|
if (performance && browserPerformanceTimeOrigin) {
|
|
const inpCallback = _trackINP(interactionIdtoRouteNameMapping, interactionsSampleRate);
|
|
|
|
return () => {
|
|
inpCallback();
|
|
};
|
|
}
|
|
|
|
return () => undefined;
|
|
}
|
|
|
|
/** 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' };
|
|
});
|
|
}
|
|
|
|
function _trackTtfb() {
|
|
return addTtfbInstrumentationHandler(({ metric }) => {
|
|
const entry = metric.entries[metric.entries.length - 1];
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
|
|
DEBUG_BUILD && logger.log('[Measurements] Adding TTFB');
|
|
_measurements['ttfb'] = { value: metric.value, unit: 'millisecond' };
|
|
});
|
|
}
|
|
|
|
const INP_ENTRY_MAP = {
|
|
click: 'click',
|
|
pointerdown: 'click',
|
|
pointerup: 'click',
|
|
mousedown: 'click',
|
|
mouseup: 'click',
|
|
touchstart: 'click',
|
|
touchend: 'click',
|
|
mouseover: 'hover',
|
|
mouseout: 'hover',
|
|
mouseenter: 'hover',
|
|
mouseleave: 'hover',
|
|
pointerover: 'hover',
|
|
pointerout: 'hover',
|
|
pointerenter: 'hover',
|
|
pointerleave: 'hover',
|
|
dragstart: 'drag',
|
|
dragend: 'drag',
|
|
drag: 'drag',
|
|
dragenter: 'drag',
|
|
dragleave: 'drag',
|
|
dragover: 'drag',
|
|
drop: 'drag',
|
|
keydown: 'press',
|
|
keyup: 'press',
|
|
keypress: 'press',
|
|
input: 'press',
|
|
};
|
|
|
|
/** Starts tracking the Interaction to Next Paint on the current page. */
|
|
function _trackINP(
|
|
interactionIdToRouteNameMapping,
|
|
interactionsSampleRate,
|
|
) {
|
|
return addInpInstrumentationHandler(({ metric }) => {
|
|
if (metric.value === undefined) {
|
|
return;
|
|
}
|
|
const entry = metric.entries.find(
|
|
entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name] !== undefined,
|
|
);
|
|
const client = getClient();
|
|
if (!entry || !client) {
|
|
return;
|
|
}
|
|
const interactionType = INP_ENTRY_MAP[entry.name];
|
|
const options = client.getOptions();
|
|
/** Build the INP span, create an envelope from the span, and then send the envelope */
|
|
const startTime = msToSec((browserPerformanceTimeOrigin ) + entry.startTime);
|
|
const duration = msToSec(metric.value);
|
|
const interaction =
|
|
entry.interactionId !== undefined ? interactionIdToRouteNameMapping[entry.interactionId] : undefined;
|
|
if (interaction === undefined) {
|
|
return;
|
|
}
|
|
const { routeName, parentContext, activeTransaction, user, replayId } = interaction;
|
|
const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined;
|
|
// eslint-disable-next-line deprecation/deprecation
|
|
const profileId = activeTransaction !== undefined ? activeTransaction.getProfileId() : undefined;
|
|
const span = new Span({
|
|
startTimestamp: startTime,
|
|
endTimestamp: startTime + duration,
|
|
op: `ui.interaction.${interactionType}`,
|
|
name: htmlTreeAsString(entry.target),
|
|
attributes: {
|
|
release: options.release,
|
|
environment: options.environment,
|
|
transaction: routeName,
|
|
...(userDisplay !== undefined && userDisplay !== '' ? { user: userDisplay } : {}),
|
|
...(profileId !== undefined ? { profile_id: profileId } : {}),
|
|
...(replayId !== undefined ? { replay_id: replayId } : {}),
|
|
},
|
|
exclusiveTime: metric.value,
|
|
measurements: {
|
|
inp: { value: metric.value, unit: 'millisecond' },
|
|
},
|
|
});
|
|
|
|
/** Check to see if the span should be sampled */
|
|
const sampleRate = getSampleRate(parentContext, options, interactionsSampleRate);
|
|
|
|
if (!sampleRate) {
|
|
return;
|
|
}
|
|
|
|
if (Math.random() < (sampleRate )) {
|
|
const envelope = span ? createSpanEnvelope([span], client.getDsn()) : undefined;
|
|
const transport = client && client.getTransport();
|
|
if (transport && envelope) {
|
|
transport.send(envelope).then(null, reason => {
|
|
DEBUG_BUILD && logger.error('Error while sending interaction:', reason);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
/** 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();
|
|
|
|
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);
|
|
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') {
|
|
_addTtfbRequestTimeToMeasurements(_measurements);
|
|
|
|
['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 request time information to measurements.
|
|
*
|
|
* ttfb information is added via vendored web vitals library.
|
|
*/
|
|
function _addTtfbRequestTimeToMeasurements(_measurements) {
|
|
const navEntry = getNavigationEntry();
|
|
if (!navEntry) {
|
|
return;
|
|
}
|
|
|
|
const { responseStart, requestStart } = navEntry;
|
|
|
|
if (requestStart <= responseStart) {
|
|
DEBUG_BUILD && logger.log('[Measurements] Adding TTFB Request Time');
|
|
_measurements['ttfb.requestTime'] = {
|
|
value: responseStart - requestStart,
|
|
unit: 'millisecond',
|
|
};
|
|
}
|
|
}
|
|
|
|
/** Taken from @sentry/core sampling.ts */
|
|
function getSampleRate(
|
|
transactionContext,
|
|
options,
|
|
interactionsSampleRate,
|
|
) {
|
|
if (!hasTracingEnabled(options)) {
|
|
return false;
|
|
}
|
|
let sampleRate;
|
|
if (transactionContext !== undefined && typeof options.tracesSampler === 'function') {
|
|
sampleRate = options.tracesSampler({
|
|
transactionContext,
|
|
name: transactionContext.name,
|
|
parentSampled: transactionContext.parentSampled,
|
|
attributes: {
|
|
// eslint-disable-next-line deprecation/deprecation
|
|
...transactionContext.data,
|
|
...transactionContext.attributes,
|
|
},
|
|
location: WINDOW.location,
|
|
});
|
|
} else if (transactionContext !== undefined && transactionContext.sampled !== undefined) {
|
|
sampleRate = transactionContext.sampled;
|
|
} else if (typeof options.tracesSampleRate !== 'undefined') {
|
|
sampleRate = options.tracesSampleRate;
|
|
} else {
|
|
sampleRate = 1;
|
|
}
|
|
if (!isValidSampleRate(sampleRate)) {
|
|
DEBUG_BUILD && logger.warn('[Tracing] Discarding interaction span because of invalid sample rate.');
|
|
return false;
|
|
}
|
|
if (sampleRate === true) {
|
|
return interactionsSampleRate;
|
|
} else if (sampleRate === false) {
|
|
return 0;
|
|
}
|
|
return sampleRate * interactionsSampleRate;
|
|
}
|
|
|
|
export { _addMeasureSpans, _addResourceSpans, addPerformanceEntries, startTrackingINP, startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals };
|
|
//# sourceMappingURL=index.js.map
|