This commit is contained in:
MarSeventh
2024-07-19 23:26:06 +08:00
commit 4e0c55d1f9
1401 changed files with 69819 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
import { getActiveTransaction, spanToJSON } from '@sentry/core';
import { logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../common/debug-build.js';
import { WINDOW } from './types.js';
/**
* Add a listener that cancels and finishes a transaction when the global
* document is hidden.
*/
function registerBackgroundTabDetection() {
if (WINDOW.document) {
WINDOW.document.addEventListener('visibilitychange', () => {
// eslint-disable-next-line deprecation/deprecation
const activeTransaction = getActiveTransaction() ;
if (WINDOW.document.hidden && activeTransaction) {
const statusType = 'cancelled';
const { op, status } = spanToJSON(activeTransaction);
DEBUG_BUILD &&
logger.log(`[Tracing] Transaction: ${statusType} -> since tab moved to the background, op: ${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 (!status) {
activeTransaction.setStatus(statusType);
}
// TODO: Can we rewrite this to an attribute?
// eslint-disable-next-line deprecation/deprecation
activeTransaction.setTag('visibilitychange', 'document.hidden');
activeTransaction.end();
}
});
} else {
DEBUG_BUILD && 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 @@
{"version":3,"file":"backgroundtab.js","sources":["../../../src/browser/backgroundtab.ts"],"sourcesContent":["import type { IdleTransaction, SpanStatusType } from '@sentry/core';\nimport { getActiveTransaction, spanToJSON } from '@sentry/core';\nimport { logger } from '@sentry/utils';\n\nimport { DEBUG_BUILD } from '../common/debug-build';\nimport { WINDOW } from './types';\n\n/**\n * Add a listener that cancels and finishes a transaction when the global\n * document is hidden.\n */\nexport function registerBackgroundTabDetection(): void {\n if (WINDOW.document) {\n WINDOW.document.addEventListener('visibilitychange', () => {\n // eslint-disable-next-line deprecation/deprecation\n const activeTransaction = getActiveTransaction() as IdleTransaction;\n if (WINDOW.document!.hidden && activeTransaction) {\n const statusType: SpanStatusType = 'cancelled';\n\n const { op, status } = spanToJSON(activeTransaction);\n\n DEBUG_BUILD &&\n logger.log(`[Tracing] Transaction: ${statusType} -> since tab moved to the background, op: ${op}`);\n // We should not set status if it is already set, this prevent important statuses like\n // error or data loss from being overwritten on transaction.\n if (!status) {\n activeTransaction.setStatus(statusType);\n }\n // TODO: Can we rewrite this to an attribute?\n // eslint-disable-next-line deprecation/deprecation\n activeTransaction.setTag('visibilitychange', 'document.hidden');\n activeTransaction.end();\n }\n });\n } else {\n DEBUG_BUILD && logger.warn('[Tracing] Could not set up background tab detection due to lack of global document');\n }\n}\n"],"names":[],"mappings":";;;;;AAOA;AACA;AACA;AACA;AACO,SAAS,8BAA8B,GAAS;AACvD,EAAE,IAAI,MAAM,CAAC,QAAQ,EAAE;AACvB,IAAI,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,MAAM;AAC/D;AACA,MAAM,MAAM,iBAAA,GAAoB,oBAAoB,EAAG,EAAA;AACvD,MAAM,IAAI,MAAM,CAAC,QAAQ,CAAE,MAAA,IAAU,iBAAiB,EAAE;AACxD,QAAQ,MAAM,UAAU,GAAmB,WAAW,CAAA;AACtD;AACA,QAAQ,MAAM,EAAE,EAAE,EAAE,MAAA,KAAW,UAAU,CAAC,iBAAiB,CAAC,CAAA;AAC5D;AACA,QAAQ,WAAY;AACpB,UAAU,MAAM,CAAC,GAAG,CAAC,CAAC,uBAAuB,EAAE,UAAU,CAAC,2CAA2C,EAAE,EAAE,CAAC,CAAA,CAAA,CAAA;AACA;AACA;AACA,QAAA,IAAA,CAAA,MAAA,EAAA;AACA,UAAA,iBAAA,CAAA,SAAA,CAAA,UAAA,CAAA,CAAA;AACA,SAAA;AACA;AACA;AACA,QAAA,iBAAA,CAAA,MAAA,CAAA,kBAAA,EAAA,iBAAA,CAAA,CAAA;AACA,QAAA,iBAAA,CAAA,GAAA,EAAA,CAAA;AACA,OAAA;AACA,KAAA,CAAA,CAAA;AACA,GAAA,MAAA;AACA,IAAA,WAAA,IAAA,MAAA,CAAA,IAAA,CAAA,oFAAA,CAAA,CAAA;AACA,GAAA;AACA;;;;"}

View File

@@ -0,0 +1,506 @@
import { TRACING_DEFAULTS, addTracingExtensions, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getCurrentHub, startIdleTransaction, getActiveTransaction, getClient, getCurrentScope } from '@sentry/core';
import { logger, browserPerformanceTimeOrigin, addHistoryInstrumentationHandler, propagationContextFromHeaders, getDomElement } from '@sentry/utils';
import { DEBUG_BUILD } from '../common/debug-build.js';
import { registerBackgroundTabDetection } from './backgroundtab.js';
import { addPerformanceInstrumentationHandler } from './instrument.js';
import { startTrackingWebVitals, startTrackingINP, startTrackingLongTasks, startTrackingInteractions, addPerformanceEntries } from './metrics/index.js';
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request.js';
import { WINDOW } from './types.js';
const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
/** Options for Browser Tracing integration */
const DEFAULT_BROWSER_TRACING_OPTIONS = {
...TRACING_DEFAULTS,
instrumentNavigation: true,
instrumentPageLoad: true,
markBackgroundSpan: true,
enableLongTask: true,
enableInp: false,
interactionsSampleRate: 1,
_experiments: {},
...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.
*
* We explicitly export the proper type here, as this has to be extended in some cases.
*/
const browserTracingIntegration = ((_options = {}) => {
const _hasSetTracePropagationTargets = DEBUG_BUILD
? !!(
// eslint-disable-next-line deprecation/deprecation
(_options.tracePropagationTargets || _options.tracingOrigins)
)
: false;
addTracingExtensions();
// 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.tracePropagationTargets && _options.tracingOrigins) {
// eslint-disable-next-line deprecation/deprecation
_options.tracePropagationTargets = _options.tracingOrigins;
}
const options = {
...DEFAULT_BROWSER_TRACING_OPTIONS,
..._options,
};
const _collectWebVitals = startTrackingWebVitals();
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
const interactionIdToRouteNameMapping = {};
if (options.enableInp) {
startTrackingINP(interactionIdToRouteNameMapping, options.interactionsSampleRate);
}
if (options.enableLongTask) {
startTrackingLongTasks();
}
if (options._experiments.enableInteractions) {
startTrackingInteractions();
}
const latestRoute
= {
name: undefined,
context: undefined,
};
/** Create routing idle transaction. */
function _createRouteTransaction(context) {
// eslint-disable-next-line deprecation/deprecation
const hub = getCurrentHub();
const { beforeStartSpan, idleTimeout, finalTimeout, heartbeatInterval } = options;
const isPageloadTransaction = context.op === 'pageload';
let expandedContext;
if (isPageloadTransaction) {
const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : '';
const baggage = isPageloadTransaction ? getMetaContent('baggage') : undefined;
const { traceId, dsc, parentSpanId, sampled } = propagationContextFromHeaders(sentryTrace, baggage);
expandedContext = {
traceId,
parentSpanId,
parentSampled: sampled,
...context,
metadata: {
// eslint-disable-next-line deprecation/deprecation
...context.metadata,
dynamicSamplingContext: dsc,
},
trimEnd: true,
};
} else {
expandedContext = {
trimEnd: true,
...context,
};
}
const finalContext = beforeStartSpan ? beforeStartSpan(expandedContext) : expandedContext;
// If `beforeStartSpan` set a custom name, record that fact
// eslint-disable-next-line deprecation/deprecation
finalContext.metadata =
finalContext.name !== expandedContext.name
? // eslint-disable-next-line deprecation/deprecation
{ ...finalContext.metadata, source: 'custom' }
: // eslint-disable-next-line deprecation/deprecation
finalContext.metadata;
latestRoute.name = finalContext.name;
latestRoute.context = finalContext;
if (finalContext.sampled === false) {
DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
}
DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`);
const { location } = WINDOW;
const idleTransaction = startIdleTransaction(
hub,
finalContext,
idleTimeout,
finalTimeout,
true,
{ location }, // for use in the tracesSampler
heartbeatInterval,
isPageloadTransaction, // should wait for finish signal if it's a pageload transaction
);
if (isPageloadTransaction && WINDOW.document) {
WINDOW.document.addEventListener('readystatechange', () => {
if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
idleTransaction.sendAutoFinishSignal();
}
});
if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
idleTransaction.sendAutoFinishSignal();
}
}
idleTransaction.registerBeforeFinishCallback(transaction => {
_collectWebVitals();
addPerformanceEntries(transaction);
});
return idleTransaction ;
}
return {
name: BROWSER_TRACING_INTEGRATION_ID,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setupOnce: () => {},
afterAllSetup(client) {
const clientOptions = client.getOptions();
const { markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, _experiments } =
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.
// eslint-disable-next-line deprecation/deprecation
const tracePropagationTargets = clientOptionsTracePropagationTargets || options.tracePropagationTargets;
if (DEBUG_BUILD && _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.',
);
}
let activeSpan;
let startingUrl = WINDOW.location && WINDOW.location.href;
if (client.on) {
client.on('startNavigationSpan', (context) => {
if (activeSpan) {
DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`);
// If there's an open transaction on the scope, we need to finish it before creating an new one.
activeSpan.end();
}
activeSpan = _createRouteTransaction({
op: 'navigation',
...context,
});
});
client.on('startPageLoadSpan', (context) => {
if (activeSpan) {
DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`);
// If there's an open transaction on the scope, we need to finish it before creating an new one.
activeSpan.end();
}
activeSpan = _createRouteTransaction({
op: 'pageload',
...context,
});
});
}
if (options.instrumentPageLoad && client.emit && WINDOW.location) {
const context = {
name: WINDOW.location.pathname,
// pageload should always start at timeOrigin (and needs to be in s, not ms)
startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
origin: 'auto.pageload.browser',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
};
startBrowserTracingPageLoadSpan(client, context);
}
if (options.instrumentNavigation && client.emit && WINDOW.location) {
addHistoryInstrumentationHandler(({ 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;
const context = {
name: WINDOW.location.pathname,
origin: 'auto.navigation.browser',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
};
startBrowserTracingNavigationSpan(client, context);
}
});
}
if (markBackgroundSpan) {
registerBackgroundTabDetection();
}
if (_experiments.enableInteractions) {
registerInteractionListener(options, latestRoute);
}
if (options.enableInp) {
registerInpInteractionListener(interactionIdToRouteNameMapping, latestRoute);
}
instrumentOutgoingRequests({
traceFetch,
traceXHR,
tracePropagationTargets,
shouldCreateSpanForRequest,
enableHTTPTimings,
});
},
// TODO v8: Remove this again
// This is private API that we use to fix converted BrowserTracing integrations in Next.js & SvelteKit
options,
};
}) ;
/**
* Manually start a page load span.
* This will only do something if the BrowserTracing integration has been setup.
*/
function startBrowserTracingPageLoadSpan(client, spanOptions) {
if (!client.emit) {
return;
}
client.emit('startPageLoadSpan', spanOptions);
const span = getActiveSpan();
const op = span && spanToJSON(span).op;
return op === 'pageload' ? span : undefined;
}
/**
* Manually start a navigation span.
* This will only do something if the BrowserTracing integration has been setup.
*/
function startBrowserTracingNavigationSpan(client, spanOptions) {
if (!client.emit) {
return;
}
client.emit('startNavigationSpan', spanOptions);
const span = getActiveSpan();
const op = span && spanToJSON(span).op;
return op === 'navigation' ? span : undefined;
}
/** 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') : undefined;
}
/** Start listener for interaction transactions */
function registerInteractionListener(
options,
latestRoute
,
) {
let inflightInteractionTransaction;
const registerInteractionTransaction = () => {
const { idleTimeout, finalTimeout, heartbeatInterval } = options;
const op = 'ui.action.click';
// eslint-disable-next-line deprecation/deprecation
const currentTransaction = getActiveTransaction();
if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) {
DEBUG_BUILD &&
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.end();
inflightInteractionTransaction = undefined;
}
if (!latestRoute.name) {
DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
return undefined;
}
const { location } = WINDOW;
const context = {
name: latestRoute.name,
op,
trimEnd: true,
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.context ? getSource(latestRoute.context) : 'url',
},
};
inflightInteractionTransaction = startIdleTransaction(
// eslint-disable-next-line deprecation/deprecation
getCurrentHub(),
context,
idleTimeout,
finalTimeout,
true,
{ location }, // for use in the tracesSampler
heartbeatInterval,
);
};
['click'].forEach(type => {
if (WINDOW.document) {
addEventListener(type, registerInteractionTransaction, { once: false, capture: true });
}
});
}
function isPerformanceEventTiming(entry) {
return 'duration' in entry;
}
/** We store up to 10 interaction candidates max to cap memory usage. This is the same cap as getINP from web-vitals */
const MAX_INTERACTIONS = 10;
/** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
function registerInpInteractionListener(
interactionIdToRouteNameMapping,
latestRoute
,
) {
const handleEntries = ({ entries }) => {
const client = getClient();
// We need to get the replay, user, and activeTransaction from the current scope
// so that we can associate replay id, profile id, and a user display to the span
const replay =
client !== undefined && client.getIntegrationByName !== undefined
? (client.getIntegrationByName('Replay') )
: undefined;
const replayId = replay !== undefined ? replay.getReplayId() : undefined;
// eslint-disable-next-line deprecation/deprecation
const activeTransaction = getActiveTransaction();
const currentScope = getCurrentScope();
const user = currentScope !== undefined ? currentScope.getUser() : undefined;
entries.forEach(entry => {
if (isPerformanceEventTiming(entry)) {
const interactionId = entry.interactionId;
if (interactionId === undefined) {
return;
}
const existingInteraction = interactionIdToRouteNameMapping[interactionId];
const duration = entry.duration;
const startTime = entry.startTime;
const keys = Object.keys(interactionIdToRouteNameMapping);
const minInteractionId =
keys.length > 0
? keys.reduce((a, b) => {
return interactionIdToRouteNameMapping[a].duration < interactionIdToRouteNameMapping[b].duration
? a
: b;
})
: undefined;
// For a first input event to be considered, we must check that an interaction event does not already exist with the same duration and start time.
// This is also checked in the web-vitals library.
if (entry.entryType === 'first-input') {
const matchingEntry = keys
.map(key => interactionIdToRouteNameMapping[key])
.some(interaction => {
return interaction.duration === duration && interaction.startTime === startTime;
});
if (matchingEntry) {
return;
}
}
// Interactions with an id of 0 and are not first-input are not valid.
if (!interactionId) {
return;
}
// If the interaction already exists, we want to use the duration of the longest entry, since that is what the INP metric uses.
if (existingInteraction) {
existingInteraction.duration = Math.max(existingInteraction.duration, duration);
} else if (
keys.length < MAX_INTERACTIONS ||
minInteractionId === undefined ||
duration > interactionIdToRouteNameMapping[minInteractionId].duration
) {
// If the interaction does not exist, we want to add it to the mapping if there is space, or if the duration is longer than the shortest entry.
const routeName = latestRoute.name;
const parentContext = latestRoute.context;
if (routeName && parentContext) {
if (minInteractionId && Object.keys(interactionIdToRouteNameMapping).length >= MAX_INTERACTIONS) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete interactionIdToRouteNameMapping[minInteractionId];
}
interactionIdToRouteNameMapping[interactionId] = {
routeName,
duration,
parentContext,
user,
activeTransaction,
replayId,
startTime,
};
}
}
}
});
};
addPerformanceInstrumentationHandler('event', handleEntries);
addPerformanceInstrumentationHandler('first-input', handleEntries);
}
function getSource(context) {
const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
// eslint-disable-next-line deprecation/deprecation
const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
// eslint-disable-next-line deprecation/deprecation
const sourceFromMetadata = context.metadata && context.metadata.source;
return sourceFromAttributes || sourceFromData || sourceFromMetadata;
}
export { BROWSER_TRACING_INTEGRATION_ID, browserTracingIntegration, getMetaContent, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan };
//# sourceMappingURL=browserTracingIntegration.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,457 @@
import { TRACING_DEFAULTS, addTracingExtensions, startIdleTransaction, getActiveTransaction, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getClient, getCurrentScope } from '@sentry/core';
import { logger, propagationContextFromHeaders, getDomElement } from '@sentry/utils';
import { DEBUG_BUILD } from '../common/debug-build.js';
import { registerBackgroundTabDetection } from './backgroundtab.js';
import { addPerformanceInstrumentationHandler } from './instrument.js';
import { startTrackingWebVitals, startTrackingINP, 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,
enableInp: false,
interactionsSampleRate: 1,
_experiments: {},
...defaultRequestInstrumentationOptions,
};
/** We store up to 10 interaction candidates max to cap memory usage. This is the same cap as getINP from web-vitals */
const MAX_INTERACTIONS = 10;
/**
* 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.
*
* @deprecated Use `browserTracingIntegration()` instead.
*/
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
*/
// eslint-disable-next-line deprecation/deprecation
constructor(_options) {
this.name = BROWSER_TRACING_INTEGRATION_ID;
this._hasSetTracePropagationTargets = false;
addTracingExtensions();
if (DEBUG_BUILD) {
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();
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
this._interactionIdToRouteNameMapping = {};
if (this.options.enableInp) {
startTrackingINP(this._interactionIdToRouteNameMapping, this.options.interactionsSampleRate);
}
if (this.options.enableLongTask) {
startTrackingLongTasks();
}
if (this.options._experiments.enableInteractions) {
startTrackingInteractions();
}
this._latestRoute = {
name: undefined,
context: undefined,
};
}
/**
* @inheritDoc
*/
// eslint-disable-next-line deprecation/deprecation
setupOnce(_, getCurrentHub) {
this._getCurrentHub = getCurrentHub;
const hub = getCurrentHub();
// eslint-disable-next-line deprecation/deprecation
const client = hub.getClient();
const clientOptions = client && client.getOptions();
const {
routingInstrumentation: instrumentRouting,
startTransactionOnLocationChange,
startTransactionOnPageLoad,
markBackgroundTransactions,
traceFetch,
traceXHR,
shouldCreateSpanForRequest,
enableHTTPTimings,
_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.
// eslint-disable-next-line deprecation/deprecation
const tracePropagationTargets = clientOptionsTracePropagationTargets || this.options.tracePropagationTargets;
if (DEBUG_BUILD && 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();
}
if (this.options.enableInp) {
this._registerInpInteractionListener();
}
instrumentOutgoingRequests({
traceFetch,
traceXHR,
tracePropagationTargets,
shouldCreateSpanForRequest,
enableHTTPTimings,
});
}
/** Create routing idle transaction. */
_createRouteTransaction(context) {
if (!this._getCurrentHub) {
DEBUG_BUILD &&
logger.warn(`[Tracing] Did not create ${context.op} transaction because _getCurrentHub is invalid.`);
return undefined;
}
const hub = this._getCurrentHub();
const { beforeNavigate, idleTimeout, finalTimeout, heartbeatInterval } = this.options;
const isPageloadTransaction = context.op === 'pageload';
let expandedContext;
if (isPageloadTransaction) {
const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : '';
const baggage = isPageloadTransaction ? getMetaContent('baggage') : undefined;
const { traceId, dsc, parentSpanId, sampled } = propagationContextFromHeaders(sentryTrace, baggage);
expandedContext = {
traceId,
parentSpanId,
parentSampled: sampled,
...context,
metadata: {
// eslint-disable-next-line deprecation/deprecation
...context.metadata,
dynamicSamplingContext: dsc,
},
trimEnd: true,
};
} else {
expandedContext = {
trimEnd: true,
...context,
};
}
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
// eslint-disable-next-line deprecation/deprecation
finalContext.metadata =
finalContext.name !== expandedContext.name
? // eslint-disable-next-line deprecation/deprecation
{ ...finalContext.metadata, source: 'custom' }
: // eslint-disable-next-line deprecation/deprecation
finalContext.metadata;
this._latestRoute.name = finalContext.name;
this._latestRoute.context = finalContext;
// eslint-disable-next-line deprecation/deprecation
if (finalContext.sampled === false) {
DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
}
DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`);
const { location } = WINDOW;
const idleTransaction = startIdleTransaction(
hub,
finalContext,
idleTimeout,
finalTimeout,
true,
{ location }, // for use in the tracesSampler
heartbeatInterval,
isPageloadTransaction, // should wait for finish signal if it's a pageload transaction
);
if (isPageloadTransaction) {
if (WINDOW.document) {
WINDOW.document.addEventListener('readystatechange', () => {
if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
idleTransaction.sendAutoFinishSignal();
}
});
if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
idleTransaction.sendAutoFinishSignal();
}
}
}
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';
// eslint-disable-next-line deprecation/deprecation
const currentTransaction = getActiveTransaction();
if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) {
DEBUG_BUILD &&
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.end();
inflightInteractionTransaction = undefined;
}
if (!this._getCurrentHub) {
DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _getCurrentHub is invalid.`);
return undefined;
}
if (!this._latestRoute.name) {
DEBUG_BUILD && 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._latestRoute.name,
op,
trimEnd: true,
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: this._latestRoute.context
? getSource(this._latestRoute.context)
: 'url',
},
};
inflightInteractionTransaction = startIdleTransaction(
hub,
context,
idleTimeout,
finalTimeout,
true,
{ location }, // for use in the tracesSampler
heartbeatInterval,
);
};
['click'].forEach(type => {
if (WINDOW.document) {
addEventListener(type, registerInteractionTransaction, { once: false, capture: true });
}
});
}
/** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
_registerInpInteractionListener() {
const handleEntries = ({ entries }) => {
const client = getClient();
// We need to get the replay, user, and activeTransaction from the current scope
// so that we can associate replay id, profile id, and a user display to the span
const replay =
client !== undefined && client.getIntegrationByName !== undefined
? (client.getIntegrationByName('Replay') )
: undefined;
const replayId = replay !== undefined ? replay.getReplayId() : undefined;
// eslint-disable-next-line deprecation/deprecation
const activeTransaction = getActiveTransaction();
const currentScope = getCurrentScope();
const user = currentScope !== undefined ? currentScope.getUser() : undefined;
entries.forEach(entry => {
if (isPerformanceEventTiming(entry)) {
const interactionId = entry.interactionId;
if (interactionId === undefined) {
return;
}
const existingInteraction = this._interactionIdToRouteNameMapping[interactionId];
const duration = entry.duration;
const startTime = entry.startTime;
const keys = Object.keys(this._interactionIdToRouteNameMapping);
const minInteractionId =
keys.length > 0
? keys.reduce((a, b) => {
return this._interactionIdToRouteNameMapping[a].duration <
this._interactionIdToRouteNameMapping[b].duration
? a
: b;
})
: undefined;
// For a first input event to be considered, we must check that an interaction event does not already exist with the same duration and start time.
// This is also checked in the web-vitals library.
if (entry.entryType === 'first-input') {
const matchingEntry = keys
.map(key => this._interactionIdToRouteNameMapping[key])
.some(interaction => {
return interaction.duration === duration && interaction.startTime === startTime;
});
if (matchingEntry) {
return;
}
}
// Interactions with an id of 0 and are not first-input are not valid.
if (!interactionId) {
return;
}
// If the interaction already exists, we want to use the duration of the longest entry, since that is what the INP metric uses.
if (existingInteraction) {
existingInteraction.duration = Math.max(existingInteraction.duration, duration);
} else if (
keys.length < MAX_INTERACTIONS ||
minInteractionId === undefined ||
duration > this._interactionIdToRouteNameMapping[minInteractionId].duration
) {
// If the interaction does not exist, we want to add it to the mapping if there is space, or if the duration is longer than the shortest entry.
const routeName = this._latestRoute.name;
const parentContext = this._latestRoute.context;
if (routeName && parentContext) {
if (minInteractionId && Object.keys(this._interactionIdToRouteNameMapping).length >= MAX_INTERACTIONS) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this._interactionIdToRouteNameMapping[minInteractionId];
}
this._interactionIdToRouteNameMapping[interactionId] = {
routeName,
duration,
parentContext,
user,
activeTransaction,
replayId,
startTime,
};
}
}
}
});
};
addPerformanceInstrumentationHandler('event', handleEntries);
addPerformanceInstrumentationHandler('first-input', handleEntries);
}
}
/** 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') : undefined;
}
function getSource(context) {
const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
// eslint-disable-next-line deprecation/deprecation
const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
// eslint-disable-next-line deprecation/deprecation
const sourceFromMetadata = context.metadata && context.metadata.source;
return sourceFromAttributes || sourceFromData || sourceFromMetadata;
}
function isPerformanceEventTiming(entry) {
return 'duration' in entry;
}
export { BROWSER_TRACING_INTEGRATION_ID, BrowserTracing, getMetaContent };
//# sourceMappingURL=browsertracing.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,230 @@
import { logger, getFunctionName } from '@sentry/utils';
import { DEBUG_BUILD } from '../common/debug-build.js';
import { onCLS } from './web-vitals/getCLS.js';
import { onFID } from './web-vitals/getFID.js';
import { onINP } from './web-vitals/getINP.js';
import { onLCP } from './web-vitals/getLCP.js';
import { observe } from './web-vitals/lib/observe.js';
import { onTTFB } from './web-vitals/onTTFB.js';
const handlers = {};
const instrumented = {};
let _previousCls;
let _previousFid;
let _previousLcp;
let _previousTtfb;
let _previousInp;
/**
* Add a callback that will be triggered when a CLS metric is available.
* Returns a cleanup callback which can be called to remove the instrumentation handler.
*
* Pass `stopOnCallback = true` to stop listening for CLS when the cleanup callback is called.
* This will lead to the CLS being finalized and frozen.
*/
function addClsInstrumentationHandler(
callback,
stopOnCallback = false,
) {
return addMetricObserver('cls', callback, instrumentCls, _previousCls, stopOnCallback);
}
/**
* Add a callback that will be triggered when a LCP metric is available.
* Returns a cleanup callback which can be called to remove the instrumentation handler.
*
* Pass `stopOnCallback = true` to stop listening for LCP when the cleanup callback is called.
* This will lead to the LCP being finalized and frozen.
*/
function addLcpInstrumentationHandler(
callback,
stopOnCallback = false,
) {
return addMetricObserver('lcp', callback, instrumentLcp, _previousLcp, stopOnCallback);
}
/**
* Add a callback that will be triggered when a FID metric is available.
*/
function addTtfbInstrumentationHandler(callback) {
return addMetricObserver('ttfb', callback, instrumentTtfb, _previousTtfb);
}
/**
* Add a callback that will be triggered when a FID metric is available.
* Returns a cleanup callback which can be called to remove the instrumentation handler.
*/
function addFidInstrumentationHandler(callback) {
return addMetricObserver('fid', callback, instrumentFid, _previousFid);
}
/**
* Add a callback that will be triggered when a INP metric is available.
* Returns a cleanup callback which can be called to remove the instrumentation handler.
*/
function addInpInstrumentationHandler(
callback,
) {
return addMetricObserver('inp', callback, instrumentInp, _previousInp);
}
/**
* Add a callback that will be triggered when a performance observer is triggered,
* and receives the entries of the observer.
* Returns a cleanup callback which can be called to remove the instrumentation handler.
*/
function addPerformanceInstrumentationHandler(
type,
callback,
) {
addHandler(type, callback);
if (!instrumented[type]) {
instrumentPerformanceObserver(type);
instrumented[type] = true;
}
return getCleanupCallback(type, callback);
}
/** Trigger all handlers of a given type. */
function triggerHandlers(type, data) {
const typeHandlers = handlers[type];
if (!typeHandlers || !typeHandlers.length) {
return;
}
for (const handler of typeHandlers) {
try {
handler(data);
} catch (e) {
DEBUG_BUILD &&
logger.error(
`Error while triggering instrumentation handler.\nType: ${type}\nName: ${getFunctionName(handler)}\nError:`,
e,
);
}
}
}
function instrumentCls() {
return onCLS(
metric => {
triggerHandlers('cls', {
metric,
});
_previousCls = metric;
},
{ reportAllChanges: true },
);
}
function instrumentFid() {
return onFID(metric => {
triggerHandlers('fid', {
metric,
});
_previousFid = metric;
});
}
function instrumentLcp() {
return onLCP(metric => {
triggerHandlers('lcp', {
metric,
});
_previousLcp = metric;
});
}
function instrumentTtfb() {
return onTTFB(metric => {
triggerHandlers('ttfb', {
metric,
});
_previousTtfb = metric;
});
}
function instrumentInp() {
return onINP(metric => {
triggerHandlers('inp', {
metric,
});
_previousInp = metric;
});
}
function addMetricObserver(
type,
callback,
instrumentFn,
previousValue,
stopOnCallback = false,
) {
addHandler(type, callback);
let stopListening;
if (!instrumented[type]) {
stopListening = instrumentFn();
instrumented[type] = true;
}
if (previousValue) {
callback({ metric: previousValue });
}
return getCleanupCallback(type, callback, stopOnCallback ? stopListening : undefined);
}
function instrumentPerformanceObserver(type) {
const options = {};
// Special per-type options we want to use
if (type === 'event') {
options.durationThreshold = 0;
}
observe(
type,
entries => {
triggerHandlers(type, { entries });
},
options,
);
}
function addHandler(type, handler) {
handlers[type] = handlers[type] || [];
(handlers[type] ).push(handler);
}
// Get a callback which can be called to remove the instrumentation handler
function getCleanupCallback(
type,
callback,
stopListening,
) {
return () => {
if (stopListening) {
stopListening();
}
const typeHandlers = handlers[type];
if (!typeHandlers) {
return;
}
const index = typeHandlers.indexOf(callback);
if (index !== -1) {
typeHandlers.splice(index, 1);
}
};
}
export { addClsInstrumentationHandler, addFidInstrumentationHandler, addInpInstrumentationHandler, addLcpInstrumentationHandler, addPerformanceInstrumentationHandler, addTtfbInstrumentationHandler };
//# sourceMappingURL=instrument.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,706 @@
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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
/**
* 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.
*
* Note: this will not be possible anymore in v8,
* unless we do some special handling for browser here...
*/
function _startChild(transaction, { startTimestamp, ...ctx }) {
// eslint-disable-next-line deprecation/deprecation
if (startTimestamp && transaction.startTimestamp > startTimestamp) {
// eslint-disable-next-line deprecation/deprecation
transaction.startTimestamp = startTimestamp;
}
// eslint-disable-next-line deprecation/deprecation
return transaction.startChild({
startTimestamp,
...ctx,
});
}
export { _startChild, isMeasurementValue };
//# sourceMappingURL=utils.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"utils.js","sources":["../../../../src/browser/metrics/utils.ts"],"sourcesContent":["import type { Transaction } from '@sentry/core';\nimport type { Span, SpanContext } from '@sentry/types';\n\n/**\n * Checks if a given value is a valid measurement value.\n */\nexport function isMeasurementValue(value: unknown): value is number {\n return typeof value === 'number' && isFinite(value);\n}\n\n/**\n * Helper function to start child on transactions. This function will make sure that the transaction will\n * use the start timestamp of the created child span if it is earlier than the transactions actual\n * start timestamp.\n *\n * Note: this will not be possible anymore in v8,\n * unless we do some special handling for browser here...\n */\nexport function _startChild(transaction: Transaction, { startTimestamp, ...ctx }: SpanContext): Span {\n // eslint-disable-next-line deprecation/deprecation\n if (startTimestamp && transaction.startTimestamp > startTimestamp) {\n // eslint-disable-next-line deprecation/deprecation\n transaction.startTimestamp = startTimestamp;\n }\n\n // eslint-disable-next-line deprecation/deprecation\n return transaction.startChild({\n startTimestamp,\n ...ctx,\n });\n}\n"],"names":[],"mappings":"AAGA;AACA;AACA;AACO,SAAS,kBAAkB,CAAC,KAAK,EAA4B;AACpE,EAAE,OAAO,OAAO,KAAM,KAAI,YAAY,QAAQ,CAAC,KAAK,CAAC,CAAA;AACrD,CAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,WAAW,CAAC,WAAW,EAAe,EAAE,cAAc,EAAE,GAAG,GAAA,EAAK,EAAqB;AACrG;AACA,EAAE,IAAI,cAAe,IAAG,WAAW,CAAC,cAAA,GAAiB,cAAc,EAAE;AACrE;AACA,IAAI,WAAW,CAAC,cAAe,GAAE,cAAc,CAAA;AAC/C,GAAE;AACF;AACA;AACA,EAAE,OAAO,WAAW,CAAC,UAAU,CAAC;AAChC,IAAI,cAAc;AAClB,IAAI,GAAG,GAAG;AACV,GAAG,CAAC,CAAA;AACJ;;;;"}

