Files
klz-cables.com/.pnpm-store/v10/files/94/5cffe1066fc5c2b66c16d6aa4031c73181212ccaced43d97cf5d0afb9c0a9a82f3268b48318b438f6dbe40746d125c26ae70eb6251817c2543c4079edc4e89
Marc Mintel 5397309103
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
fix(products): fix breadcrumbs and product filtering (backport from main)
2026-02-24 16:04:21 +01:00

726 lines
24 KiB
Plaintext

Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const core = require('@sentry/core');
const types = require('../types.js');
const cls = require('./cls.js');
const instrument = require('./instrument.js');
const lcp = require('./lcp.js');
const resourceTiming = require('./resourceTiming.js');
const utils = require('./utils.js');
const getActivationStart = require('./web-vitals/lib/getActivationStart.js');
const getNavigationEntry = require('./web-vitals/lib/getNavigationEntry.js');
const getVisibilityWatcher = require('./web-vitals/lib/getVisibilityWatcher.js');
const MAX_INT_AS_BYTES = 2147483647;
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({
recordClsStandaloneSpans,
recordLcpStandaloneSpans,
client,
}) {
const performance = utils.getBrowserPerformanceAPI();
if (performance && core.browserPerformanceTimeOrigin()) {
// @ts-expect-error we want to make sure all of these are available, even if TS is sure they are
if (performance.mark) {
types.WINDOW.performance.mark('sentry-tracing-init');
}
const lcpCleanupCallback = recordLcpStandaloneSpans ? lcp.trackLcpAsStandaloneSpan(client) : _trackLCP();
const ttfbCleanupCallback = _trackTtfb();
const clsCleanupCallback = recordClsStandaloneSpans ? cls.trackClsAsStandaloneSpan(client) : _trackCLS();
return () => {
lcpCleanupCallback?.();
ttfbCleanupCallback();
clsCleanupCallback?.();
};
}
return () => undefined;
}
/**
* Start tracking long tasks.
*/
function startTrackingLongTasks() {
instrument.addPerformanceInstrumentationHandler('longtask', ({ entries }) => {
const parent = core.getActiveSpan();
if (!parent) {
return;
}
const { op: parentOp, start_timestamp: parentStartTimestamp } = core.spanToJSON(parent);
for (const entry of entries) {
const startTime = utils.msToSec((core.browserPerformanceTimeOrigin() ) + entry.startTime);
const duration = utils.msToSec(entry.duration);
if (parentOp === 'navigation' && parentStartTimestamp && startTime < parentStartTimestamp) {
// Skip adding a span if the long task started before the navigation started.
// `startAndEndSpan` will otherwise adjust the parent's start time to the span's start
// time, potentially skewing the duration of the actual navigation as reported via our
// routing instrumentations
continue;
}
utils.startAndEndSpan(parent, startTime, startTime + duration, {
name: 'Main UI thread blocked',
op: 'ui.long-task',
attributes: {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
});
}
});
}
/**
* Start tracking long animation frames.
*/
function startTrackingLongAnimationFrames() {
// NOTE: the current web-vitals version (3.5.2) does not support long-animation-frame, so
// we directly observe `long-animation-frame` events instead of through the web-vitals
// `observe` helper function.
const observer = new PerformanceObserver(list => {
const parent = core.getActiveSpan();
if (!parent) {
return;
}
for (const entry of list.getEntries() ) {
if (!entry.scripts[0]) {
continue;
}
const startTime = utils.msToSec((core.browserPerformanceTimeOrigin() ) + entry.startTime);
const { start_timestamp: parentStartTimestamp, op: parentOp } = core.spanToJSON(parent);
if (parentOp === 'navigation' && parentStartTimestamp && startTime < parentStartTimestamp) {
// Skip adding the span if the long animation frame started before the navigation started.
// `startAndEndSpan` will otherwise adjust the parent's start time to the span's start
// time, potentially skewing the duration of the actual navigation as reported via our
// routing instrumentations
continue;
}
const duration = utils.msToSec(entry.duration);
const attributes = {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
};
const initialScript = entry.scripts[0];
const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript;
attributes['browser.script.invoker'] = invoker;
attributes['browser.script.invoker_type'] = invokerType;
if (sourceURL) {
attributes['code.filepath'] = sourceURL;
}
if (sourceFunctionName) {
attributes['code.function'] = sourceFunctionName;
}
if (sourceCharPosition !== -1) {
attributes['browser.script.source_char_position'] = sourceCharPosition;
}
utils.startAndEndSpan(parent, startTime, startTime + duration, {
name: 'Main UI thread blocked',
op: 'ui.long-animation-frame',
attributes,
});
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
}
/**
* Start tracking interaction events.
*/
function startTrackingInteractions() {
instrument.addPerformanceInstrumentationHandler('event', ({ entries }) => {
const parent = core.getActiveSpan();
if (!parent) {
return;
}
for (const entry of entries) {
if (entry.name === 'click') {
const startTime = utils.msToSec((core.browserPerformanceTimeOrigin() ) + entry.startTime);
const duration = utils.msToSec(entry.duration);
const spanOptions = {
name: core.htmlTreeAsString(entry.target),
op: `ui.interaction.${entry.name}`,
startTime: startTime,
attributes: {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
};
const componentName = core.getComponentName(entry.target);
if (componentName) {
spanOptions.attributes['ui.component_name'] = componentName;
}
utils.startAndEndSpan(parent, startTime, startTime + duration, spanOptions);
}
}
});
}
/**
* Starts tracking the Cumulative Layout Shift on the current page and collects the value and last entry
* to the `_measurements` object which ultimately is applied to the pageload span's measurements.
*/
function _trackCLS() {
return instrument.addClsInstrumentationHandler(({ metric }) => {
const entry = metric.entries[metric.entries.length - 1] ;
if (!entry) {
return;
}
_measurements['cls'] = { value: metric.value, unit: '' };
_clsEntry = entry;
}, true);
}
/** Starts tracking the Largest Contentful Paint on the current page. */
function _trackLCP() {
return instrument.addLcpInstrumentationHandler(({ metric }) => {
const entry = metric.entries[metric.entries.length - 1];
if (!entry) {
return;
}
_measurements['lcp'] = { value: metric.value, unit: 'millisecond' };
_lcpEntry = entry ;
}, true);
}
function _trackTtfb() {
return instrument.addTtfbInstrumentationHandler(({ metric }) => {
const entry = metric.entries[metric.entries.length - 1];
if (!entry) {
return;
}
_measurements['ttfb'] = { value: metric.value, unit: 'millisecond' };
});
}
/** Add performance related spans to a transaction */
function addPerformanceEntries(span, options) {
const performance = utils.getBrowserPerformanceAPI();
const origin = core.browserPerformanceTimeOrigin();
if (!performance?.getEntries || !origin) {
// Gatekeeper if performance API not available
return;
}
const timeOrigin = utils.msToSec(origin);
const performanceEntries = performance.getEntries();
const { op, start_timestamp: transactionStartTime } = core.spanToJSON(span);
performanceEntries.slice(_performanceCursor).forEach(entry => {
const startTime = utils.msToSec(entry.startTime);
const duration = utils.msToSec(
// Inexplicably, Chrome sometimes emits a negative duration. We need to work around this.
// There is a SO post attempting to explain this, but it leaves one with open questions: https://stackoverflow.com/questions/23191918/peformance-getentries-and-negative-duration-display
// The way we clamp the value is probably not accurate, since we have observed this happen for things that may take a while to load, like for example the replay worker.
// TODO: Investigate why this happens and how to properly mitigate. For now, this is a workaround to prevent transactions being dropped due to negative duration spans.
Math.max(0, entry.duration),
);
if (op === 'navigation' && transactionStartTime && timeOrigin + startTime < transactionStartTime) {
return;
}
switch (entry.entryType) {
case 'navigation': {
_addNavigationSpans(span, entry , timeOrigin);
break;
}
case 'mark':
case 'paint':
case 'measure': {
_addMeasureSpans(span, entry, startTime, duration, timeOrigin, options.ignorePerformanceApiSpans);
// capture web vitals
const firstHidden = getVisibilityWatcher.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) {
_measurements['fp'] = { value: entry.startTime, unit: 'millisecond' };
}
if (entry.name === 'first-contentful-paint' && shouldRecord) {
_measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' };
}
break;
}
case 'resource': {
_addResourceSpans(
span,
entry ,
entry.name,
startTime,
duration,
timeOrigin,
options.ignoreResourceSpans,
);
break;
}
// Ignore other entry types.
}
});
_performanceCursor = Math.max(performanceEntries.length - 1, 0);
_trackNavigator(span);
// Measurements are only available for pageload transactions
if (op === 'pageload') {
_addTtfbRequestTimeToMeasurements(_measurements);
// If CLS standalone spans are enabled, don't record CLS as a measurement
if (!options.recordClsOnPageloadSpan) {
delete _measurements.cls;
}
// If LCP standalone spans are enabled, don't record LCP as a measurement
if (!options.recordLcpOnPageloadSpan) {
delete _measurements.lcp;
}
Object.entries(_measurements).forEach(([measurementName, measurement]) => {
core.setMeasurement(measurementName, measurement.value, measurement.unit);
});
// Set timeOrigin which denotes the timestamp which to base the LCP/FCP/FP/TTFB measurements on
span.setAttribute('performance.timeOrigin', timeOrigin);
// In prerendering scenarios, where a page might be prefetched and pre-rendered before the user clicks the link,
// the navigation starts earlier than when the user clicks it. Web Vitals should always be based on the
// user-perceived time, so they are not reported from the actual start of the navigation, but rather from the
// time where the user actively started the navigation, for example by clicking a link.
// This is user action is called "activation" and the time between navigation and activation is stored in
// the `activationStart` attribute of the "navigation" PerformanceEntry.
span.setAttribute('performance.activationStart', getActivationStart.getActivationStart());
_setWebVitalAttributes(span, options);
}
_lcpEntry = undefined;
_clsEntry = undefined;
_measurements = {};
}
/**
* React 19.2+ creates performance.measure entries for component renders.
* We can identify them by the `detail.devtools.track` property being set to 'Components ⚛'.
* see: https://react.dev/reference/dev-tools/react-performance-tracks
* see: https://github.com/facebook/react/blob/06fcc8f380c6a905c7bc18d94453f623cf8cbc81/packages/react-reconciler/src/ReactFiberPerformanceTrack.js#L454-L473
*/
function isReact19MeasureEntry(entry) {
if (entry?.entryType !== 'measure') {
return;
}
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return (entry ).detail.devtools.track === 'Components ⚛';
} catch {
return;
}
}
/**
* Create measure related spans.
* Exported only for tests.
*/
function _addMeasureSpans(
span,
entry,
startTime,
duration,
timeOrigin,
ignorePerformanceApiSpans,
) {
if (isReact19MeasureEntry(entry)) {
return;
}
if (
['mark', 'measure'].includes(entry.entryType) &&
core.stringMatchesSomePattern(entry.name, ignorePerformanceApiSpans)
) {
return;
}
const navEntry = getNavigationEntry.getNavigationEntry(false);
const requestTime = utils.msToSec(navEntry ? navEntry.requestStart : 0);
// Because performance.measure accepts arbitrary timestamps it can produce
// spans that happen before the browser even makes a request for the page.
//
// An example of this is the automatically generated Next.js-before-hydration
// spans created by the Next.js framework.
//
// To prevent this we will pin the start timestamp to the request start time
// This does make duration inaccurate, so if this does happen, we will add
// an attribute to the span
const measureStartTimestamp = timeOrigin + Math.max(startTime, requestTime);
const startTimeStamp = timeOrigin + startTime;
const measureEndTimestamp = startTimeStamp + duration;
const attributes = {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
if (measureStartTimestamp !== startTimeStamp) {
attributes['sentry.browser.measure_happened_before_request'] = true;
attributes['sentry.browser.measure_start_time'] = measureStartTimestamp;
}
_addDetailToSpanAttributes(attributes, entry );
// Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process.
if (measureStartTimestamp <= measureEndTimestamp) {
utils.startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, {
name: entry.name,
op: entry.entryType,
attributes,
});
}
}
function _addDetailToSpanAttributes(attributes, performanceMeasure) {
try {
// Accessing detail might throw in some browsers (e.g., Firefox) due to security restrictions
const detail = performanceMeasure.detail;
if (!detail) {
return;
}
// Process detail based on its type
if (typeof detail === 'object') {
// Handle object details
for (const [key, value] of Object.entries(detail)) {
if (value && core.isPrimitive(value)) {
attributes[`sentry.browser.measure.detail.${key}`] = value ;
} else if (value !== undefined) {
try {
// This is user defined so we can't guarantee it's serializable
attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value);
} catch {
// Skip values that can't be stringified
}
}
}
return;
}
if (core.isPrimitive(detail)) {
// Handle primitive details
attributes['sentry.browser.measure.detail'] = detail ;
return;
}
try {
attributes['sentry.browser.measure.detail'] = JSON.stringify(detail);
} catch {
// Skip if stringification fails
}
} catch {
// Silently ignore any errors when accessing detail
// This handles the Firefox "Permission denied to access object" error
}
}
/**
* Instrument navigation entries
* exported only for tests
*/
function _addNavigationSpans(span, entry, timeOrigin) {
(['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'] ).forEach(event => {
_addPerformanceNavigationTiming(span, entry, event, timeOrigin);
});
_addPerformanceNavigationTiming(span, entry, 'secureConnection', timeOrigin, 'TLS/SSL');
_addPerformanceNavigationTiming(span, entry, 'fetch', timeOrigin, 'cache');
_addPerformanceNavigationTiming(span, entry, 'domainLookup', timeOrigin, 'DNS');
_addRequest(span, entry, timeOrigin);
}
/** Create performance navigation related spans */
function _addPerformanceNavigationTiming(
span,
entry,
event,
timeOrigin,
name = event,
) {
const eventEnd = _getEndPropertyNameForNavigationTiming(event) ;
const end = entry[eventEnd];
const start = entry[`${event}Start`];
if (!start || !end) {
return;
}
utils.startAndEndSpan(span, timeOrigin + utils.msToSec(start), timeOrigin + utils.msToSec(end), {
op: `browser.${name}`,
name: entry.name,
attributes: {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
...(event === 'redirect' && entry.redirectCount != null ? { 'http.redirect_count': entry.redirectCount } : {}),
},
});
}
function _getEndPropertyNameForNavigationTiming(event) {
if (event === 'secureConnection') {
return 'connectEnd';
}
if (event === 'fetch') {
return 'domainLookupStart';
}
return `${event}End`;
}
/** Create request and response related spans */
function _addRequest(span, entry, timeOrigin) {
const requestStartTimestamp = timeOrigin + utils.msToSec(entry.requestStart);
const responseEndTimestamp = timeOrigin + utils.msToSec(entry.responseEnd);
const responseStartTimestamp = timeOrigin + utils.msToSec(entry.responseStart);
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 span if it contained faulty spans.
utils.startAndEndSpan(span, requestStartTimestamp, responseEndTimestamp, {
op: 'browser.request',
name: entry.name,
attributes: {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
});
utils.startAndEndSpan(span, responseStartTimestamp, responseEndTimestamp, {
op: 'browser.response',
name: entry.name,
attributes: {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
});
}
}
/**
* Create resource-related spans.
* Exported only for tests.
*/
function _addResourceSpans(
span,
entry,
resourceUrl,
startTime,
duration,
timeOrigin,
ignoredResourceSpanOps,
) {
// 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 op = entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other';
if (ignoredResourceSpanOps?.includes(op)) {
return;
}
const attributes = {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
const parsedUrl = core.parseUrl(resourceUrl);
if (parsedUrl.protocol) {
attributes['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) {
attributes['server.address'] = parsedUrl.host;
}
attributes['url.same_origin'] = resourceUrl.includes(types.WINDOW.location.origin);
_setResourceRequestAttributes(entry, attributes, [
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStatus
['responseStatus', 'http.response.status_code'],
['transferSize', 'http.response_transfer_size'],
['encodedBodySize', 'http.response_content_length'],
['decodedBodySize', 'http.decoded_response_content_length'],
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/renderBlockingStatus
['renderBlockingStatus', 'resource.render_blocking_status'],
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/deliveryType
['deliveryType', 'http.response_delivery_type'],
]);
const attributesWithResourceTiming = { ...attributes, ...resourceTiming.resourceTimingToSpanAttributes(entry) };
const startTimestamp = timeOrigin + startTime;
const endTimestamp = startTimestamp + duration;
utils.startAndEndSpan(span, startTimestamp, endTimestamp, {
name: resourceUrl.replace(types.WINDOW.location.origin, ''),
op,
attributes: attributesWithResourceTiming,
});
}
/**
* Capture the information of the user agent.
*/
function _trackNavigator(span) {
const navigator = types.WINDOW.navigator ;
if (!navigator) {
return;
}
// track network connectivity
const connection = navigator.connection;
if (connection) {
if (connection.effectiveType) {
span.setAttribute('effectiveConnectionType', connection.effectiveType);
}
if (connection.type) {
span.setAttribute('connectionType', connection.type);
}
if (utils.isMeasurementValue(connection.rtt)) {
_measurements['connection.rtt'] = { value: connection.rtt, unit: 'millisecond' };
}
}
if (utils.isMeasurementValue(navigator.deviceMemory)) {
span.setAttribute('deviceMemory', `${navigator.deviceMemory} GB`);
}
if (utils.isMeasurementValue(navigator.hardwareConcurrency)) {
span.setAttribute('hardwareConcurrency', String(navigator.hardwareConcurrency));
}
}
/** Add LCP / CLS data to span to allow debugging */
function _setWebVitalAttributes(span, options) {
// Only add LCP attributes if LCP is being recorded on the pageload span
if (_lcpEntry && options.recordLcpOnPageloadSpan) {
// Capture Properties of the LCP element that contributes to the LCP.
if (_lcpEntry.element) {
span.setAttribute('lcp.element', core.htmlTreeAsString(_lcpEntry.element));
}
if (_lcpEntry.id) {
span.setAttribute('lcp.id', _lcpEntry.id);
}
if (_lcpEntry.url) {
// Trim URL to the first 200 characters.
span.setAttribute('lcp.url', _lcpEntry.url.trim().slice(0, 200));
}
if (_lcpEntry.loadTime != null) {
// loadTime is the time of LCP that's related to receiving the LCP element response..
span.setAttribute('lcp.loadTime', _lcpEntry.loadTime);
}
if (_lcpEntry.renderTime != null) {
// renderTime is loadTime + rendering time
// it's 0 if the LCP element is loaded from a 3rd party origin that doesn't send the
// `Timing-Allow-Origin` header.
span.setAttribute('lcp.renderTime', _lcpEntry.renderTime);
}
span.setAttribute('lcp.size', _lcpEntry.size);
}
// Only add CLS attributes if CLS is being recorded on the pageload span
if (_clsEntry?.sources && options.recordClsOnPageloadSpan) {
_clsEntry.sources.forEach((source, index) =>
span.setAttribute(`cls.source.${index + 1}`, core.htmlTreeAsString(source.node)),
);
}
}
/**
* Use this to set any attributes we can take directly form the PerformanceResourceTiming entry.
*
* This is just a mapping function for entry->attribute to keep bundle-size minimal.
* Experimental properties are also accepted (see {@link ExperimentalResourceTimingProperty}).
* Assumes that all entry properties might be undefined for browser-specific differences.
* Only accepts string and number values for now and also sets 0-values.
*/
function _setResourceRequestAttributes(
entry,
attributes,
properties,
) {
properties.forEach(([entryKey, attributeKey]) => {
const entryVal = entry[entryKey];
if (
entryVal != null &&
((typeof entryVal === 'number' && entryVal < MAX_INT_AS_BYTES) || typeof entryVal === 'string')
) {
attributes[attributeKey] = entryVal;
}
});
}
/**
* Add ttfb request time information to measurements.
*
* ttfb information is added via vendored web vitals library.
*/
function _addTtfbRequestTimeToMeasurements(_measurements) {
const navEntry = getNavigationEntry.getNavigationEntry(false);
if (!navEntry) {
return;
}
const { responseStart, requestStart } = navEntry;
if (requestStart <= responseStart) {
_measurements['ttfb.requestTime'] = {
value: responseStart - requestStart,
unit: 'millisecond',
};
}
}
exports._addMeasureSpans = _addMeasureSpans;
exports._addNavigationSpans = _addNavigationSpans;
exports._addResourceSpans = _addResourceSpans;
exports._setResourceRequestAttributes = _setResourceRequestAttributes;
exports.addPerformanceEntries = addPerformanceEntries;
exports.startTrackingInteractions = startTrackingInteractions;
exports.startTrackingLongAnimationFrames = startTrackingLongAnimationFrames;
exports.startTrackingLongTasks = startTrackingLongTasks;
exports.startTrackingWebVitals = startTrackingWebVitals;
//# sourceMappingURL=browserMetrics.js.map