init commit

This commit is contained in:
rxliuli
2025-11-04 05:03:50 +08:00
commit bce557cc2d
1396 changed files with 172991 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { getActiveTransaction } from '@sentry/core';
import { logger } from '@sentry/utils';
import { WINDOW } from './types.js';
/**
* Add a listener that cancels and finishes a transaction when the global
* document is hidden.
*/
function registerBackgroundTabDetection() {
if (WINDOW && WINDOW.document) {
WINDOW.document.addEventListener('visibilitychange', () => {
const activeTransaction = getActiveTransaction() ;
if (WINDOW.document.hidden && activeTransaction) {
const statusType = 'cancelled';
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
logger.log(
`[Tracing] Transaction: ${statusType} -> since tab moved to the background, op: ${activeTransaction.op}`,
);
// We should not set status if it is already set, this prevent important statuses like
// error or data loss from being overwritten on transaction.
if (!activeTransaction.status) {
activeTransaction.setStatus(statusType);
}
activeTransaction.setTag('visibilitychange', 'document.hidden');
activeTransaction.finish();
}
});
} else {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
logger.warn('[Tracing] Could not set up background tab detection due to lack of global document');
}
}
export { registerBackgroundTabDetection };
//# sourceMappingURL=backgroundtab.js.map

View File