View File

@@ -0,0 +1,302 @@
import { spanToJSON, hasTracingEnabled, setHttpStatus, getCurrentScope, getIsolationScope, startInactiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getClient, spanToTraceHeader, getDynamicSamplingContextFromSpan, getDynamicSamplingContextFromClient } from '@sentry/core';
import { addFetchInstrumentationHandler, parseUrl, addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY, generateSentryTraceHeader, dynamicSamplingContextToSentryBaggageHeader, BAGGAGE_HEADER_NAME, browserPerformanceTimeOrigin, stringMatchesSomePattern } from '@sentry/utils';
import { instrumentFetchRequest } from '../common/fetch.js';
import { addPerformanceInstrumentationHandler } from './instrument.js';
import { WINDOW } from './types.js';
/* eslint-disable max-lines */
const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
/** Options for Request Instrumentation */
const defaultRequestInstrumentationOptions = {
traceFetch: true,
traceXHR: true,
enableHTTPTimings: true,
// TODO (v8): Remove this property
tracingOrigins: DEFAULT_TRACE_PROPAGATION_TARGETS,
tracePropagationTargets: DEFAULT_TRACE_PROPAGATION_TARGETS,
};
/** Registers span creators for xhr and fetch requests */
function instrumentOutgoingRequests(_options) {
const {
traceFetch,
traceXHR,
// eslint-disable-next-line deprecation/deprecation
tracePropagationTargets,
// eslint-disable-next-line deprecation/deprecation
tracingOrigins,
shouldCreateSpanForRequest,
enableHTTPTimings,
} = {
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) {
addFetchInstrumentationHandler(handlerData => {
const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.url);
const host = fullUrl ? parseUrl(fullUrl).host : undefined;
createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
});
}
if (enableHTTPTimings && createdSpan) {
addHTTPTimings(createdSpan);
}
});
}
if (traceXHR) {
addXhrInstrumentationHandler(handlerData => {
const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
if (enableHTTPTimings && createdSpan) {
addHTTPTimings(createdSpan);
}
});
}
}
function isPerformanceResourceTiming(entry) {
return (
entry.entryType === 'resource' &&
'initiatorType' in entry &&
typeof (entry ).nextHopProtocol === 'string' &&
(entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest')
);
}
/**
* 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 } = spanToJSON(span).data || {};
if (!url || typeof url !== 'string') {
return;
}
const cleanup = addPerformanceInstrumentationHandler('resource', ({ entries }) => {
entries.forEach(entry => {
if (isPerformanceResourceTiming(entry) && entry.name.endsWith(url)) {
const spanData = resourceTimingEntryToSpanData(entry);
spanData.forEach(data => span.setAttribute(...data));
// In the next tick, clean this handler up
// We have to wait here because otherwise this cleans itself up before it is fully done
setTimeout(cleanup);
}
});
});
}
/**
* Converts ALPN protocol ids to name and version.
*
* (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
* @param nextHopProtocol PerformanceResourceTiming.nextHopProtocol
*/
function extractNetworkProtocol(nextHopProtocol) {
let name = 'unknown';
let version = 'unknown';
let _name = '';
for (const char of nextHopProtocol) {
// http/1.1 etc.
if (char === '/') {
[name, version] = nextHopProtocol.split('/');
break;
}
// h2, h3 etc.
if (!isNaN(Number(char))) {
name = _name === 'h' ? 'http' : _name;
version = nextHopProtocol.split(_name)[1];
break;
}
_name += char;
}
if (_name === nextHopProtocol) {
// webrtc, ftp, etc.
name = _name;
}
return { name, version };
}
function getAbsoluteTime(time = 0) {
return ((browserPerformanceTimeOrigin || performance.timeOrigin) + time) / 1000;
}
function resourceTimingEntryToSpanData(resourceTiming) {
const { name, version } = extractNetworkProtocol(resourceTiming.nextHopProtocol);
const timingSpanData = [];
timingSpanData.push(['network.protocol.version', version], ['network.protocol.name', name]);
if (!browserPerformanceTimeOrigin) {
return timingSpanData;
}
return [
...timingSpanData,
['http.request.redirect_start', getAbsoluteTime(resourceTiming.redirectStart)],
['http.request.fetch_start', getAbsoluteTime(resourceTiming.fetchStart)],
['http.request.domain_lookup_start', getAbsoluteTime(resourceTiming.domainLookupStart)],
['http.request.domain_lookup_end', getAbsoluteTime(resourceTiming.domainLookupEnd)],
['http.request.connect_start', getAbsoluteTime(resourceTiming.connectStart)],
['http.request.secure_connection_start', getAbsoluteTime(resourceTiming.secureConnectionStart)],
['http.request.connection_end', getAbsoluteTime(resourceTiming.connectEnd)],
['http.request.request_start', getAbsoluteTime(resourceTiming.requestStart)],
['http.request.response_start', getAbsoluteTime(resourceTiming.responseStart)],
['http.request.response_end', getAbsoluteTime(resourceTiming.responseEnd)],
];
}
/**
* 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 xhr request spans
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
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__ || !sentryXhrData) {
return undefined;
}
const shouldCreateSpanResult = shouldCreateSpan(sentryXhrData.url);
// check first if the request has finished and is tracked by an existing span which should now end
if (handlerData.endTimestamp && shouldCreateSpanResult) {
const spanId = xhr.__sentry_xhr_span_id__;
if (!spanId) return;
const span = spans[spanId];
if (span && sentryXhrData.status_code !== undefined) {
setHttpStatus(span, sentryXhrData.status_code);
span.end();
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete spans[spanId];
}
return undefined;
}
const scope = getCurrentScope();
const isolationScope = getIsolationScope();
const fullUrl = getFullURL(sentryXhrData.url);
const host = fullUrl ? parseUrl(fullUrl).host : undefined;
const span = shouldCreateSpanResult
? startInactiveSpan({
name: `${sentryXhrData.method} ${sentryXhrData.url}`,
onlyIfParent: true,
attributes: {
type: 'xhr',
'http.method': sentryXhrData.method,
'http.url': fullUrl,
url: sentryXhrData.url,
'server.address': host,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser',
},
op: 'http.client',
})
: undefined;
if (span) {
xhr.__sentry_xhr_span_id__ = span.spanContext().spanId;
spans[xhr.__sentry_xhr_span_id__] = span;
}
const client = getClient();
if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url) && client) {
const { traceId, spanId, sampled, dsc } = {
...isolationScope.getPropagationContext(),
...scope.getPropagationContext(),
};
const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled);
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(
dsc ||
(span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client, scope)),
);
setHeaderOnXhr(xhr, sentryTraceHeader, sentryBaggageHeader);
}
return span;
}
function setHeaderOnXhr(
xhr,
sentryTraceHeader,
sentryBaggageHeader,
) {
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
xhr.setRequestHeader('sentry-trace', sentryTraceHeader);
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
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
}
} catch (_) {
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
}
}
function getFullURL(url) {
try {
// By adding a base URL to new URL(), this will also work for relative urls
// If `url` is a full URL, the base URL is ignored anyhow
const parsed = new URL(url, WINDOW.location.origin);
return parsed.href;
} catch (e) {
return undefined;
}
}
export { DEFAULT_TRACE_PROPAGATION_TARGETS, defaultRequestInstrumentationOptions, extractNetworkProtocol, instrumentOutgoingRequests, shouldAttachHeaders, xhrCallback };
//# sourceMappingURL=request.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,67 @@
import { logger, browserPerformanceTimeOrigin, addHistoryInstrumentationHandler } from '@sentry/utils';
import { DEBUG_BUILD } from '../common/debug-build.js';
import { WINDOW } from './types.js';
/**
* Default function implementing pageload and navigation transactions
*/
function instrumentRoutingWithDefaults(
customStartTransaction,
startTransactionOnPageLoad = true,
startTransactionOnLocationChange = true,
) {
if (!WINDOW || !WINDOW.location) {
DEBUG_BUILD && 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',
origin: 'auto.pageload.browser',
metadata: { source: 'url' },
});
}
if (startTransactionOnLocationChange) {
addHistoryInstrumentationHandler(({ 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) {
DEBUG_BUILD && 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.end();
}
activeTransaction = customStartTransaction({
name: WINDOW.location.pathname,
op: 'navigation',
origin: 'auto.navigation.browser',
metadata: { source: 'url' },
});
}
});
}
}
export { instrumentRoutingWithDefaults };
//# sourceMappingURL=router.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"router.js","sources":["../../../src/browser/router.ts"],"sourcesContent":["import type { Transaction, TransactionContext } from '@sentry/types';\nimport { addHistoryInstrumentationHandler, browserPerformanceTimeOrigin, logger } from '@sentry/utils';\n\nimport { DEBUG_BUILD } from '../common/debug-build';\nimport { WINDOW } from './types';\n\n/**\n * Default function implementing pageload and navigation transactions\n */\nexport function instrumentRoutingWithDefaults<T extends Transaction>(\n customStartTransaction: (context: TransactionContext) => T | undefined,\n startTransactionOnPageLoad: boolean = true,\n startTransactionOnLocationChange: boolean = true,\n): void {\n if (!WINDOW || !WINDOW.location) {\n DEBUG_BUILD && logger.warn('Could not initialize routing instrumentation due to invalid location');\n return;\n }\n\n let startingUrl: string | undefined = WINDOW.location.href;\n\n let activeTransaction: T | undefined;\n if (startTransactionOnPageLoad) {\n activeTransaction = customStartTransaction({\n name: WINDOW.location.pathname,\n // pageload should always start at timeOrigin (and needs to be in s, not ms)\n startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,\n op: 'pageload',\n origin: 'auto.pageload.browser',\n metadata: { source: 'url' },\n });\n }\n\n if (startTransactionOnLocationChange) {\n addHistoryInstrumentationHandler(({ to, from }) => {\n /**\n * This early return is there to account for some cases where a navigation transaction starts right after\n * long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't\n * create an uneccessary navigation transaction.\n *\n * This was hard to duplicate, but this behavior stopped as soon as this fix was applied. This issue might also\n * only be caused in certain development environments where the usage of a hot module reloader is causing\n * errors.\n */\n if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {\n startingUrl = undefined;\n return;\n }\n\n if (from !== to) {\n startingUrl = undefined;\n if (activeTransaction) {\n DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${activeTransaction.op}`);\n // If there's an open transaction on the scope, we need to finish it before creating an new one.\n activeTransaction.end();\n }\n activeTransaction = customStartTransaction({\n name: WINDOW.location.pathname,\n op: 'navigation',\n origin: 'auto.navigation.browser',\n metadata: { source: 'url' },\n });\n }\n });\n }\n}\n"],"names":[],"mappings":";;;;AAMA;AACA;AACA;AACO,SAAS,6BAA6B;AAC7C,EAAE,sBAAsB;AACxB,EAAE,0BAA0B,GAAY,IAAI;AAC5C,EAAE,gCAAgC,GAAY,IAAI;AAClD,EAAQ;AACR,EAAE,IAAI,CAAC,MAAA,IAAU,CAAC,MAAM,CAAC,QAAQ,EAAE;AACnC,IAAI,eAAe,MAAM,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAA;AACtG,IAAI,OAAM;AACV,GAAE;AACF;AACA,EAAE,IAAI,WAAW,GAAuB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAA;AAC5D;AACA,EAAE,IAAI,iBAAiB,CAAA;AACvB,EAAE,IAAI,0BAA0B,EAAE;AAClC,IAAI,iBAAA,GAAoB,sBAAsB,CAAC;AAC/C,MAAM,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;AACpC;AACA,MAAM,cAAc,EAAE,4BAA6B,GAAE,+BAA+B,IAAA,GAAO,SAAS;AACpG,MAAM,EAAE,EAAE,UAAU;AACpB,MAAM,MAAM,EAAE,uBAAuB;AACrC,MAAM,QAAQ,EAAE,EAAE,MAAM,EAAE,OAAO;AACjC,KAAK,CAAC,CAAA;AACN,GAAE;AACF;AACA,EAAE,IAAI,gCAAgC,EAAE;AACxC,IAAI,gCAAgC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAA,EAAM,KAAK;AACvD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,IAAI,IAAA,KAAS,SAAA,IAAa,WAAY,IAAG,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE;AAC/E,QAAQ,WAAA,GAAc,SAAS,CAAA;AAC/B,QAAQ,OAAM;AACd,OAAM;AACN;AACA,MAAM,IAAI,IAAK,KAAI,EAAE,EAAE;AACvB,QAAQ,WAAA,GAAc,SAAS,CAAA;AAC/B,QAAQ,IAAI,iBAAiB,EAAE;AAC/B,UAAU,WAAY,IAAG,MAAM,CAAC,GAAG,CAAC,CAAC,iDAAiD,EAAE,iBAAiB,CAAC,EAAE,CAAC,CAAA,CAAA,CAAA;AACA;AACA,UAAA,iBAAA,CAAA,GAAA,EAAA,CAAA;AACA,SAAA;AACA,QAAA,iBAAA,GAAA,sBAAA,CAAA;AACA,UAAA,IAAA,EAAA,MAAA,CAAA,QAAA,CAAA,QAAA;AACA,UAAA,EAAA,EAAA,YAAA;AACA,UAAA,MAAA,EAAA,yBAAA;AACA,UAAA,QAAA,EAAA,EAAA,MAAA,EAAA,KAAA,EAAA;AACA,SAAA,CAAA,CAAA;AACA,OAAA;AACA,KAAA,CAAA,CAAA;AACA,GAAA;AACA;;;;"}

View File

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

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sources":["../../../src/browser/types.ts"],"sourcesContent":["import { GLOBAL_OBJ } from '@sentry/utils';\n\nexport const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ &\n // document is not available in all browser environments (webworkers). We make it optional so you have to explicitly check for it\n Omit<Window, 'document'> &\n Partial<Pick<Window, 'document'>>;\n"],"names":[],"mappings":";;AAEO,MAAM,MAAO,GAAE,UAAW;;;;;;"}

View File

@@ -0,0 +1,108 @@
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,
options = {},
) => {
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, options.reportAllChanges);
const stopListening = () => {
handleEntries(po.takeRecords() );
report(true);
};
onHidden(stopListening);
return stopListening;
}
return;
};
export { onCLS };
//# sourceMappingURL=getCLS.js.map

File diff suppressed because one or more lines are too long

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 @@
{"version":3,"file":"getFID.js","sources":["../../../../src/browser/web-vitals/getFID.ts"],"sourcesContent":["/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { bindReporter } from './lib/bindReporter';\nimport { getVisibilityWatcher } from './lib/getVisibilityWatcher';\nimport { initMetric } from './lib/initMetric';\nimport { observe } from './lib/observe';\nimport { onHidden } from './lib/onHidden';\nimport type { FIDMetric, PerformanceEventTiming, ReportCallback } from './types';\n\n/**\n * Calculates the [FID](https://web.dev/fid/) value for the current page and\n * calls the `callback` function once the value is ready, along with the\n * relevant `first-input` performance entry used to determine the value. The\n * reported value is a `DOMHighResTimeStamp`.\n *\n * _**Important:** since FID is only reported after the user interacts with the\n * page, it's possible that it will not be reported for some page loads._\n */\nexport const onFID = (onReport: ReportCallback): void => {\n const visibilityWatcher = getVisibilityWatcher();\n const metric = initMetric('FID');\n // eslint-disable-next-line prefer-const\n let report: ReturnType<typeof bindReporter>;\n\n const handleEntry = (entry: PerformanceEventTiming): void => {\n // Only report if the page wasn't hidden prior to the first input.\n if (entry.startTime < visibilityWatcher.firstHiddenTime) {\n metric.value = entry.processingStart - entry.startTime;\n metric.entries.push(entry);\n report(true);\n }\n };\n\n const handleEntries = (entries: FIDMetric['entries']): void => {\n (entries as PerformanceEventTiming[]).forEach(handleEntry);\n };\n\n const po = observe('first-input', handleEntries);\n report = bindReporter(onReport, metric);\n\n if (po) {\n onHidden(() => {\n handleEntries(po.takeRecords() as FIDMetric['entries']);\n po.disconnect();\n }, true);\n }\n};\n"],"names":[],"mappings":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AASA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACa,MAAA,KAAA,GAAQ,CAAC,QAAQ,KAA2B;AACzD,EAAE,MAAM,iBAAA,GAAoB,oBAAoB,EAAE,CAAA;AAClD,EAAE,MAAM,MAAO,GAAE,UAAU,CAAC,KAAK,CAAC,CAAA;AAClC;AACA,EAAE,IAAI,MAAM,CAAA;AACZ;AACA,EAAE,MAAM,WAAA,GAAc,CAAC,KAAK,KAAmC;AAC/D;AACA,IAAI,IAAI,KAAK,CAAC,YAAY,iBAAiB,CAAC,eAAe,EAAE;AAC7D,MAAM,MAAM,CAAC,KAAA,GAAQ,KAAK,CAAC,eAAgB,GAAE,KAAK,CAAC,SAAS,CAAA;AAC5D,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAChC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;AAClB,KAAI;AACJ,GAAG,CAAA;AACH;AACA,EAAE,MAAM,aAAA,GAAgB,CAAC,OAAO,KAAiC;AACjE,IAAI,CAAC,OAAQ,GAA6B,OAAO,CAAC,WAAW,CAAC,CAAA;AAC9D,GAAG,CAAA;AACH;AACA,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,aAAa,CAAC,CAAA;AAClD,EAAE,SAAS,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;AACzC;AACA,EAAE,IAAI,EAAE,EAAE;AACV,IAAI,QAAQ,CAAC,MAAM;AACnB,MAAM,aAAa,CAAC,EAAE,CAAC,WAAW,IAA2B,CAAA;AAC7D,MAAM,EAAE,CAAC,UAAU,EAAE,CAAA;AACrB,KAAK,EAAE,IAAI,CAAC,CAAA;AACZ,GAAE;AACF;;;;"}

View File

@@ -0,0 +1,210 @@
import { bindReporter } from './lib/bindReporter.js';
import { initMetric } from './lib/initMetric.js';
import { observe } from './lib/observe.js';
import { onHidden } from './lib/onHidden.js';
import { initInteractionCountPolyfill, getInteractionCount } from './lib/polyfills/interactionCountPolyfill.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.
*/
/**
* Returns the interaction count since the last bfcache restore (or for the
* full page lifecycle if there were no bfcache restores).
*/
const getInteractionCountForNavigation = () => {
return getInteractionCount();
};
// To prevent unnecessary memory usage on pages with lots of interactions,
// store at most 10 of the longest interactions to consider as INP candidates.
const MAX_INTERACTIONS_TO_CONSIDER = 10;
// A list of longest interactions on the page (by latency) sorted so the
// longest one is first. The list is as most MAX_INTERACTIONS_TO_CONSIDER long.
const longestInteractionList = [];
// A mapping of longest interactions by their interaction ID.
// This is used for faster lookup.
const longestInteractionMap = {};
/**
* Takes a performance entry and adds it to the list of worst interactions
* if its duration is long enough to make it among the worst. If the
* entry is part of an existing interaction, it is merged and the latency
* and entries list is updated as needed.
*/
const processEntry = (entry) => {
// The least-long of the 10 longest interactions.
const minLongestInteraction = longestInteractionList[longestInteractionList.length - 1];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const existingInteraction = longestInteractionMap[entry.interactionId];
// Only process the entry if it's possibly one of the ten longest,
// or if it's part of an existing interaction.
if (
existingInteraction ||
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
entry.duration > minLongestInteraction.latency
) {
// If the interaction already exists, update it. Otherwise create one.
if (existingInteraction) {
existingInteraction.entries.push(entry);
existingInteraction.latency = Math.max(existingInteraction.latency, entry.duration);
} else {
const interaction = {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: entry.interactionId,
latency: entry.duration,
entries: [entry],
};
longestInteractionMap[interaction.id] = interaction;
longestInteractionList.push(interaction);
}
// Sort the entries by latency (descending) and keep only the top ten.
longestInteractionList.sort((a, b) => b.latency - a.latency);
longestInteractionList.splice(MAX_INTERACTIONS_TO_CONSIDER).forEach(i => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete longestInteractionMap[i.id];
});
}
};
/**
* Returns the estimated p98 longest interaction based on the stored
* interaction candidates and the interaction count for the current page.
*/
const estimateP98LongestInteraction = () => {
const candidateInteractionIndex = Math.min(
longestInteractionList.length - 1,
Math.floor(getInteractionCountForNavigation() / 50),
);
return longestInteractionList[candidateInteractionIndex];
};
/**
* Calculates the [INP](https://web.dev/responsiveness/) value for the current
* page and calls the `callback` function once the value is ready, along with
* the `event` performance entries reported for that interaction. The reported
* value is a `DOMHighResTimeStamp`.
*
* A custom `durationThreshold` configuration option can optionally be passed to
* control what `event-timing` entries are considered for INP reporting. The
* default threshold is `40`, which means INP scores of less than 40 are
* reported as 0. Note that this will not affect your 75th percentile INP value
* unless that value is also less than 40 (well below the recommended
* [good](https://web.dev/inp/#what-is-a-good-inp-score) threshold).
*
* 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:** INP 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 onINP = (onReport, opts) => {
// Set defaults
// eslint-disable-next-line no-param-reassign
opts = opts || {};
// https://web.dev/inp/#what's-a-%22good%22-inp-value
// const thresholds = [200, 500];
// TODO(philipwalton): remove once the polyfill is no longer needed.
initInteractionCountPolyfill();
const metric = initMetric('INP');
// eslint-disable-next-line prefer-const
let report;
const handleEntries = (entries) => {
entries.forEach(entry => {
if (entry.interactionId) {
processEntry(entry);
}
// Entries of type `first-input` don't currently have an `interactionId`,
// so to consider them in INP we have to first check that an existing
// entry doesn't match the `duration` and `startTime`.
// Note that this logic assumes that `event` entries are dispatched
// before `first-input` entries. This is true in Chrome but it is not
// true in Firefox; however, Firefox doesn't support interactionId, so
// it's not an issue at the moment.
// TODO(philipwalton): remove once crbug.com/1325826 is fixed.
if (entry.entryType === 'first-input') {
const noMatchingEntry = !longestInteractionList.some(interaction => {
return interaction.entries.some(prevEntry => {
return entry.duration === prevEntry.duration && entry.startTime === prevEntry.startTime;
});
});
if (noMatchingEntry) {
processEntry(entry);
}
}
});
const inp = estimateP98LongestInteraction();
if (inp && inp.latency !== metric.value) {
metric.value = inp.latency;
metric.entries = inp.entries;
report();
}
};
const po = observe('event', handleEntries, {
// Event Timing entries have their durations rounded to the nearest 8ms,
// so a duration of 40ms would be any event that spans 2.5 or more frames
// at 60Hz. This threshold is chosen to strike a balance between usefulness
// and performance. Running this callback for any interaction that spans
// just one or two frames is likely not worth the insight that could be
// gained.
durationThreshold: opts.durationThreshold || 40,
} );
report = bindReporter(onReport, metric, opts.reportAllChanges);
if (po) {
// Also observe entries of type `first-input`. This is useful in cases
// where the first interaction is less than the `durationThreshold`.
po.observe({ type: 'first-input', buffered: true });
onHidden(() => {
handleEntries(po.takeRecords() );
// If the interaction count shows that there were interactions but
// none were captured by the PerformanceObserver, report a latency of 0.
if (metric.value < 0 && getInteractionCountForNavigation() > 0) {
metric.value = 0;
metric.entries = [];
}
report(true);
});
}
};
export { onINP };
//# sourceMappingURL=getINP.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,88 @@
import { WINDOW } from '../types.js';
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 => {
if (WINDOW.document) {
addEventListener(type, stopListening, { once: true, capture: true });
}
});
onHidden(stopListening, true);
return stopListening;
}
return;
};
export { onLCP };
//# sourceMappingURL=getLCP.js.map

File diff suppressed because one or more lines are too long

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 @@
{"version":3,"file":"bindReporter.js","sources":["../../../../../src/browser/web-vitals/lib/bindReporter.ts"],"sourcesContent":["/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Metric, ReportCallback } from '../types';\n\nexport const bindReporter = (\n callback: ReportCallback,\n metric: Metric,\n reportAllChanges?: boolean,\n): ((forceReport?: boolean) => void) => {\n let prevValue: number;\n let delta: number;\n return (forceReport?: boolean) => {\n if (metric.value >= 0) {\n if (forceReport || reportAllChanges) {\n delta = metric.value - (prevValue || 0);\n\n // Report the metric if there's a non-zero delta or if no previous\n // value exists (which can happen in the case of the document becoming\n // hidden when the metric value is 0).\n // See: https://github.com/GoogleChrome/web-vitals/issues/14\n if (delta || prevValue === undefined) {\n prevValue = metric.value;\n metric.delta = delta;\n callback(metric);\n }\n }\n }\n };\n};\n"],"names":[],"mappings":"AAkBO,MAAM,eAAe;AAC5B,EAAE,QAAQ;AACV,EAAE,MAAM;AACR,EAAE,gBAAgB;AAClB,KAAwC;AACxC,EAAE,IAAI,SAAS,CAAA;AACf,EAAE,IAAI,KAAK,CAAA;AACX,EAAE,OAAO,CAAC,WAAW,KAAe;AACpC,IAAI,IAAI,MAAM,CAAC,KAAM,IAAG,CAAC,EAAE;AAC3B,MAAM,IAAI,WAAY,IAAG,gBAAgB,EAAE;AAC3C,QAAQ,KAAA,GAAQ,MAAM,CAAC,KAAA,IAAS,SAAA,IAAa,CAAC,CAAC,CAAA;AAC/C;AACA;AACA;AACA;AACA;AACA,QAAQ,IAAI,KAAA,IAAS,SAAU,KAAI,SAAS,EAAE;AAC9C,UAAU,SAAU,GAAE,MAAM,CAAC,KAAK,CAAA;AAClC,UAAU,MAAM,CAAC,KAAM,GAAE,KAAK,CAAA;AAC9B,UAAU,QAAQ,CAAC,MAAM,CAAC,CAAA;AAC1B,SAAQ;AACR,OAAM;AACN,KAAI;AACJ,GAAG,CAAA;AACH;;;;"}

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 @@
{"version":3,"file":"generateUniqueID.js","sources":["../../../../../src/browser/web-vitals/lib/generateUniqueID.ts"],"sourcesContent":["/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Performantly generate a unique, 30-char string by combining a version\n * number, the current timestamp with a 13-digit number integer.\n * @return {string}\n */\nexport const generateUniqueID = (): string => {\n return `v3-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;\n};\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACa,MAAA,gBAAA,GAAmB,MAAc;AAC9C,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAC,IAAK,IAAA,GAAO,CAAC,CAAC,CAAA,GAAI,IAAI,CAAC,CAAA,CAAA;AACA;;;;"}

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 @@
{"version":3,"file":"getActivationStart.js","sources":["../../../../../src/browser/web-vitals/lib/getActivationStart.ts"],"sourcesContent":["/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getNavigationEntry } from './getNavigationEntry';\n\nexport const getActivationStart = (): number => {\n const navEntry = getNavigationEntry();\n return (navEntry && navEntry.activationStart) || 0;\n};\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACa,MAAA,kBAAA,GAAqB,MAAc;AAChD,EAAE,MAAM,QAAA,GAAW,kBAAkB,EAAE,CAAA;AACvC,EAAE,OAAO,CAAC,QAAS,IAAG,QAAQ,CAAC,eAAe,KAAK,CAAC,CAAA;AACpD;;;;"}

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 @@
{"version":3,"file":"getNavigationEntry.js","sources":["../../../../../src/browser/web-vitals/lib/getNavigationEntry.ts"],"sourcesContent":["/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { WINDOW } from '../../types';\nimport type { NavigationTimingPolyfillEntry } from '../types';\n\nconst getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => {\n // eslint-disable-next-line deprecation/deprecation\n const timing = WINDOW.performance.timing;\n // eslint-disable-next-line deprecation/deprecation\n const type = WINDOW.performance.navigation.type;\n\n const navigationEntry: { [key: string]: number | string } = {\n entryType: 'navigation',\n startTime: 0,\n type: type == 2 ? 'back_forward' : type === 1 ? 'reload' : 'navigate',\n };\n\n for (const key in timing) {\n if (key !== 'navigationStart' && key !== 'toJSON') {\n // eslint-disable-next-line deprecation/deprecation\n navigationEntry[key] = Math.max((timing[key as keyof PerformanceTiming] as number) - timing.navigationStart, 0);\n }\n }\n return navigationEntry as unknown as NavigationTimingPolyfillEntry;\n};\n\nexport const getNavigationEntry = (): PerformanceNavigationTiming | NavigationTimingPolyfillEntry | undefined => {\n if (WINDOW.__WEB_VITALS_POLYFILL__) {\n return (\n WINDOW.performance &&\n ((performance.getEntriesByType && performance.getEntriesByType('navigation')[0]) ||\n getNavigationEntryFromPerformanceTiming())\n );\n } else {\n return WINDOW.performance && performance.getEntriesByType && performance.getEntriesByType('navigation')[0];\n }\n};\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAKA,MAAM,uCAAA,GAA0C,MAAqC;AACrF;AACA,EAAE,MAAM,MAAO,GAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAA;AAC1C;AACA,EAAE,MAAM,OAAO,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,IAAI,CAAA;AACjD;AACA,EAAE,MAAM,eAAe,GAAuC;AAC9D,IAAI,SAAS,EAAE,YAAY;AAC3B,IAAI,SAAS,EAAE,CAAC;AAChB,IAAI,IAAI,EAAE,IAAK,IAAG,IAAI,cAAA,GAAiB,IAAA,KAAS,CAAA,GAAI,QAAA,GAAW,UAAU;AACzE,GAAG,CAAA;AACH;AACA,EAAE,KAAK,MAAM,GAAI,IAAG,MAAM,EAAE;AAC5B,IAAI,IAAI,GAAI,KAAI,qBAAqB,GAAA,KAAQ,QAAQ,EAAE;AACvD;AACA,MAAM,eAAe,CAAC,GAAG,CAAA,GAAI,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAA,OAA6C,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC,CAAA;AACrH,KAAI;AACJ,GAAE;AACF,EAAE,OAAO,eAAgB,EAAA;AACzB,CAAC,CAAA;AACD;AACa,MAAA,kBAAA,GAAqB,MAA+E;AACjH,EAAE,IAAI,MAAM,CAAC,uBAAuB,EAAE;AACtC,IAAI;AACJ,MAAM,MAAM,CAAC,WAAY;AACzB,OAAO,CAAC,WAAW,CAAC,oBAAoB,WAAW,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AACrF,QAAQ,uCAAuC,EAAE,CAAA;AACjD,MAAK;AACL,SAAS;AACT,IAAI,OAAO,MAAM,CAAC,WAAY,IAAG,WAAW,CAAC,gBAAA,IAAoB,WAAW,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;AAC9G,GAAE;AACF;;;;"}

View File

@@ -0,0 +1,56 @@
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.
if (WINDOW.document && WINDOW.document.visibilityState) {
firstHiddenTime = 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.
initHiddenTime();
trackChanges();
}
return {
get firstHiddenTime() {
return firstHiddenTime;
},
};
};
export { getVisibilityWatcher };
//# sourceMappingURL=getVisibilityWatcher.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"getVisibilityWatcher.js","sources":["../../../../../src/browser/web-vitals/lib/getVisibilityWatcher.ts"],"sourcesContent":["/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { WINDOW } from '../../types';\nimport { onHidden } from './onHidden';\n\nlet firstHiddenTime = -1;\n\nconst initHiddenTime = (): void => {\n // If the document is hidden and not prerendering, assume it was always\n // hidden and the page was loaded in the background.\n if (WINDOW.document && WINDOW.document.visibilityState) {\n firstHiddenTime = WINDOW.document.visibilityState === 'hidden' && !WINDOW.document.prerendering ? 0 : Infinity;\n }\n};\n\nconst trackChanges = (): void => {\n // Update the time if/when the document becomes hidden.\n onHidden(({ timeStamp }) => {\n firstHiddenTime = timeStamp;\n }, true);\n};\n\nexport const getVisibilityWatcher = (): {\n readonly firstHiddenTime: number;\n} => {\n if (firstHiddenTime < 0) {\n // If the document is hidden when this code runs, assume it was hidden\n // since navigation start. This isn't a perfect heuristic, but it's the\n // best we can do until an API is available to support querying past\n // visibilityState.\n initHiddenTime();\n trackChanges();\n }\n return {\n get firstHiddenTime() {\n return firstHiddenTime;\n },\n };\n};\n"],"names":[],"mappings":";;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAIA;AACA,IAAI,eAAA,GAAkB,CAAC,CAAC,CAAA;AACxB;AACA,MAAM,cAAA,GAAiB,MAAY;AACnC;AACA;AACA,EAAE,IAAI,MAAM,CAAC,QAAA,IAAY,MAAM,CAAC,QAAQ,CAAC,eAAe,EAAE;AAC1D,IAAI,kBAAkB,MAAM,CAAC,QAAQ,CAAC,oBAAoB,QAAA,IAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAA,GAAI,QAAQ,CAAA;AAClH,GAAE;AACF,CAAC,CAAA;AACD;AACA,MAAM,YAAA,GAAe,MAAY;AACjC;AACA,EAAE,QAAQ,CAAC,CAAC,EAAE,SAAU,EAAC,KAAK;AAC9B,IAAI,eAAA,GAAkB,SAAS,CAAA;AAC/B,GAAG,EAAE,IAAI,CAAC,CAAA;AACV,CAAC,CAAA;AACD;AACO,MAAM,oBAAqB,GAAE;AAClC;AACF,KAAK;AACL,EAAE,IAAI,eAAgB,GAAE,CAAC,EAAE;AAC3B;AACA;AACA;AACA;AACA,IAAI,cAAc,EAAE,CAAA;AACpB,IAAI,YAAY,EAAE,CAAA;AAClB,GAAE;AACF,EAAE,OAAO;AACT,IAAI,IAAI,eAAe,GAAG;AAC1B,MAAM,OAAO,eAAe,CAAA;AAC5B,KAAK;AACL,GAAG,CAAA;AACH;;;;"}

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 && 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 @@
{"version":3,"file":"initMetric.js","sources":["../../../../../src/browser/web-vitals/lib/initMetric.ts"],"sourcesContent":["/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { WINDOW } from '../../types';\nimport type { Metric } from '../types';\nimport { generateUniqueID } from './generateUniqueID';\nimport { getActivationStart } from './getActivationStart';\nimport { getNavigationEntry } from './getNavigationEntry';\n\nexport const initMetric = (name: Metric['name'], value?: number): Metric => {\n const navEntry = getNavigationEntry();\n let navigationType: Metric['navigationType'] = 'navigate';\n\n if (navEntry) {\n if ((WINDOW.document && WINDOW.document.prerendering) || getActivationStart() > 0) {\n navigationType = 'prerender';\n } else {\n navigationType = navEntry.type.replace(/_/g, '-') as Metric['navigationType'];\n }\n }\n\n return {\n name,\n value: typeof value === 'undefined' ? -1 : value,\n rating: 'good', // Will be updated if the value changes.\n delta: 0,\n entries: [],\n id: generateUniqueID(),\n navigationType,\n };\n};\n"],"names":[],"mappings":";;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAOA;MACa,UAAW,GAAE,CAAC,IAAI,EAAkB,KAAK,KAAsB;AAC5E,EAAE,MAAM,QAAA,GAAW,kBAAkB,EAAE,CAAA;AACvC,EAAE,IAAI,cAAc,GAA6B,UAAU,CAAA;AAC3D;AACA,EAAE,IAAI,QAAQ,EAAE;AAChB,IAAI,IAAI,CAAC,MAAM,CAAC,QAAA,IAAY,MAAM,CAAC,QAAQ,CAAC,YAAY,KAAK,kBAAkB,EAAG,GAAE,CAAC,EAAE;AACvF,MAAM,cAAA,GAAiB,WAAW,CAAA;AAClC,WAAW;AACX,MAAM,cAAA,GAAiB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAE,EAAA;AACxD,KAAI;AACJ,GAAE;AACF;AACA,EAAE,OAAO;AACT,IAAI,IAAI;AACR,IAAI,KAAK,EAAE,OAAO,KAAM,KAAI,cAAc,CAAC,CAAE,GAAE,KAAK;AACpD,IAAI,MAAM,EAAE,MAAM;AAClB,IAAI,KAAK,EAAE,CAAC;AACZ,IAAI,OAAO,EAAE,EAAE;AACf,IAAI,EAAE,EAAE,gBAAgB,EAAE;AAC1B,IAAI,cAAc;AAClB,GAAG,CAAA;AACH;;;;"}

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 @@
{"version":3,"file":"observe.js","sources":["../../../../../src/browser/web-vitals/lib/observe.ts"],"sourcesContent":["/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry, PerformancePaintTiming } from '../types';\n\nexport interface PerformanceEntryHandler {\n (entry: PerformanceEntry): void;\n}\n\ninterface PerformanceEntryMap {\n event: PerformanceEventTiming[];\n paint: PerformancePaintTiming[];\n 'layout-shift': LayoutShift[];\n 'largest-contentful-paint': LargestContentfulPaint[];\n 'first-input': PerformanceEventTiming[] | FirstInputPolyfillEntry[];\n navigation: PerformanceNavigationTiming[] | NavigationTimingPolyfillEntry[];\n resource: PerformanceResourceTiming[];\n longtask: PerformanceEntry[];\n}\n\n/**\n * Takes a performance entry type and a callback function, and creates a\n * `PerformanceObserver` instance that will observe the specified entry type\n * with buffering enabled and call the callback _for each entry_.\n *\n * This function also feature-detects entry support and wraps the logic in a\n * try/catch to avoid errors in unsupporting browsers.\n */\nexport const observe = <K extends keyof PerformanceEntryMap>(\n type: K,\n callback: (entries: PerformanceEntryMap[K]) => void,\n opts?: PerformanceObserverInit,\n): PerformanceObserver | undefined => {\n try {\n if (PerformanceObserver.supportedEntryTypes.includes(type)) {\n const po = new PerformanceObserver(list => {\n callback(list.getEntries() as PerformanceEntryMap[K]);\n });\n po.observe(\n Object.assign(\n {\n type,\n buffered: true,\n },\n opts || {},\n ) as PerformanceObserverInit,\n );\n return po;\n }\n } catch (e) {\n // Do nothing.\n }\n return;\n};\n"],"names":[],"mappings":"AAiCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAM,UAAU;AACvB,EAAE,IAAI;AACN,EAAE,QAAQ;AACV,EAAE,IAAI;AACN,KAAsC;AACtC,EAAE,IAAI;AACN,IAAI,IAAI,mBAAmB,CAAC,mBAAmB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;AAChE,MAAM,MAAM,EAAG,GAAE,IAAI,mBAAmB,CAAC,QAAQ;AACjD,QAAQ,QAAQ,CAAC,IAAI,CAAC,UAAU,IAA6B,CAAA;AAC7D,OAAO,CAAC,CAAA;AACR,MAAM,EAAE,CAAC,OAAO;AAChB,QAAQ,MAAM,CAAC,MAAM;AACrB,UAAU;AACV,YAAY,IAAI;AAChB,YAAY,QAAQ,EAAE,IAAI;AAC1B,WAAW;AACX,UAAU,IAAA,IAAQ,EAAE;AACpB,SAAU;AACV,OAAO,CAAA;AACP,MAAM,OAAO,EAAE,CAAA;AACf,KAAI;AACJ,GAAI,CAAA,OAAO,CAAC,EAAE;AACd;AACA,GAAE;AACF,EAAE,OAAM;AACR;;;;"}

View File

@@ -0,0 +1,39 @@
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);
}
}
};
if (WINDOW.document) {
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

View File

@@ -0,0 +1 @@
{"version":3,"file":"onHidden.js","sources":["../../../../../src/browser/web-vitals/lib/onHidden.ts"],"sourcesContent":["/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { WINDOW } from '../../types';\n\nexport interface OnHiddenCallback {\n (event: Event): void;\n}\n\nexport const onHidden = (cb: OnHiddenCallback, once?: boolean): void => {\n const onHiddenOrPageHide = (event: Event): void => {\n if (event.type === 'pagehide' || WINDOW.document!.visibilityState === 'hidden') {\n cb(event);\n if (once) {\n removeEventListener('visibilitychange', onHiddenOrPageHide, true);\n removeEventListener('pagehide', onHiddenOrPageHide, true);\n }\n }\n };\n\n if (WINDOW.document) {\n addEventListener('visibilitychange', onHiddenOrPageHide, true);\n // Some browsers have buggy implementations of visibilitychange,\n // so we use pagehide in addition, just to be safe.\n addEventListener('pagehide', onHiddenOrPageHide, true);\n }\n};\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;MAQa,QAAS,GAAE,CAAC,EAAE,EAAoB,IAAI,KAAqB;AACxE,EAAE,MAAM,kBAAA,GAAqB,CAAC,KAAK,KAAkB;AACrD,IAAI,IAAI,KAAK,CAAC,SAAS,UAAA,IAAc,MAAM,CAAC,QAAQ,CAAE,eAAgB,KAAI,QAAQ,EAAE;AACpF,MAAM,EAAE,CAAC,KAAK,CAAC,CAAA;AACf,MAAM,IAAI,IAAI,EAAE;AAChB,QAAQ,mBAAmB,CAAC,kBAAkB,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAA;AACzE,QAAQ,mBAAmB,CAAC,UAAU,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAA;AACjE,OAAM;AACN,KAAI;AACJ,GAAG,CAAA;AACH;AACA,EAAE,IAAI,MAAM,CAAC,QAAQ,EAAE;AACvB,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAA;AAClE;AACA;AACA,IAAI,gBAAgB,CAAC,UAAU,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAA;AAC1D,GAAE;AACF;;;;"}

View File

@@ -0,0 +1,42 @@
import { observe } from '../observe.js';
let interactionCountEstimate = 0;
let minKnownInteractionId = Infinity;
let maxKnownInteractionId = 0;
const updateEstimate = (entries) => {
(entries ).forEach(e => {
if (e.interactionId) {
minKnownInteractionId = Math.min(minKnownInteractionId, e.interactionId);
maxKnownInteractionId = Math.max(maxKnownInteractionId, e.interactionId);
interactionCountEstimate = maxKnownInteractionId ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1 : 0;
}
});
};
let po;
/**
* Returns the `interactionCount` value using the native API (if available)
* or the polyfill estimate in this module.
*/
const getInteractionCount = () => {
return po ? interactionCountEstimate : performance.interactionCount || 0;
};
/**
* Feature detects native support or initializes the polyfill if needed.
*/
const initInteractionCountPolyfill = () => {
if ('interactionCount' in performance || po) return;
po = observe('event', updateEstimate, {
type: 'event',
buffered: true,
durationThreshold: 0,
} );
};
export { getInteractionCount, initInteractionCountPolyfill };
//# sourceMappingURL=interactionCountPolyfill.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"interactionCountPolyfill.js","sources":["../../../../../../src/browser/web-vitals/lib/polyfills/interactionCountPolyfill.ts"],"sourcesContent":["/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Metric } from '../../types';\nimport { observe } from '../observe';\n\ndeclare global {\n interface Performance {\n interactionCount: number;\n }\n}\n\nlet interactionCountEstimate = 0;\nlet minKnownInteractionId = Infinity;\nlet maxKnownInteractionId = 0;\n\nconst updateEstimate = (entries: Metric['entries']): void => {\n (entries as PerformanceEventTiming[]).forEach(e => {\n if (e.interactionId) {\n minKnownInteractionId = Math.min(minKnownInteractionId, e.interactionId);\n maxKnownInteractionId = Math.max(maxKnownInteractionId, e.interactionId);\n\n interactionCountEstimate = maxKnownInteractionId ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1 : 0;\n }\n });\n};\n\nlet po: PerformanceObserver | undefined;\n\n/**\n * Returns the `interactionCount` value using the native API (if available)\n * or the polyfill estimate in this module.\n */\nexport const getInteractionCount = (): number => {\n return po ? interactionCountEstimate : performance.interactionCount || 0;\n};\n\n/**\n * Feature detects native support or initializes the polyfill if needed.\n */\nexport const initInteractionCountPolyfill = (): void => {\n if ('interactionCount' in performance || po) return;\n\n po = observe('event', updateEstimate, {\n type: 'event',\n buffered: true,\n durationThreshold: 0,\n } as PerformanceObserverInit);\n};\n"],"names":[],"mappings":";;AAyBA,IAAI,wBAAA,GAA2B,CAAC,CAAA;AAChC,IAAI,qBAAA,GAAwB,QAAQ,CAAA;AACpC,IAAI,qBAAA,GAAwB,CAAC,CAAA;AAC7B;AACA,MAAM,cAAe,GAAE,CAAC,OAAO,KAA8B;AAC7D,EAAE,CAAC,OAAQ,GAA6B,OAAO,CAAC,KAAK;AACrD,IAAI,IAAI,CAAC,CAAC,aAAa,EAAE;AACzB,MAAM,qBAAA,GAAwB,IAAI,CAAC,GAAG,CAAC,qBAAqB,EAAE,CAAC,CAAC,aAAa,CAAC,CAAA;AAC9E,MAAM,qBAAA,GAAwB,IAAI,CAAC,GAAG,CAAC,qBAAqB,EAAE,CAAC,CAAC,aAAa,CAAC,CAAA;AAC9E;AACA,MAAM,wBAAyB,GAAE,qBAAsB,GAAE,CAAC,qBAAsB,GAAE,qBAAqB,IAAI,CAAA,GAAI,CAAA,GAAI,CAAC,CAAA;AACpH,KAAI;AACJ,GAAG,CAAC,CAAA;AACJ,CAAC,CAAA;AACD;AACA,IAAI,EAAE,CAAA;AACN;AACA;AACA;AACA;AACA;AACa,MAAA,mBAAA,GAAsB,MAAc;AACjD,EAAE,OAAO,KAAK,wBAAA,GAA2B,WAAW,CAAC,gBAAiB,IAAG,CAAC,CAAA;AAC1E,EAAC;AACD;AACA;AACA;AACA;AACa,MAAA,4BAAA,GAA+B,MAAY;AACxD,EAAE,IAAI,kBAAmB,IAAG,eAAe,EAAE,EAAE,OAAM;AACrD;AACA,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE;AACxC,IAAI,IAAI,EAAE,OAAO;AACjB,IAAI,QAAQ,EAAE,IAAI;AAClB,IAAI,iBAAiB,EAAE,CAAC;AACxB,KAA+B,CAAA;AAC/B;;;;"}

View File

@@ -0,0 +1,92 @@
import { WINDOW } from '../types.js';
import { bindReporter } from './lib/bindReporter.js';
import { getActivationStart } from './lib/getActivationStart.js';
import { getNavigationEntry } from './lib/getNavigationEntry.js';
import { initMetric } from './lib/initMetric.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.
*/
/**
* Runs in the next task after the page is done loading and/or prerendering.
* @param callback
*/
const whenReady = (callback) => {
if (!WINDOW.document) {
return;
}
if (WINDOW.document.prerendering) {
addEventListener('prerenderingchange', () => whenReady(callback), true);
} else if (WINDOW.document.readyState !== 'complete') {
addEventListener('load', () => whenReady(callback), true);
} else {
// Queue a task so the callback runs after `loadEventEnd`.
setTimeout(callback, 0);
}
};
/**
* Calculates the [TTFB](https://web.dev/time-to-first-byte/) value for the
* current page and calls the `callback` function once the page has loaded,
* along with the relevant `navigation` performance entry used to determine the
* value. The reported value is a `DOMHighResTimeStamp`.
*
* Note, this function waits until after the page is loaded to call `callback`
* in order to ensure all properties of the `navigation` entry are populated.
* This is useful if you want to report on other metrics exposed by the
* [Navigation Timing API](https://w3c.github.io/navigation-timing/). For
* example, the TTFB metric starts from the page's [time
* origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it
* includes time spent on DNS lookup, connection negotiation, network latency,
* and server processing time.
*/
const onTTFB = (onReport, opts) => {
// Set defaults
// eslint-disable-next-line no-param-reassign
opts = opts || {};
// https://web.dev/ttfb/#what-is-a-good-ttfb-score
// const thresholds = [800, 1800];
const metric = initMetric('TTFB');
const report = bindReporter(onReport, metric, opts.reportAllChanges);
whenReady(() => {
const navEntry = getNavigationEntry() ;
if (navEntry) {
// The activationStart reference is used because TTFB should be
// relative to page activation rather than navigation start if the
// page was prerendered. But in cases where `activationStart` occurs
// after the first byte is received, this time should be clamped at 0.
metric.value = Math.max(navEntry.responseStart - getActivationStart(), 0);
// In some cases the value reported is negative or is larger
// than the current page time. Ignore these cases:
// https://github.com/GoogleChrome/web-vitals/issues/137
// https://github.com/GoogleChrome/web-vitals/issues/162
if (metric.value < 0 || metric.value > performance.now()) return;
metric.entries = [navEntry];
report(true);
}
});
};
export { onTTFB };
//# sourceMappingURL=onTTFB.js.map

File diff suppressed because one or more lines are too long