@@ -0,0 +1,300 @@
import { TRACING_DEFAULTS, addTracingExtensions, extractTraceparentData, startIdleTransaction, getActiveTransaction } from '@sentry/core';
import { logger, baggageHeaderToDynamicSamplingContext, getDomElement } from '@sentry/utils';
import { registerBackgroundTabDetection } from './backgroundtab.js';
import { startTrackingWebVitals, startTrackingLongTasks, startTrackingInteractions, addPerformanceEntries } from './metrics/index.js';
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request.js';
import { instrumentRoutingWithDefaults } from './router.js';
import { WINDOW } from './types.js';
const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
/** Options for Browser Tracing integration */
const DEFAULT_BROWSER_TRACING_OPTIONS = {
...TRACING_DEFAULTS,
markBackgroundTransactions: true,
routingInstrumentation: instrumentRoutingWithDefaults,
startTransactionOnLocationChange: true,
startTransactionOnPageLoad: true,
enableLongTask: true,
...defaultRequestInstrumentationOptions,
};
/**
* The Browser Tracing integration automatically instruments browser pageload/navigation
* actions as transactions, and captures requests, metrics and errors as spans.
*
* The integration can be configured with a variety of options, and can be extended to use
* any routing library. This integration uses {@see IdleTransaction} to create transactions.
*/
class BrowserTracing {
// This class currently doesn't have a static `id` field like the other integration classes, because it prevented
// @sentry/tracing from being treeshaken. Tree shakers do not like static fields, because they behave like side effects.
// TODO: Come up with a better plan, than using static fields on integration classes, and use that plan on all
// integrations.
/** Browser Tracing integration options */
/**
* @inheritDoc
*/
__init() {this.name = BROWSER_TRACING_INTEGRATION_ID;}
__init2() {this._hasSetTracePropagationTargets = false;}
constructor(_options) {BrowserTracing.prototype.__init.call(this);BrowserTracing.prototype.__init2.call(this);
addTracingExtensions();
if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) {
this._hasSetTracePropagationTargets = !!(
_options &&
// eslint-disable-next-line deprecation/deprecation
(_options.tracePropagationTargets || _options.tracingOrigins)
);
}
this.options = {
...DEFAULT_BROWSER_TRACING_OPTIONS,
..._options,
};
// Special case: enableLongTask can be set in _experiments
// TODO (v8): Remove this in v8
if (this.options._experiments.enableLongTask !== undefined) {
this.options.enableLongTask = this.options._experiments.enableLongTask;
}
// TODO (v8): remove this block after tracingOrigins is removed
// Set tracePropagationTargets to tracingOrigins if specified by the user
// In case both are specified, tracePropagationTargets takes precedence
// eslint-disable-next-line deprecation/deprecation
if (_options && !_options.tracePropagationTargets && _options.tracingOrigins) {
// eslint-disable-next-line deprecation/deprecation
this.options.tracePropagationTargets = _options.tracingOrigins;
}
this._collectWebVitals = startTrackingWebVitals();
if (this.options.enableLongTask) {
startTrackingLongTasks();
}
if (this.options._experiments.enableInteractions) {
startTrackingInteractions();
}
}
/**
* @inheritDoc
*/
setupOnce(_, getCurrentHub) {
this._getCurrentHub = getCurrentHub;
const hub = getCurrentHub();
const client = hub.getClient();
const clientOptions = client && client.getOptions();
const {
routingInstrumentation: instrumentRouting,
startTransactionOnLocationChange,
startTransactionOnPageLoad,
markBackgroundTransactions,
traceFetch,
traceXHR,
shouldCreateSpanForRequest,
_experiments,
} = this.options;
const clientOptionsTracePropagationTargets = clientOptions && clientOptions.tracePropagationTargets;
// There are three ways to configure tracePropagationTargets:
// 1. via top level client option `tracePropagationTargets`
// 2. via BrowserTracing option `tracePropagationTargets`
// 3. via BrowserTracing option `tracingOrigins` (deprecated)
//
// To avoid confusion, favour top level client option `tracePropagationTargets`, and fallback to
// BrowserTracing option `tracePropagationTargets` and then `tracingOrigins` (deprecated).
// This is done as it minimizes bundle size (we don't have to have undefined checks).
//
// If both 1 and either one of 2 or 3 are set (from above), we log out a warning.
const tracePropagationTargets = clientOptionsTracePropagationTargets || this.options.tracePropagationTargets;
if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && this._hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) {
logger.warn(
'[Tracing] The `tracePropagationTargets` option was set in the BrowserTracing integration and top level `Sentry.init`. The top level `Sentry.init` value is being used.',
);
}
instrumentRouting(
(context) => {
const transaction = this._createRouteTransaction(context);
this.options._experiments.onStartRouteTransaction &&
this.options._experiments.onStartRouteTransaction(transaction, context, getCurrentHub);
return transaction;
},
startTransactionOnPageLoad,
startTransactionOnLocationChange,
);
if (markBackgroundTransactions) {
registerBackgroundTabDetection();
}
if (_experiments.enableInteractions) {
this._registerInteractionListener();
}
instrumentOutgoingRequests({
traceFetch,
traceXHR,
tracePropagationTargets,
shouldCreateSpanForRequest,
_experiments: {
enableHTTPTimings: _experiments.enableHTTPTimings,
},
});
}
/** Create routing idle transaction. */
_createRouteTransaction(context) {
if (!this._getCurrentHub) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
logger.warn(`[Tracing] Did not create ${context.op} transaction because _getCurrentHub is invalid.`);
return undefined;
}
const { beforeNavigate, idleTimeout, finalTimeout, heartbeatInterval } = this.options;
const isPageloadTransaction = context.op === 'pageload';
const sentryTraceMetaTagValue = isPageloadTransaction ? getMetaContent('sentry-trace') : null;
const baggageMetaTagValue = isPageloadTransaction ? getMetaContent('baggage') : null;
const traceParentData = sentryTraceMetaTagValue ? extractTraceparentData(sentryTraceMetaTagValue) : undefined;
const dynamicSamplingContext = baggageMetaTagValue
? baggageHeaderToDynamicSamplingContext(baggageMetaTagValue)
: undefined;
const expandedContext = {
...context,
...traceParentData,
metadata: {
...context.metadata,
dynamicSamplingContext: traceParentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
trimEnd: true,
};
const modifiedContext = typeof beforeNavigate === 'function' ? beforeNavigate(expandedContext) : expandedContext;
// For backwards compatibility reasons, beforeNavigate can return undefined to "drop" the transaction (prevent it
// from being sent to Sentry).
const finalContext = modifiedContext === undefined ? { ...expandedContext, sampled: false } : modifiedContext;
// If `beforeNavigate` set a custom name, record that fact
finalContext.metadata =
finalContext.name !== expandedContext.name
? { ...finalContext.metadata, source: 'custom' }
: finalContext.metadata;
this._latestRouteName = finalContext.name;
this._latestRouteSource = finalContext.metadata && finalContext.metadata.source;
if (finalContext.sampled === false) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
}
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`);
const hub = this._getCurrentHub();
const { location } = WINDOW;
const idleTransaction = startIdleTransaction(
hub,
finalContext,
idleTimeout,
finalTimeout,
true,
{ location }, // for use in the tracesSampler
heartbeatInterval,
);
idleTransaction.registerBeforeFinishCallback(transaction => {
this._collectWebVitals();
addPerformanceEntries(transaction);
});
return idleTransaction ;
}
/** Start listener for interaction transactions */
_registerInteractionListener() {
let inflightInteractionTransaction;
const registerInteractionTransaction = () => {
const { idleTimeout, finalTimeout, heartbeatInterval } = this.options;
const op = 'ui.action.click';
const currentTransaction = getActiveTransaction();
if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
logger.warn(
`[Tracing] Did not create ${op} transaction because a pageload or navigation transaction is in progress.`,
);
return undefined;
}
if (inflightInteractionTransaction) {
inflightInteractionTransaction.setFinishReason('interactionInterrupted');
inflightInteractionTransaction.finish();
inflightInteractionTransaction = undefined;
}
if (!this._getCurrentHub) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn(`[Tracing] Did not create ${op} transaction because _getCurrentHub is invalid.`);
return undefined;
}
if (!this._latestRouteName) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
return undefined;
}
const hub = this._getCurrentHub();
const { location } = WINDOW;
const context = {
name: this._latestRouteName,
op,
trimEnd: true,
metadata: {
source: this._latestRouteSource || 'url',
},
};
inflightInteractionTransaction = startIdleTransaction(
hub,
context,
idleTimeout,
finalTimeout,
true,
{ location }, // for use in the tracesSampler
heartbeatInterval,
);
};
['click'].forEach(type => {
addEventListener(type, registerInteractionTransaction, { once: false, capture: true });
});
}
}
/** Returns the value of a meta tag */
function getMetaContent(metaName) {
// Can't specify generic to `getDomElement` because tracing can be used
// in a variety of environments, have to disable `no-unsafe-member-access`
// as a result.
const metaTag = getDomElement(`meta[name=${metaName}]`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return metaTag ? metaTag.getAttribute('content') : null;
}
export { BROWSER_TRACING_INTEGRATION_ID, BrowserTracing, getMetaContent };
//# sourceMappingURL=browsertracing.js.map

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,335 @@
import { _optionalChain } from '@sentry/utils/esm/buildPolyfills';
import { hasTracingEnabled, getCurrentHub } from '@sentry/core';
import { addInstrumentationHandler, browserPerformanceTimeOrigin, dynamicSamplingContextToSentryBaggageHeader, isInstanceOf, BAGGAGE_HEADER_NAME, SENTRY_XHR_DATA_KEY, stringMatchesSomePattern } from '@sentry/utils';
/* eslint-disable max-lines */
const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
/** Options for Request Instrumentation */
const defaultRequestInstrumentationOptions = {
traceFetch: true,
traceXHR: true,
// TODO (v8): Remove this property
tracingOrigins: DEFAULT_TRACE_PROPAGATION_TARGETS,
tracePropagationTargets: DEFAULT_TRACE_PROPAGATION_TARGETS,
_experiments: {},
};
/** Registers span creators for xhr and fetch requests */
function instrumentOutgoingRequests(_options) {
// eslint-disable-next-line deprecation/deprecation
const { traceFetch, traceXHR, tracePropagationTargets, tracingOrigins, shouldCreateSpanForRequest, _experiments } = {
traceFetch: defaultRequestInstrumentationOptions.traceFetch,
traceXHR: defaultRequestInstrumentationOptions.traceXHR,
..._options,
};
const shouldCreateSpan =
typeof shouldCreateSpanForRequest === 'function' ? shouldCreateSpanForRequest : (_) => true;
// TODO(v8) Remove tracingOrigins here
// The only reason we're passing it in here is because this instrumentOutgoingRequests function is publicly exported
// and we don't want to break the API. We can remove it in v8.
const shouldAttachHeadersWithTargets = (url) =>
shouldAttachHeaders(url, tracePropagationTargets || tracingOrigins);
const spans = {};
if (traceFetch) {
addInstrumentationHandler('fetch', (handlerData) => {
const createdSpan = fetchCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
if (_optionalChain([_experiments, 'optionalAccess', _2 => _2.enableHTTPTimings]) && createdSpan) {
addHTTPTimings(createdSpan);
}
});
}
if (traceXHR) {
addInstrumentationHandler('xhr', (handlerData) => {
const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
if (_optionalChain([_experiments, 'optionalAccess', _3 => _3.enableHTTPTimings]) && createdSpan) {
addHTTPTimings(createdSpan);
}
});
}
}
/**
* Creates a temporary observer to listen to the next fetch/xhr resourcing timings,
* so that when timings hit their per-browser limit they don't need to be removed.
*
* @param span A span that has yet to be finished, must contain `url` on data.
*/
function addHTTPTimings(span) {
const url = span.data.url;
const observer = new PerformanceObserver(list => {
const entries = list.getEntries() ;
entries.forEach(entry => {
if ((entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') && entry.name.endsWith(url)) {
const spanData = resourceTimingEntryToSpanData(entry);
spanData.forEach(data => span.setData(...data));
observer.disconnect();
}
});
});
observer.observe({
entryTypes: ['resource'],
});
}
function resourceTimingEntryToSpanData(resourceTiming) {
const version = resourceTiming.nextHopProtocol.split('/')[1] || 'none';
const timingSpanData = [];
if (version) {
timingSpanData.push(['network.protocol.version', version]);
}
if (!browserPerformanceTimeOrigin) {
return timingSpanData;
}
return [
...timingSpanData,
['http.request.connect_start', (browserPerformanceTimeOrigin + resourceTiming.connectStart) / 1000],
['http.request.request_start', (browserPerformanceTimeOrigin + resourceTiming.requestStart) / 1000],
['http.request.response_start', (browserPerformanceTimeOrigin + resourceTiming.responseStart) / 1000],
];
}
/**
* A function that determines whether to attach tracing headers to a request.
* This was extracted from `instrumentOutgoingRequests` to make it easier to test shouldAttachHeaders.
* We only export this fuction for testing purposes.
*/
function shouldAttachHeaders(url, tracePropagationTargets) {
return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS);
}
/**
* Create and track fetch request spans
*
* @returns Span if a span was created, otherwise void.
*/
function fetchCallback(
handlerData,
shouldCreateSpan,
shouldAttachHeaders,
spans,
) {
if (!hasTracingEnabled() || !(handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url))) {
return;
}
if (handlerData.endTimestamp) {
const spanId = handlerData.fetchData.__span;
if (!spanId) return;
const span = spans[spanId];
if (span) {
if (handlerData.response) {
// TODO (kmclb) remove this once types PR goes through
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
span.setHttpStatus(handlerData.response.status);
const contentLength =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length');
const contentLengthNum = parseInt(contentLength);
if (contentLengthNum > 0) {
span.setData('http.response_content_length', contentLengthNum);
}
} else if (handlerData.error) {
span.setStatus('internal_error');
}
span.finish();
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete spans[spanId];
}
return;
}
const currentSpan = getCurrentHub().getScope().getSpan();
const activeTransaction = currentSpan && currentSpan.transaction;
if (currentSpan && activeTransaction) {
const { method, url } = handlerData.fetchData;
const span = currentSpan.startChild({
data: {
url,
type: 'fetch',
'http.method': method,
},
description: `${method} ${url}`,
op: 'http.client',
});
handlerData.fetchData.__span = span.spanId;
spans[span.spanId] = span;
const request = handlerData.args[0];
// In case the user hasn't set the second argument of a fetch call we default it to `{}`.
handlerData.args[1] = handlerData.args[1] || {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options = handlerData.args[1];
if (shouldAttachHeaders(handlerData.fetchData.url)) {
options.headers = addTracingHeadersToFetchRequest(
request,
activeTransaction.getDynamicSamplingContext(),
span,
options,
);
}
return span;
}
}
/**
* Adds sentry-trace and baggage headers to the various forms of fetch headers
*/
function addTracingHeadersToFetchRequest(
request, // unknown is actually type Request but we can't export DOM types from this package,
dynamicSamplingContext,
span,
options
,
) {
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
const sentryTraceHeader = span.toTraceparent();
const headers =
typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request ).headers : options.headers;
if (!headers) {
return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader };
} else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) {
const newHeaders = new Headers(headers );
newHeaders.append('sentry-trace', sentryTraceHeader);
if (sentryBaggageHeader) {
// If the same header is appended multiple times the browser will merge the values into a single request header.
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
newHeaders.append(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
}
return newHeaders ;
} else if (Array.isArray(headers)) {
const newHeaders = [...headers, ['sentry-trace', sentryTraceHeader]];
if (sentryBaggageHeader) {
// If there are multiple entries with the same key, the browser will merge the values into a single request header.
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
newHeaders.push([BAGGAGE_HEADER_NAME, sentryBaggageHeader]);
}
return newHeaders ;
} else {
const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined;
const newBaggageHeaders = [];
if (Array.isArray(existingBaggageHeader)) {
newBaggageHeaders.push(...existingBaggageHeader);
} else if (existingBaggageHeader) {
newBaggageHeaders.push(existingBaggageHeader);
}
if (sentryBaggageHeader) {
newBaggageHeaders.push(sentryBaggageHeader);
}
return {
...(headers ),
'sentry-trace': sentryTraceHeader,
baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined,
};
}
}
/**
* Create and track xhr request spans
*
* @returns Span if a span was created, otherwise void.
*/
function xhrCallback(
handlerData,
shouldCreateSpan,
shouldAttachHeaders,
spans,
) {
const xhr = handlerData.xhr;
const sentryXhrData = xhr && xhr[SENTRY_XHR_DATA_KEY];
if (
!hasTracingEnabled() ||
(xhr && xhr.__sentry_own_request__) ||
!(xhr && sentryXhrData && shouldCreateSpan(sentryXhrData.url))
) {
return;
}
// check first if the request has finished and is tracked by an existing span which should now end
if (handlerData.endTimestamp) {
const spanId = xhr.__sentry_xhr_span_id__;
if (!spanId) return;
const span = spans[spanId];
if (span) {
span.setHttpStatus(sentryXhrData.status_code);
span.finish();
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete spans[spanId];
}
return;
}
const currentSpan = getCurrentHub().getScope().getSpan();
const activeTransaction = currentSpan && currentSpan.transaction;
if (currentSpan && activeTransaction) {
const span = currentSpan.startChild({
data: {
...sentryXhrData.data,
type: 'xhr',
'http.method': sentryXhrData.method,
url: sentryXhrData.url,
},
description: `${sentryXhrData.method} ${sentryXhrData.url}`,
op: 'http.client',
});
xhr.__sentry_xhr_span_id__ = span.spanId;
spans[xhr.__sentry_xhr_span_id__] = span;
if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) {
try {
xhr.setRequestHeader('sentry-trace', span.toTraceparent());
const dynamicSamplingContext = activeTransaction.getDynamicSamplingContext();
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
if (sentryBaggageHeader) {
// From MDN: "If this method is called several times with the same header, the values are merged into one single request header."
// We can therefore simply set a baggage header without checking what was there before
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
}
} catch (_) {
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
}
}
return span;
}
}
export { DEFAULT_TRACE_PROPAGATION_TARGETS, addTracingHeadersToFetchRequest, defaultRequestInstrumentationOptions, instrumentOutgoingRequests, shouldAttachHeaders };
//# sourceMappingURL=request.js.map

View File

@@ -0,0 +1,64 @@
import { logger, browserPerformanceTimeOrigin, addInstrumentationHandler } from '@sentry/utils';
import { WINDOW } from './types.js';
/**
* Default function implementing pageload and navigation transactions
*/
function instrumentRoutingWithDefaults(
customStartTransaction,
startTransactionOnPageLoad = true,
startTransactionOnLocationChange = true,
) {
if (!WINDOW || !WINDOW.location) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('Could not initialize routing instrumentation due to invalid location');
return;
}
let startingUrl = WINDOW.location.href;
let activeTransaction;
if (startTransactionOnPageLoad) {
activeTransaction = customStartTransaction({
name: WINDOW.location.pathname,
// pageload should always start at timeOrigin (and needs to be in s, not ms)
startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
op: 'pageload',
metadata: { source: 'url' },
});
}
if (startTransactionOnLocationChange) {
addInstrumentationHandler('history', ({ to, from }) => {
/**
* This early return is there to account for some cases where a navigation transaction starts right after
* long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't
* create an uneccessary navigation transaction.
*
* This was hard to duplicate, but this behavior stopped as soon as this fix was applied. This issue might also
* only be caused in certain development environments where the usage of a hot module reloader is causing
* errors.
*/
if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
startingUrl = undefined;
return;
}
if (from !== to) {
startingUrl = undefined;
if (activeTransaction) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`[Tracing] Finishing current transaction with op: ${activeTransaction.op}`);
// If there's an open transaction on the scope, we need to finish it before creating an new one.
activeTransaction.finish();
}
activeTransaction = customStartTransaction({
name: WINDOW.location.pathname,
op: 'navigation',
metadata: { source: 'url' },
});
}
});
}
}
export { instrumentRoutingWithDefaults };
//# sourceMappingURL=router.js.map

View File

@@ -0,0 +1,6 @@
import { GLOBAL_OBJ } from '@sentry/utils';
const WINDOW = GLOBAL_OBJ ;
export { WINDOW };
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1,105 @@
import { bindReporter } from './lib/bindReporter.js';
import { initMetric } from './lib/initMetric.js';
import { observe } from './lib/observe.js';
import { onHidden } from './lib/onHidden.js';
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Calculates the [CLS](https://web.dev/cls/) value for the current page and
* calls the `callback` function once the value is ready to be reported, along
* with all `layout-shift` performance entries that were used in the metric
* value calculation. The reported value is a `double` (corresponding to a
* [layout shift score](https://web.dev/cls/#layout-shift-score)).
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called as soon as the value is initially
* determined as well as any time the value changes throughout the page
* lifespan.
*
* _**Important:** CLS should be continually monitored for changes throughout
* the entire lifespan of a page—including if the user returns to the page after
* it's been hidden/backgrounded. However, since browsers often [will not fire
* additional callbacks once the user has backgrounded a
* page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
* `callback` is always called when the page's visibility state changes to
* hidden. As a result, the `callback` function might be called multiple times
* during the same page load._
*/
const onCLS = (onReport) => {
const metric = initMetric('CLS', 0);
let report;
let sessionValue = 0;
let sessionEntries = [];
// const handleEntries = (entries: Metric['entries']) => {
const handleEntries = (entries) => {
entries.forEach(entry => {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// If the entry occurred less than 1 second after the previous entry and
// less than 5 seconds after the first entry in the session, include the
// entry in the current session. Otherwise, start a new session.
if (
sessionValue &&
sessionEntries.length !== 0 &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
// If the current session value is larger than the current CLS value,
// update CLS and the entries contributing to it.
if (sessionValue > metric.value) {
metric.value = sessionValue;
metric.entries = sessionEntries;
if (report) {
report();
}
}
}
});
};
const po = observe('layout-shift', handleEntries);
if (po) {
report = bindReporter(onReport, metric);
const stopListening = () => {
handleEntries(po.takeRecords() );
report(true);
};
onHidden(stopListening);
return stopListening;
}
return;
};
export { onCLS };
//# sourceMappingURL=getCLS.js.map

View File

@@ -0,0 +1,63 @@
import { bindReporter } from './lib/bindReporter.js';
import { getVisibilityWatcher } from './lib/getVisibilityWatcher.js';
import { initMetric } from './lib/initMetric.js';
import { observe } from './lib/observe.js';
import { onHidden } from './lib/onHidden.js';
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Calculates the [FID](https://web.dev/fid/) value for the current page and
* calls the `callback` function once the value is ready, along with the
* relevant `first-input` performance entry used to determine the value. The
* reported value is a `DOMHighResTimeStamp`.
*
* _**Important:** since FID is only reported after the user interacts with the
* page, it's possible that it will not be reported for some page loads._
*/
const onFID = (onReport) => {
const visibilityWatcher = getVisibilityWatcher();
const metric = initMetric('FID');
// eslint-disable-next-line prefer-const
let report;
const handleEntry = (entry) => {
// Only report if the page wasn't hidden prior to the first input.
if (entry.startTime < visibilityWatcher.firstHiddenTime) {
metric.value = entry.processingStart - entry.startTime;
metric.entries.push(entry);
report(true);
}
};
const handleEntries = (entries) => {
(entries ).forEach(handleEntry);
};
const po = observe('first-input', handleEntries);
report = bindReporter(onReport, metric);
if (po) {
onHidden(() => {
handleEntries(po.takeRecords() );
po.disconnect();
}, true);
}
};
export { onFID };
//# sourceMappingURL=getFID.js.map

View File

@@ -0,0 +1,85 @@
import { bindReporter } from './lib/bindReporter.js';
import { getActivationStart } from './lib/getActivationStart.js';
import { getVisibilityWatcher } from './lib/getVisibilityWatcher.js';
import { initMetric } from './lib/initMetric.js';
import { observe } from './lib/observe.js';
import { onHidden } from './lib/onHidden.js';
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const reportedMetricIDs = {};
/**
* Calculates the [LCP](https://web.dev/lcp/) value for the current page and
* calls the `callback` function once the value is ready (along with the
* relevant `largest-contentful-paint` performance entry used to determine the
* value). The reported value is a `DOMHighResTimeStamp`.
*/
const onLCP = (onReport) => {
const visibilityWatcher = getVisibilityWatcher();
const metric = initMetric('LCP');
let report;
const handleEntries = (entries) => {
const lastEntry = entries[entries.length - 1] ;
if (lastEntry) {
// The startTime attribute returns the value of the renderTime if it is
// not 0, and the value of the loadTime otherwise. The activationStart
// reference is used because LCP should be relative to page activation
// rather than navigation start if the page was prerendered.
const value = Math.max(lastEntry.startTime - getActivationStart(), 0);
// Only report if the page wasn't hidden prior to LCP.
if (value < visibilityWatcher.firstHiddenTime) {
metric.value = value;
metric.entries = [lastEntry];
report();
}
}
};
const po = observe('largest-contentful-paint', handleEntries);
if (po) {
report = bindReporter(onReport, metric);
const stopListening = () => {
if (!reportedMetricIDs[metric.id]) {
handleEntries(po.takeRecords() );
po.disconnect();
reportedMetricIDs[metric.id] = true;
report(true);
}
};
// Stop listening after input. Note: while scrolling is an input that
// stop LCP observation, it's unreliable since it can be programmatically
// generated. See: https://github.com/GoogleChrome/web-vitals/issues/75
['keydown', 'click'].forEach(type => {
addEventListener(type, stopListening, { once: true, capture: true });
});
onHidden(stopListening, true);
return stopListening;
}
return;
};
export { onLCP };
//# sourceMappingURL=getLCP.js.map

View File

@@ -0,0 +1,28 @@
const bindReporter = (
callback,
metric,
reportAllChanges,
) => {
let prevValue;
let delta;
return (forceReport) => {
if (metric.value >= 0) {
if (forceReport || reportAllChanges) {
delta = metric.value - (prevValue || 0);
// Report the metric if there's a non-zero delta or if no previous
// value exists (which can happen in the case of the document becoming
// hidden when the metric value is 0).
// See: https://github.com/GoogleChrome/web-vitals/issues/14
if (delta || prevValue === undefined) {
prevValue = metric.value;
metric.delta = delta;
callback(metric);
}
}
}
};
};
export { bindReporter };
//# sourceMappingURL=bindReporter.js.map

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Performantly generate a unique, 30-char string by combining a version
* number, the current timestamp with a 13-digit number integer.
* @return {string}
*/
const generateUniqueID = () => {
return `v3-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
};
export { generateUniqueID };
//# sourceMappingURL=generateUniqueID.js.map

View File

@@ -0,0 +1,25 @@
import { getNavigationEntry } from './getNavigationEntry.js';
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const getActivationStart = () => {
const navEntry = getNavigationEntry();
return (navEntry && navEntry.activationStart) || 0;
};
export { getActivationStart };
//# sourceMappingURL=getActivationStart.js.map

View File

@@ -0,0 +1,53 @@
import { WINDOW } from '../../types.js';
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const getNavigationEntryFromPerformanceTiming = () => {
// eslint-disable-next-line deprecation/deprecation
const timing = WINDOW.performance.timing;
// eslint-disable-next-line deprecation/deprecation
const type = WINDOW.performance.navigation.type;
const navigationEntry = {
entryType: 'navigation',
startTime: 0,
type: type == 2 ? 'back_forward' : type === 1 ? 'reload' : 'navigate',
};
for (const key in timing) {
if (key !== 'navigationStart' && key !== 'toJSON') {
// eslint-disable-next-line deprecation/deprecation
navigationEntry[key] = Math.max((timing[key ] ) - timing.navigationStart, 0);
}
}
return navigationEntry ;
};
const getNavigationEntry = () => {
if (WINDOW.__WEB_VITALS_POLYFILL__) {
return (
WINDOW.performance &&
((performance.getEntriesByType && performance.getEntriesByType('navigation')[0]) ||
getNavigationEntryFromPerformanceTiming())
);
} else {
return WINDOW.performance && performance.getEntriesByType && performance.getEntriesByType('navigation')[0];
}
};
export { getNavigationEntry };
//# sourceMappingURL=getNavigationEntry.js.map

View File

@@ -0,0 +1,54 @@
import { WINDOW } from '../../types.js';
import { onHidden } from './onHidden.js';
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
let firstHiddenTime = -1;
const initHiddenTime = () => {
// If the document is hidden and not prerendering, assume it was always
// hidden and the page was loaded in the background.
return WINDOW.document.visibilityState === 'hidden' && !WINDOW.document.prerendering ? 0 : Infinity;
};
const trackChanges = () => {
// Update the time if/when the document becomes hidden.
onHidden(({ timeStamp }) => {
firstHiddenTime = timeStamp;
}, true);
};
const getVisibilityWatcher = (
) => {
if (firstHiddenTime < 0) {
// If the document is hidden when this code runs, assume it was hidden
// since navigation start. This isn't a perfect heuristic, but it's the
// best we can do until an API is available to support querying past
// visibilityState.
firstHiddenTime = initHiddenTime();
trackChanges();
}
return {
get firstHiddenTime() {
return firstHiddenTime;
},
};
};
export { getVisibilityWatcher };
//# sourceMappingURL=getVisibilityWatcher.js.map

View File

@@ -0,0 +1,46 @@
import { WINDOW } from '../../types.js';
import { generateUniqueID } from './generateUniqueID.js';
import { getActivationStart } from './getActivationStart.js';
import { getNavigationEntry } from './getNavigationEntry.js';
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const initMetric = (name, value) => {
const navEntry = getNavigationEntry();
let navigationType = 'navigate';
if (navEntry) {
if (WINDOW.document.prerendering || getActivationStart() > 0) {
navigationType = 'prerender';
} else {
navigationType = navEntry.type.replace(/_/g, '-') ;
}
}
return {
name,
value: typeof value === 'undefined' ? -1 : value,
rating: 'good', // Will be updated if the value changes.
delta: 0,
entries: [],
id: generateUniqueID(),
navigationType,
};
};
export { initMetric };
//# sourceMappingURL=initMetric.js.map

View File

@@ -0,0 +1,37 @@
/**
* Takes a performance entry type and a callback function, and creates a
* `PerformanceObserver` instance that will observe the specified entry type
* with buffering enabled and call the callback _for each entry_.
*
* This function also feature-detects entry support and wraps the logic in a
* try/catch to avoid errors in unsupporting browsers.
*/
const observe = (
type,
callback,
opts,
) => {
try {
if (PerformanceObserver.supportedEntryTypes.includes(type)) {
const po = new PerformanceObserver(list => {
callback(list.getEntries() );
});
po.observe(
Object.assign(
{
type,
buffered: true,
},
opts || {},
) ,
);
return po;
}
} catch (e) {
// Do nothing.
}
return;
};
export { observe };
//# sourceMappingURL=observe.js.map

View File

@@ -0,0 +1,36 @@
import { WINDOW } from '../../types.js';
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const onHidden = (cb, once) => {
const onHiddenOrPageHide = (event) => {
if (event.type === 'pagehide' || WINDOW.document.visibilityState === 'hidden') {
cb(event);
if (once) {
removeEventListener('visibilitychange', onHiddenOrPageHide, true);
removeEventListener('pagehide', onHiddenOrPageHide, true);
}
}
};
addEventListener('visibilitychange', onHiddenOrPageHide, true);
// Some browsers have buggy implementations of visibilitychange,
// so we use pagehide in addition, just to be safe.
addEventListener('pagehide', onHiddenOrPageHide, true);
};
export { onHidden };
//# sourceMappingURL=onHidden.js.map