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

View File

@@ -0,0 +1,9 @@
/**
* This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
*
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
*/
const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__);
export { DEBUG_BUILD };
//# sourceMappingURL=debug-build.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"debug-build.js","sources":["../../../src/common/debug-build.ts"],"sourcesContent":["declare const __DEBUG_BUILD__: boolean;\n\n/**\n * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.\n *\n * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.\n */\nexport const DEBUG_BUILD = __DEBUG_BUILD__;\n"],"names":[],"mappings":"AAEA;AACA;AACA;AACA;AACA;AACO,MAAM,WAAY,IAAE,OAAA,gBAAA,KAAA,WAAA,IAAA,gBAAA;;;;"}

View File

@@ -0,0 +1,188 @@
import { hasTracingEnabled, getCurrentScope, getClient, startInactiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getIsolationScope, spanToTraceHeader, getDynamicSamplingContextFromSpan, getDynamicSamplingContextFromClient, setHttpStatus } from '@sentry/core';
import { parseUrl, generateSentryTraceHeader, dynamicSamplingContextToSentryBaggageHeader, isInstanceOf, BAGGAGE_HEADER_NAME } from '@sentry/utils';
/**
* Create and track fetch request spans for usage in combination with `addInstrumentationHandler`.
*
* @returns Span if a span was created, otherwise void.
*/
function instrumentFetchRequest(
handlerData,
shouldCreateSpan,
shouldAttachHeaders,
spans,
spanOrigin = 'auto.http.browser',
) {
if (!hasTracingEnabled() || !handlerData.fetchData) {
return undefined;
}
const shouldCreateSpanResult = shouldCreateSpan(handlerData.fetchData.url);
if (handlerData.endTimestamp && shouldCreateSpanResult) {
const spanId = handlerData.fetchData.__span;
if (!spanId) return;
const span = spans[spanId];
if (span) {
endSpan(span, handlerData);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete spans[spanId];
}
return undefined;
}
const scope = getCurrentScope();
const client = getClient();
const { method, url } = handlerData.fetchData;
const fullUrl = getFullURL(url);
const host = fullUrl ? parseUrl(fullUrl).host : undefined;
const span = shouldCreateSpanResult
? startInactiveSpan({
name: `${method} ${url}`,
onlyIfParent: true,
attributes: {
url,
type: 'fetch',
'http.method': method,
'http.url': fullUrl,
'server.address': host,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin,
},
op: 'http.client',
})
: undefined;
if (span) {
handlerData.fetchData.__span = span.spanContext().spanId;
spans[span.spanContext().spanId] = span;
}
if (shouldAttachHeaders(handlerData.fetchData.url) && client) {
const request = handlerData.args[0];
// In case the user hasn't set the second argument of a fetch call we default it to `{}`.
handlerData.args[1] = handlerData.args[1] || {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options = handlerData.args[1];
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
options.headers = addTracingHeadersToFetchRequest(request, client, scope, options, span);
}
return span;
}
/**
* Adds sentry-trace and baggage headers to the various forms of fetch headers
*/
function addTracingHeadersToFetchRequest(
request, // unknown is actually type Request but we can't export DOM types from this package,
client,
scope,
options
,
requestSpan,
) {
// eslint-disable-next-line deprecation/deprecation
const span = requestSpan || scope.getSpan();
const isolationScope = getIsolationScope();
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)),
);
const headers =
options.headers ||
(typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request ).headers : undefined);
if (!headers) {
return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader };
} else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) {
const newHeaders = new Headers(headers );
newHeaders.append('sentry-trace', sentryTraceHeader);
if (sentryBaggageHeader) {
// If the same header is appended multiple times the browser will merge the values into a single request header.
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
newHeaders.append(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
}
return newHeaders ;
} else if (Array.isArray(headers)) {
const newHeaders = [...headers, ['sentry-trace', sentryTraceHeader]];
if (sentryBaggageHeader) {
// If there are multiple entries with the same key, the browser will merge the values into a single request header.
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
newHeaders.push([BAGGAGE_HEADER_NAME, sentryBaggageHeader]);
}
return newHeaders ;
} else {
const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined;
const newBaggageHeaders = [];
if (Array.isArray(existingBaggageHeader)) {
newBaggageHeaders.push(...existingBaggageHeader);
} else if (existingBaggageHeader) {
newBaggageHeaders.push(existingBaggageHeader);
}
if (sentryBaggageHeader) {
newBaggageHeaders.push(sentryBaggageHeader);
}
return {
...(headers ),
'sentry-trace': sentryTraceHeader,
baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined,
};
}
}
function getFullURL(url) {
try {
const parsed = new URL(url);
return parsed.href;
} catch (e) {
return undefined;
}
}
function endSpan(span, handlerData) {
if (handlerData.response) {
setHttpStatus(span, handlerData.response.status);
const contentLength =
handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length');
if (contentLength) {
const contentLengthNum = parseInt(contentLength);
if (contentLengthNum > 0) {
span.setAttribute('http.response_content_length', contentLengthNum);
}
}
} else if (handlerData.error) {
span.setStatus('internal_error');
}
span.end();
}
export { addTracingHeadersToFetchRequest, instrumentFetchRequest };
//# sourceMappingURL=fetch.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,69 @@
import { addTracingExtensions, getMainCarrier } from '@sentry/core';
import { isNodeEnv, loadModule, dynamicRequire } from '@sentry/utils';
/**
* @private
*/
function _autoloadDatabaseIntegrations() {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
return;
}
const packageToIntegrationMapping = {
mongodb() {
const integration = dynamicRequire(module, './node/integrations/mongo')
;
return new integration.Mongo();
},
mongoose() {
const integration = dynamicRequire(module, './node/integrations/mongo')
;
return new integration.Mongo();
},
mysql() {
const integration = dynamicRequire(module, './node/integrations/mysql')
;
return new integration.Mysql();
},
pg() {
const integration = dynamicRequire(module, './node/integrations/postgres')
;
return new integration.Postgres();
},
};
const mappedPackages = Object.keys(packageToIntegrationMapping)
.filter(moduleName => !!loadModule(moduleName))
.map(pkg => {
try {
return packageToIntegrationMapping[pkg]();
} catch (e) {
return undefined;
}
})
.filter(p => p) ;
if (mappedPackages.length > 0) {
carrier.__SENTRY__.integrations = [...(carrier.__SENTRY__.integrations || []), ...mappedPackages];
}
}
/**
* This patches the global object and injects the Tracing extensions methods
*/
function addExtensionMethods() {
addTracingExtensions();
// Detect and automatically load specified integrations.
if (isNodeEnv()) {
_autoloadDatabaseIntegrations();
}
}
export { addExtensionMethods };
//# sourceMappingURL=extensions.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"extensions.js","sources":["../../src/extensions.ts"],"sourcesContent":["import { addTracingExtensions, getMainCarrier } from '@sentry/core';\nimport type { Integration, IntegrationClass } from '@sentry/types';\nimport { dynamicRequire, isNodeEnv, loadModule } from '@sentry/utils';\n\n/**\n * @private\n */\nfunction _autoloadDatabaseIntegrations(): void {\n const carrier = getMainCarrier();\n if (!carrier.__SENTRY__) {\n return;\n }\n\n const packageToIntegrationMapping: Record<string, () => Integration> = {\n mongodb() {\n const integration = dynamicRequire(module, './node/integrations/mongo') as {\n Mongo: IntegrationClass<Integration>;\n };\n return new integration.Mongo();\n },\n mongoose() {\n const integration = dynamicRequire(module, './node/integrations/mongo') as {\n Mongo: IntegrationClass<Integration>;\n };\n return new integration.Mongo();\n },\n mysql() {\n const integration = dynamicRequire(module, './node/integrations/mysql') as {\n Mysql: IntegrationClass<Integration>;\n };\n return new integration.Mysql();\n },\n pg() {\n const integration = dynamicRequire(module, './node/integrations/postgres') as {\n Postgres: IntegrationClass<Integration>;\n };\n return new integration.Postgres();\n },\n };\n\n const mappedPackages = Object.keys(packageToIntegrationMapping)\n .filter(moduleName => !!loadModule(moduleName))\n .map(pkg => {\n try {\n return packageToIntegrationMapping[pkg]();\n } catch (e) {\n return undefined;\n }\n })\n .filter(p => p) as Integration[];\n\n if (mappedPackages.length > 0) {\n carrier.__SENTRY__.integrations = [...(carrier.__SENTRY__.integrations || []), ...mappedPackages];\n }\n}\n\n/**\n * This patches the global object and injects the Tracing extensions methods\n */\nexport function addExtensionMethods(): void {\n addTracingExtensions();\n\n // Detect and automatically load specified integrations.\n if (isNodeEnv()) {\n _autoloadDatabaseIntegrations();\n }\n}\n"],"names":[],"mappings":";;;AAIA;AACA;AACA;AACA,SAAS,6BAA6B,GAAS;AAC/C,EAAE,MAAM,OAAA,GAAU,cAAc,EAAE,CAAA;AAClC,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;AAC3B,IAAI,OAAM;AACV,GAAE;AACF;AACA,EAAE,MAAM,2BAA2B,GAAsC;AACzE,IAAI,OAAO,GAAG;AACd,MAAM,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,2BAA2B,CAAE;;AAExE,CAAA;AACN,MAAM,OAAO,IAAI,WAAW,CAAC,KAAK,EAAE,CAAA;AACpC,KAAK;AACL,IAAI,QAAQ,GAAG;AACf,MAAM,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,2BAA2B,CAAE;;AAExE,CAAA;AACN,MAAM,OAAO,IAAI,WAAW,CAAC,KAAK,EAAE,CAAA;AACpC,KAAK;AACL,IAAI,KAAK,GAAG;AACZ,MAAM,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,2BAA2B,CAAE;;AAExE,CAAA;AACN,MAAM,OAAO,IAAI,WAAW,CAAC,KAAK,EAAE,CAAA;AACpC,KAAK;AACL,IAAI,EAAE,GAAG;AACT,MAAM,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,8BAA8B,CAAE;;AAE3E,CAAA;AACN,MAAM,OAAO,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAA;AACvC,KAAK;AACL,GAAG,CAAA;AACH;AACA,EAAE,MAAM,cAAe,GAAE,MAAM,CAAC,IAAI,CAAC,2BAA2B,CAAA;AAChE,KAAK,MAAM,CAAC,UAAW,IAAG,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAA;AAClD,KAAK,GAAG,CAAC,GAAA,IAAO;AAChB,MAAM,IAAI;AACV,QAAQ,OAAO,2BAA2B,CAAC,GAAG,CAAC,EAAE,CAAA;AACjD,OAAQ,CAAA,OAAO,CAAC,EAAE;AAClB,QAAQ,OAAO,SAAS,CAAA;AACxB,OAAM;AACN,KAAK,CAAA;AACL,KAAK,MAAM,CAAC,CAAE,IAAG,CAAC,CAAE,EAAA;AACpB;AACA,EAAE,IAAI,cAAc,CAAC,MAAO,GAAE,CAAC,EAAE;AACjC,IAAI,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,EAAE,GAAG,cAAc,CAAC,CAAA;AACrG,GAAE;AACF,CAAA;AACA;AACA;AACA;AACA;AACO,SAAS,mBAAmB,GAAS;AAC5C,EAAE,oBAAoB,EAAE,CAAA;AACxB;AACA;AACA,EAAE,IAAI,SAAS,EAAE,EAAE;AACnB,IAAI,6BAA6B,EAAE,CAAA;AACnC,GAAE;AACF;;;;"}

17
node_modules/@sentry-internal/tracing/esm/index.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
export { IdleTransaction, Span, SpanStatus, Transaction, extractTraceparentData, getActiveTransaction, hasTracingEnabled, spanStatusfromHttpCode, startIdleTransaction } from '@sentry/core';
export { TRACEPARENT_REGEXP, stripUrlQueryAndFragment } from '@sentry/utils';
export { Express } from './node/integrations/express.js';
export { Postgres } from './node/integrations/postgres.js';
export { Mysql } from './node/integrations/mysql.js';
export { Mongo } from './node/integrations/mongo.js';
export { Prisma } from './node/integrations/prisma.js';
export { GraphQL } from './node/integrations/graphql.js';
export { Apollo } from './node/integrations/apollo.js';
export { lazyLoadedNodePerformanceMonitoringIntegrations } from './node/integrations/lazy.js';
export { BROWSER_TRACING_INTEGRATION_ID, BrowserTracing } from './browser/browsertracing.js';
export { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from './browser/browserTracingIntegration.js';
export { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './browser/request.js';
export { addClsInstrumentationHandler, addFidInstrumentationHandler, addLcpInstrumentationHandler, addPerformanceInstrumentationHandler } from './browser/instrument.js';
export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './common/fetch.js';
export { addExtensionMethods } from './extensions.js';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;"}

View File

@@ -0,0 +1,186 @@
import { _optionalChain } from '@sentry/utils';
import { loadModule, logger, fill, arrayify, isThenable } from '@sentry/utils';
import { DEBUG_BUILD } from '../../common/debug-build.js';
import { shouldDisableAutoInstrumentation } from './utils/node-utils.js';
/** Tracing integration for Apollo */
class Apollo {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'Apollo';}
/**
* @inheritDoc
*/
/**
* @inheritDoc
*/
constructor(
options = {
useNestjs: false,
},
) {
this.name = Apollo.id;
this._useNest = !!options.useNestjs;
}
/** @inheritdoc */
loadDependency() {
if (this._useNest) {
this._module = this._module || loadModule('@nestjs/graphql');
} else {
this._module = this._module || loadModule('apollo-server-core');
}
return this._module;
}
/**
* @inheritDoc
*/
// eslint-disable-next-line deprecation/deprecation
setupOnce(_, getCurrentHub) {
if (shouldDisableAutoInstrumentation(getCurrentHub)) {
DEBUG_BUILD && logger.log('Apollo Integration is skipped because of instrumenter configuration.');
return;
}
if (this._useNest) {
const pkg = this.loadDependency();
if (!pkg) {
DEBUG_BUILD && logger.error('Apollo-NestJS Integration was unable to require @nestjs/graphql package.');
return;
}
/**
* Iterate over resolvers of NestJS ResolversExplorerService before schemas are constructed.
*/
fill(
pkg.GraphQLFactory.prototype,
'mergeWithSchema',
function (orig) {
return function (
...args
) {
fill(this.resolversExplorerService, 'explore', function (orig) {
return function () {
const resolvers = arrayify(orig.call(this));
const instrumentedResolvers = instrumentResolvers(resolvers, getCurrentHub);
return instrumentedResolvers;
};
});
return orig.call(this, ...args);
};
},
);
} else {
const pkg = this.loadDependency();
if (!pkg) {
DEBUG_BUILD && logger.error('Apollo Integration was unable to require apollo-server-core package.');
return;
}
/**
* Iterate over resolvers of the ApolloServer instance before schemas are constructed.
*/
fill(pkg.ApolloServerBase.prototype, 'constructSchema', function (orig) {
return function (
) {
if (!this.config.resolvers) {
if (DEBUG_BUILD) {
if (this.config.schema) {
logger.warn(
'Apollo integration is not able to trace `ApolloServer` instances constructed via `schema` property.' +
'If you are using NestJS with Apollo, please use `Sentry.Integrations.Apollo({ useNestjs: true })` instead.',
);
logger.warn();
} else if (this.config.modules) {
logger.warn(
'Apollo integration is not able to trace `ApolloServer` instances constructed via `modules` property.',
);
}
logger.error('Skipping tracing as no resolvers found on the `ApolloServer` instance.');
}
return orig.call(this);
}
const resolvers = arrayify(this.config.resolvers);
this.config.resolvers = instrumentResolvers(resolvers, getCurrentHub);
return orig.call(this);
};
});
}
}
}Apollo.__initStatic();
// eslint-disable-next-line deprecation/deprecation
function instrumentResolvers(resolvers, getCurrentHub) {
return resolvers.map(model => {
Object.keys(model).forEach(resolverGroupName => {
Object.keys(model[resolverGroupName]).forEach(resolverName => {
if (typeof model[resolverGroupName][resolverName] !== 'function') {
return;
}
wrapResolver(model, resolverGroupName, resolverName, getCurrentHub);
});
});
return model;
});
}
/**
* Wrap a single resolver which can be a parent of other resolvers and/or db operations.
*/
function wrapResolver(
model,
resolverGroupName,
resolverName,
// eslint-disable-next-line deprecation/deprecation
getCurrentHub,
) {
fill(model[resolverGroupName], resolverName, function (orig) {
return function ( ...args) {
// eslint-disable-next-line deprecation/deprecation
const scope = getCurrentHub().getScope();
// eslint-disable-next-line deprecation/deprecation
const parentSpan = scope.getSpan();
// eslint-disable-next-line deprecation/deprecation
const span = _optionalChain([parentSpan, 'optionalAccess', _2 => _2.startChild, 'call', _3 => _3({
description: `${resolverGroupName}.${resolverName}`,
op: 'graphql.resolve',
origin: 'auto.graphql.apollo',
})]);
const rv = orig.call(this, ...args);
if (isThenable(rv)) {
return rv.then((res) => {
_optionalChain([span, 'optionalAccess', _4 => _4.end, 'call', _5 => _5()]);
return res;
});
}
_optionalChain([span, 'optionalAccess', _6 => _6.end, 'call', _7 => _7()]);
return rv;
};
});
}
export { Apollo };
//# sourceMappingURL=apollo.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,488 @@
import { _optionalChain } from '@sentry/utils';
import { spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
import { logger, getNumberOfUrlSegments, stripUrlQueryAndFragment, extractPathForTransaction, isRegExp, GLOBAL_OBJ } from '@sentry/utils';
import { DEBUG_BUILD } from '../../common/debug-build.js';
import { shouldDisableAutoInstrumentation } from './utils/node-utils.js';
/* eslint-disable max-lines */
/**
* Express integration
*
* Provides an request and error handler for Express framework as well as tracing capabilities
*/
class Express {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'Express';}
/**
* @inheritDoc
*/
/**
* Express App instance
*/
/**
* @inheritDoc
*/
constructor(options = {}) {
this.name = Express.id;
this._router = options.router || options.app;
this._methods = (Array.isArray(options.methods) ? options.methods : []).concat('use');
}
/**
* @inheritDoc
*/
// eslint-disable-next-line deprecation/deprecation
setupOnce(_, getCurrentHub) {
if (!this._router) {
DEBUG_BUILD && logger.error('ExpressIntegration is missing an Express instance');
return;
}
if (shouldDisableAutoInstrumentation(getCurrentHub)) {
DEBUG_BUILD && logger.log('Express Integration is skipped because of instrumenter configuration.');
return;
}
instrumentMiddlewares(this._router, this._methods);
instrumentRouter(this._router );
}
}Express.__initStatic();
/**
* Wraps original middleware function in a tracing call, which stores the info about the call as a span,
* and finishes it once the middleware is done invoking.
*
* Express middlewares have 3 various forms, thus we have to take care of all of them:
* // sync
* app.use(function (req, res) { ... })
* // async
* app.use(function (req, res, next) { ... })
* // error handler
* app.use(function (err, req, res, next) { ... })
*
* They all internally delegate to the `router[method]` of the given application instance.
*/
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
function wrap(fn, method) {
const arity = fn.length;
switch (arity) {
case 2: {
return function ( req, res) {
const transaction = res.__sentry_transaction;
if (transaction) {
// eslint-disable-next-line deprecation/deprecation
const span = transaction.startChild({
description: fn.name,
op: `middleware.express.${method}`,
origin: 'auto.middleware.express',
});
res.once('finish', () => {
span.end();
});
}
return fn.call(this, req, res);
};
}
case 3: {
return function (
req,
res,
next,
) {
const transaction = res.__sentry_transaction;
// eslint-disable-next-line deprecation/deprecation
const span = _optionalChain([transaction, 'optionalAccess', _2 => _2.startChild, 'call', _3 => _3({
description: fn.name,
op: `middleware.express.${method}`,
origin: 'auto.middleware.express',
})]);
fn.call(this, req, res, function ( ...args) {
_optionalChain([span, 'optionalAccess', _4 => _4.end, 'call', _5 => _5()]);
next.call(this, ...args);
});
};
}
case 4: {
return function (
err,
req,
res,
next,
) {
const transaction = res.__sentry_transaction;
// eslint-disable-next-line deprecation/deprecation
const span = _optionalChain([transaction, 'optionalAccess', _6 => _6.startChild, 'call', _7 => _7({
description: fn.name,
op: `middleware.express.${method}`,
origin: 'auto.middleware.express',
})]);
fn.call(this, err, req, res, function ( ...args) {
_optionalChain([span, 'optionalAccess', _8 => _8.end, 'call', _9 => _9()]);
next.call(this, ...args);
});
};
}
default: {
throw new Error(`Express middleware takes 2-4 arguments. Got: ${arity}`);
}
}
}
/**
* Takes all the function arguments passed to the original `app` or `router` method, eg. `app.use` or `router.use`
* and wraps every function, as well as array of functions with a call to our `wrap` method.
* We have to take care of the arrays as well as iterate over all of the arguments,
* as `app.use` can accept middlewares in few various forms.
*
* app.use([<path>], <fn>)
* app.use([<path>], <fn>, ...<fn>)
* app.use([<path>], ...<fn>[])
*/
function wrapMiddlewareArgs(args, method) {
return args.map((arg) => {
if (typeof arg === 'function') {
return wrap(arg, method);
}
if (Array.isArray(arg)) {
return arg.map((a) => {
if (typeof a === 'function') {
return wrap(a, method);
}
return a;
});
}
return arg;
});
}
/**
* Patches original router to utilize our tracing functionality
*/
function patchMiddleware(router, method) {
const originalCallback = router[method];
router[method] = function (...args) {
return originalCallback.call(this, ...wrapMiddlewareArgs(args, method));
};
return router;
}
/**
* Patches original router methods
*/
function instrumentMiddlewares(router, methods = []) {
methods.forEach((method) => patchMiddleware(router, method));
}
/**
* Patches the prototype of Express.Router to accumulate the resolved route
* if a layer instance's `match` function was called and it returned a successful match.
*
* @see https://github.com/expressjs/express/blob/master/lib/router/index.js
*
* @param appOrRouter the router instance which can either be an app (i.e. top-level) or a (nested) router.
*/
function instrumentRouter(appOrRouter) {
// This is how we can distinguish between app and routers
const isApp = 'settings' in appOrRouter;
// In case the app's top-level router hasn't been initialized yet, we have to do it now
if (isApp && appOrRouter._router === undefined && appOrRouter.lazyrouter) {
appOrRouter.lazyrouter();
}
const router = isApp ? appOrRouter._router : appOrRouter;
if (!router) {
/*
If we end up here, this means likely that this integration is used with Express 3 or Express 5.
For now, we don't support these versions (3 is very old and 5 is still in beta). To support Express 5,
we'd need to make more changes to the routing instrumentation because the router is no longer part of
the Express core package but maintained in its own package. The new router has different function
signatures and works slightly differently, demanding more changes than just taking the router from
`app.router` instead of `app._router`.
@see https://github.com/pillarjs/router
TODO: Proper Express 5 support
*/
DEBUG_BUILD && logger.debug('Cannot instrument router for URL Parameterization (did not find a valid router).');
DEBUG_BUILD && logger.debug('Routing instrumentation is currently only supported in Express 4.');
return;
}
const routerProto = Object.getPrototypeOf(router) ;
const originalProcessParams = routerProto.process_params;
routerProto.process_params = function process_params(
layer,
called,
req,
res,
done,
) {
// Base case: We're in the first part of the URL (thus we start with the root '/')
if (!req._reconstructedRoute) {
req._reconstructedRoute = '';
}
// If the layer's partial route has params, is a regex or an array, the route is stored in layer.route.
const { layerRoutePath, isRegex, isArray, numExtraSegments } = getLayerRoutePathInfo(layer);
if (layerRoutePath || isRegex || isArray) {
req._hasParameters = true;
}
// Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path
let partialRoute;
if (layerRoutePath) {
partialRoute = layerRoutePath;
} else {
/**
* prevent duplicate segment in _reconstructedRoute param if router match multiple routes before final path
* example:
* original url: /api/v1/1234
* prevent: /api/api/v1/:userId
* router structure
* /api -> middleware
* /api/v1 -> middleware
* /1234 -> endpoint with param :userId
* final _reconstructedRoute is /api/v1/:userId
*/
partialRoute = preventDuplicateSegments(req.originalUrl, req._reconstructedRoute, layer.path) || '';
}
// Normalize the partial route so that it doesn't contain leading or trailing slashes
// and exclude empty or '*' wildcard routes.
// The exclusion of '*' routes is our best effort to not "pollute" the transaction name
// with interim handlers (e.g. ones that check authentication or do other middleware stuff).
// We want to end up with the parameterized URL of the incoming request without any extraneous path segments.
const finalPartialRoute = partialRoute
.split('/')
.filter(segment => segment.length > 0 && (isRegex || isArray || !segment.includes('*')))
.join('/');
// If we found a valid partial URL, we append it to the reconstructed route
if (finalPartialRoute && finalPartialRoute.length > 0) {
// If the partial route is from a regex route, we append a '/' to close the regex
req._reconstructedRoute += `/${finalPartialRoute}${isRegex ? '/' : ''}`;
}
// Now we check if we are in the "last" part of the route. We determine this by comparing the
// number of URL segments from the original URL to that of our reconstructed parameterized URL.
// If we've reached our final destination, we update the transaction name.
const urlLength = getNumberOfUrlSegments(stripUrlQueryAndFragment(req.originalUrl || '')) + numExtraSegments;
const routeLength = getNumberOfUrlSegments(req._reconstructedRoute);
if (urlLength === routeLength) {
if (!req._hasParameters) {
if (req._reconstructedRoute !== req.originalUrl) {
req._reconstructedRoute = req.originalUrl ? stripUrlQueryAndFragment(req.originalUrl) : req.originalUrl;
}
}
const transaction = res.__sentry_transaction;
const attributes = (transaction && spanToJSON(transaction).data) || {};
if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
// If the request URL is '/' or empty, the reconstructed route will be empty.
// Therefore, we fall back to setting the final route to '/' in this case.
const finalRoute = req._reconstructedRoute || '/';
const [name, source] = extractPathForTransaction(req, { path: true, method: true, customRoute: finalRoute });
transaction.updateName(name);
transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
}
}
return originalProcessParams.call(this, layer, called, req, res, done);
};
}
/**
* Recreate layer.route.path from layer.regexp and layer.keys.
* Works until express.js used package path-to-regexp@0.1.7
* or until layer.keys contain offset attribute
*
* @param layer the layer to extract the stringified route from
*
* @returns string in layer.route.path structure 'router/:pathParam' or undefined
*/
const extractOriginalRoute = (
path,
regexp,
keys,
) => {
if (!path || !regexp || !keys || Object.keys(keys).length === 0 || !_optionalChain([keys, 'access', _10 => _10[0], 'optionalAccess', _11 => _11.offset])) {
return undefined;
}
const orderedKeys = keys.sort((a, b) => a.offset - b.offset);
// add d flag for getting indices from regexp result
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regexp comes from express.js
const pathRegex = new RegExp(regexp, `${regexp.flags}d`);
/**
* use custom type cause of TS error with missing indices in RegExpExecArray
*/
const execResult = pathRegex.exec(path) ;
if (!execResult || !execResult.indices) {
return undefined;
}
/**
* remove first match from regex cause contain whole layer.path
*/
const [, ...paramIndices] = execResult.indices;
if (paramIndices.length !== orderedKeys.length) {
return undefined;
}
let resultPath = path;
let indexShift = 0;
/**
* iterate param matches from regexp.exec
*/
paramIndices.forEach((item, index) => {
/** check if offsets is define because in some cases regex d flag returns undefined */
if (item) {
const [startOffset, endOffset] = item;
/**
* isolate part before param
*/
const substr1 = resultPath.substring(0, startOffset - indexShift);
/**
* define paramName as replacement in format :pathParam
*/
const replacement = `:${orderedKeys[index].name}`;
/**
* isolate part after param
*/
const substr2 = resultPath.substring(endOffset - indexShift);
/**
* recreate original path but with param replacement
*/
resultPath = substr1 + replacement + substr2;
/**
* calculate new index shift after resultPath was modified
*/
indexShift = indexShift + (endOffset - startOffset - replacement.length);
}
});
return resultPath;
};
/**
* Extracts and stringifies the layer's route which can either be a string with parameters (`users/:id`),
* a RegEx (`/test/`) or an array of strings and regexes (`['/path1', /\/path[2-5]/, /path/:id]`). Additionally
* returns extra information about the route, such as if the route is defined as regex or as an array.
*
* @param layer the layer to extract the stringified route from
*
* @returns an object containing the stringified route, a flag determining if the route was a regex
* and the number of extra segments to the matched path that are additionally in the route,
* if the route was an array (defaults to 0).
*/
function getLayerRoutePathInfo(layer) {
let lrp = _optionalChain([layer, 'access', _12 => _12.route, 'optionalAccess', _13 => _13.path]);
const isRegex = isRegExp(lrp);
const isArray = Array.isArray(lrp);
if (!lrp) {
// parse node.js major version
// Next.js will complain if we directly use `proces.versions` here because of edge runtime.
const [major] = (GLOBAL_OBJ ).process.versions.node.split('.').map(Number);
// allow call extractOriginalRoute only if node version support Regex d flag, node 16+
if (major >= 16) {
/**
* If lrp does not exist try to recreate original layer path from route regexp
*/
lrp = extractOriginalRoute(layer.path, layer.regexp, layer.keys);
}
}
if (!lrp) {
return { isRegex, isArray, numExtraSegments: 0 };
}
const numExtraSegments = isArray
? Math.max(getNumberOfArrayUrlSegments(lrp ) - getNumberOfUrlSegments(layer.path || ''), 0)
: 0;
const layerRoutePath = getLayerRoutePathString(isArray, lrp);
return { layerRoutePath, isRegex, isArray, numExtraSegments };
}
/**
* Returns the number of URL segments in an array of routes
*
* Example: ['/api/test', /\/api\/post[0-9]/, '/users/:id/details`] -> 7
*/
function getNumberOfArrayUrlSegments(routesArray) {
return routesArray.reduce((accNumSegments, currentRoute) => {
// array members can be a RegEx -> convert them toString
return accNumSegments + getNumberOfUrlSegments(currentRoute.toString());
}, 0);
}
/**
* Extracts and returns the stringified version of the layers route path
* Handles route arrays (by joining the paths together) as well as RegExp and normal
* string values (in the latter case the toString conversion is technically unnecessary but
* it doesn't hurt us either).
*/
function getLayerRoutePathString(isArray, lrp) {
if (isArray) {
return (lrp ).map(r => r.toString()).join(',');
}
return lrp && lrp.toString();
}
/**
* remove duplicate segment contain in layerPath against reconstructedRoute,
* and return only unique segment that can be added into reconstructedRoute
*/
function preventDuplicateSegments(
originalUrl,
reconstructedRoute,
layerPath,
) {
// filter query params
const normalizeURL = stripUrlQueryAndFragment(originalUrl || '');
const originalUrlSplit = _optionalChain([normalizeURL, 'optionalAccess', _14 => _14.split, 'call', _15 => _15('/'), 'access', _16 => _16.filter, 'call', _17 => _17(v => !!v)]);
let tempCounter = 0;
const currentOffset = _optionalChain([reconstructedRoute, 'optionalAccess', _18 => _18.split, 'call', _19 => _19('/'), 'access', _20 => _20.filter, 'call', _21 => _21(v => !!v), 'access', _22 => _22.length]) || 0;
const result = _optionalChain([layerPath
, 'optionalAccess', _23 => _23.split, 'call', _24 => _24('/')
, 'access', _25 => _25.filter, 'call', _26 => _26(segment => {
if (_optionalChain([originalUrlSplit, 'optionalAccess', _27 => _27[currentOffset + tempCounter]]) === segment) {
tempCounter += 1;
return true;
}
return false;
})
, 'access', _28 => _28.join, 'call', _29 => _29('/')]);
return result;
}
export { Express, extractOriginalRoute, preventDuplicateSegments };
//# sourceMappingURL=express.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,82 @@
import { _optionalChain } from '@sentry/utils';
import { loadModule, logger, fill, isThenable } from '@sentry/utils';
import { DEBUG_BUILD } from '../../common/debug-build.js';
import { shouldDisableAutoInstrumentation } from './utils/node-utils.js';
/** Tracing integration for graphql package */
class GraphQL {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'GraphQL';}
/**
* @inheritDoc
*/
constructor() {
this.name = GraphQL.id;
}
/** @inheritdoc */
loadDependency() {
return (this._module = this._module || loadModule('graphql/execution/execute.js'));
}
/**
* @inheritDoc
*/
// eslint-disable-next-line deprecation/deprecation
setupOnce(_, getCurrentHub) {
if (shouldDisableAutoInstrumentation(getCurrentHub)) {
DEBUG_BUILD && logger.log('GraphQL Integration is skipped because of instrumenter configuration.');
return;
}
const pkg = this.loadDependency();
if (!pkg) {
DEBUG_BUILD && logger.error('GraphQL Integration was unable to require graphql/execution package.');
return;
}
fill(pkg, 'execute', function (orig) {
return function ( ...args) {
// eslint-disable-next-line deprecation/deprecation
const scope = getCurrentHub().getScope();
// eslint-disable-next-line deprecation/deprecation
const parentSpan = scope.getSpan();
// eslint-disable-next-line deprecation/deprecation
const span = _optionalChain([parentSpan, 'optionalAccess', _2 => _2.startChild, 'call', _3 => _3({
description: 'execute',
op: 'graphql.execute',
origin: 'auto.graphql.graphql',
})]);
// eslint-disable-next-line deprecation/deprecation
_optionalChain([scope, 'optionalAccess', _4 => _4.setSpan, 'call', _5 => _5(span)]);
const rv = orig.call(this, ...args);
if (isThenable(rv)) {
return rv.then((res) => {
_optionalChain([span, 'optionalAccess', _6 => _6.end, 'call', _7 => _7()]);
// eslint-disable-next-line deprecation/deprecation
_optionalChain([scope, 'optionalAccess', _8 => _8.setSpan, 'call', _9 => _9(parentSpan)]);
return res;
});
}
_optionalChain([span, 'optionalAccess', _10 => _10.end, 'call', _11 => _11()]);
// eslint-disable-next-line deprecation/deprecation
_optionalChain([scope, 'optionalAccess', _12 => _12.setSpan, 'call', _13 => _13(parentSpan)]);
return rv;
};
});
}
}GraphQL.__initStatic();
export { GraphQL };
//# sourceMappingURL=graphql.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,49 @@
import { dynamicRequire } from '@sentry/utils';
const lazyLoadedNodePerformanceMonitoringIntegrations = [
() => {
const integration = dynamicRequire(module, './apollo')
;
return new integration.Apollo();
},
() => {
const integration = dynamicRequire(module, './apollo')
;
return new integration.Apollo({ useNestjs: true });
},
() => {
const integration = dynamicRequire(module, './graphql')
;
return new integration.GraphQL();
},
() => {
const integration = dynamicRequire(module, './mongo')
;
return new integration.Mongo();
},
() => {
const integration = dynamicRequire(module, './mongo')
;
return new integration.Mongo({ mongoose: true });
},
() => {
const integration = dynamicRequire(module, './mysql')
;
return new integration.Mysql();
},
() => {
const integration = dynamicRequire(module, './postgres')
;
return new integration.Postgres();
},
];
export { lazyLoadedNodePerformanceMonitoringIntegrations };
//# sourceMappingURL=lazy.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"lazy.js","sources":["../../../../src/node/integrations/lazy.ts"],"sourcesContent":["import type { Integration, IntegrationClass } from '@sentry/types';\nimport { dynamicRequire } from '@sentry/utils';\n\nexport interface LazyLoadedIntegration<T = object> extends Integration {\n /**\n * Loads the integration's dependency and caches it so it doesn't have to be loaded again.\n *\n * If this returns undefined, the dependency could not be loaded.\n */\n loadDependency(): T | undefined;\n}\n\nexport const lazyLoadedNodePerformanceMonitoringIntegrations: (() => LazyLoadedIntegration)[] = [\n () => {\n const integration = dynamicRequire(module, './apollo') as {\n Apollo: IntegrationClass<LazyLoadedIntegration>;\n };\n return new integration.Apollo();\n },\n () => {\n const integration = dynamicRequire(module, './apollo') as {\n Apollo: IntegrationClass<LazyLoadedIntegration>;\n };\n return new integration.Apollo({ useNestjs: true });\n },\n () => {\n const integration = dynamicRequire(module, './graphql') as {\n GraphQL: IntegrationClass<LazyLoadedIntegration>;\n };\n return new integration.GraphQL();\n },\n () => {\n const integration = dynamicRequire(module, './mongo') as {\n Mongo: IntegrationClass<LazyLoadedIntegration>;\n };\n return new integration.Mongo();\n },\n () => {\n const integration = dynamicRequire(module, './mongo') as {\n Mongo: IntegrationClass<LazyLoadedIntegration>;\n };\n return new integration.Mongo({ mongoose: true });\n },\n () => {\n const integration = dynamicRequire(module, './mysql') as {\n Mysql: IntegrationClass<LazyLoadedIntegration>;\n };\n return new integration.Mysql();\n },\n () => {\n const integration = dynamicRequire(module, './postgres') as {\n Postgres: IntegrationClass<LazyLoadedIntegration>;\n };\n return new integration.Postgres();\n },\n];\n"],"names":[],"mappings":";;AAYO,MAAM,+CAA+C,GAAoC;AAChG,EAAE,MAAM;AACR,IAAI,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,UAAU,CAAE;;AAEvD,CAAA;AACJ,IAAI,OAAO,IAAI,WAAW,CAAC,MAAM,EAAE,CAAA;AACnC,GAAG;AACH,EAAE,MAAM;AACR,IAAI,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,UAAU,CAAE;;AAEvD,CAAA;AACJ,IAAI,OAAO,IAAI,WAAW,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,IAAK,EAAC,CAAC,CAAA;AACtD,GAAG;AACH,EAAE,MAAM;AACR,IAAI,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,WAAW,CAAE;;AAExD,CAAA;AACJ,IAAI,OAAO,IAAI,WAAW,CAAC,OAAO,EAAE,CAAA;AACpC,GAAG;AACH,EAAE,MAAM;AACR,IAAI,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,SAAS,CAAE;;AAEtD,CAAA;AACJ,IAAI,OAAO,IAAI,WAAW,CAAC,KAAK,EAAE,CAAA;AAClC,GAAG;AACH,EAAE,MAAM;AACR,IAAI,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,SAAS,CAAE;;AAEtD,CAAA;AACJ,IAAI,OAAO,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,IAAK,EAAC,CAAC,CAAA;AACpD,GAAG;AACH,EAAE,MAAM;AACR,IAAI,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,SAAS,CAAE;;AAEtD,CAAA;AACJ,IAAI,OAAO,IAAI,WAAW,CAAC,KAAK,EAAE,CAAA;AAClC,GAAG;AACH,EAAE,MAAM;AACR,IAAI,MAAM,cAAc,cAAc,CAAC,MAAM,EAAE,YAAY,CAAE;;AAEzD,CAAA;AACJ,IAAI,OAAO,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAA;AACrC,GAAG;AACH;;;;"}

View File

@@ -0,0 +1,260 @@
import { _optionalChain } from '@sentry/utils';
import { loadModule, logger, fill, isThenable } from '@sentry/utils';
import { DEBUG_BUILD } from '../../common/debug-build.js';
import { shouldDisableAutoInstrumentation } from './utils/node-utils.js';
// This allows us to use the same array for both defaults options and the type itself.
// (note `as const` at the end to make it a union of string literal types (i.e. "a" | "b" | ... )
// and not just a string[])
const OPERATIONS = [
'aggregate', // aggregate(pipeline, options, callback)
'bulkWrite', // bulkWrite(operations, options, callback)
'countDocuments', // countDocuments(query, options, callback)
'createIndex', // createIndex(fieldOrSpec, options, callback)
'createIndexes', // createIndexes(indexSpecs, options, callback)
'deleteMany', // deleteMany(filter, options, callback)
'deleteOne', // deleteOne(filter, options, callback)
'distinct', // distinct(key, query, options, callback)
'drop', // drop(options, callback)
'dropIndex', // dropIndex(indexName, options, callback)
'dropIndexes', // dropIndexes(options, callback)
'estimatedDocumentCount', // estimatedDocumentCount(options, callback)
'find', // find(query, options, callback)
'findOne', // findOne(query, options, callback)
'findOneAndDelete', // findOneAndDelete(filter, options, callback)
'findOneAndReplace', // findOneAndReplace(filter, replacement, options, callback)
'findOneAndUpdate', // findOneAndUpdate(filter, update, options, callback)
'indexes', // indexes(options, callback)
'indexExists', // indexExists(indexes, options, callback)
'indexInformation', // indexInformation(options, callback)
'initializeOrderedBulkOp', // initializeOrderedBulkOp(options, callback)
'insertMany', // insertMany(docs, options, callback)
'insertOne', // insertOne(doc, options, callback)
'isCapped', // isCapped(options, callback)
'mapReduce', // mapReduce(map, reduce, options, callback)
'options', // options(options, callback)
'parallelCollectionScan', // parallelCollectionScan(options, callback)
'rename', // rename(newName, options, callback)
'replaceOne', // replaceOne(filter, doc, options, callback)
'stats', // stats(options, callback)
'updateMany', // updateMany(filter, update, options, callback)
'updateOne', // updateOne(filter, update, options, callback)
] ;
// All of the operations above take `options` and `callback` as their final parameters, but some of them
// take additional parameters as well. For those operations, this is a map of
// { <operation name>: [<names of additional parameters>] }, as a way to know what to call the operation's
// positional arguments when we add them to the span's `data` object later
const OPERATION_SIGNATURES
= {
// aggregate intentionally not included because `pipeline` arguments are too complex to serialize well
// see https://github.com/getsentry/sentry-javascript/pull/3102
bulkWrite: ['operations'],
countDocuments: ['query'],
createIndex: ['fieldOrSpec'],
createIndexes: ['indexSpecs'],
deleteMany: ['filter'],
deleteOne: ['filter'],
distinct: ['key', 'query'],
dropIndex: ['indexName'],
find: ['query'],
findOne: ['query'],
findOneAndDelete: ['filter'],
findOneAndReplace: ['filter', 'replacement'],
findOneAndUpdate: ['filter', 'update'],
indexExists: ['indexes'],
insertMany: ['docs'],
insertOne: ['doc'],
mapReduce: ['map', 'reduce'],
rename: ['newName'],
replaceOne: ['filter', 'doc'],
updateMany: ['filter', 'update'],
updateOne: ['filter', 'update'],
};
function isCursor(maybeCursor) {
return maybeCursor && typeof maybeCursor === 'object' && maybeCursor.once && typeof maybeCursor.once === 'function';
}
/** Tracing integration for mongo package */
class Mongo {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'Mongo';}
/**
* @inheritDoc
*/
/**
* @inheritDoc
*/
constructor(options = {}) {
this.name = Mongo.id;
this._operations = Array.isArray(options.operations) ? options.operations : (OPERATIONS );
this._describeOperations = 'describeOperations' in options ? options.describeOperations : true;
this._useMongoose = !!options.useMongoose;
}
/** @inheritdoc */
loadDependency() {
const moduleName = this._useMongoose ? 'mongoose' : 'mongodb';
return (this._module = this._module || loadModule(moduleName));
}
/**
* @inheritDoc
*/
// eslint-disable-next-line deprecation/deprecation
setupOnce(_, getCurrentHub) {
if (shouldDisableAutoInstrumentation(getCurrentHub)) {
DEBUG_BUILD && logger.log('Mongo Integration is skipped because of instrumenter configuration.');
return;
}
const pkg = this.loadDependency();
if (!pkg) {
const moduleName = this._useMongoose ? 'mongoose' : 'mongodb';
DEBUG_BUILD && logger.error(`Mongo Integration was unable to require \`${moduleName}\` package.`);
return;
}
this._instrumentOperations(pkg.Collection, this._operations, getCurrentHub);
}
/**
* Patches original collection methods
*/
// eslint-disable-next-line deprecation/deprecation
_instrumentOperations(collection, operations, getCurrentHub) {
operations.forEach((operation) => this._patchOperation(collection, operation, getCurrentHub));
}
/**
* Patches original collection to utilize our tracing functionality
*/
// eslint-disable-next-line deprecation/deprecation
_patchOperation(collection, operation, getCurrentHub) {
if (!(operation in collection.prototype)) return;
const getSpanContext = this._getSpanContextFromOperationArguments.bind(this);
fill(collection.prototype, operation, function (orig) {
return function ( ...args) {
const lastArg = args[args.length - 1];
// eslint-disable-next-line deprecation/deprecation
const hub = getCurrentHub();
// eslint-disable-next-line deprecation/deprecation
const scope = hub.getScope();
// eslint-disable-next-line deprecation/deprecation
const client = hub.getClient();
// eslint-disable-next-line deprecation/deprecation
const parentSpan = scope.getSpan();
const sendDefaultPii = _optionalChain([client, 'optionalAccess', _2 => _2.getOptions, 'call', _3 => _3(), 'access', _4 => _4.sendDefaultPii]);
// Check if the operation was passed a callback. (mapReduce requires a different check, as
// its (non-callback) arguments can also be functions.)
if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) {
// eslint-disable-next-line deprecation/deprecation
const span = _optionalChain([parentSpan, 'optionalAccess', _5 => _5.startChild, 'call', _6 => _6(getSpanContext(this, operation, args, sendDefaultPii))]);
const maybePromiseOrCursor = orig.call(this, ...args);
if (isThenable(maybePromiseOrCursor)) {
return maybePromiseOrCursor.then((res) => {
_optionalChain([span, 'optionalAccess', _7 => _7.end, 'call', _8 => _8()]);
return res;
});
}
// If the operation returns a Cursor
// we need to attach a listener to it to finish the span when the cursor is closed.
else if (isCursor(maybePromiseOrCursor)) {
const cursor = maybePromiseOrCursor ;
try {
cursor.once('close', () => {
_optionalChain([span, 'optionalAccess', _9 => _9.end, 'call', _10 => _10()]);
});
} catch (e) {
// If the cursor is already closed, `once` will throw an error. In that case, we can
// finish the span immediately.
_optionalChain([span, 'optionalAccess', _11 => _11.end, 'call', _12 => _12()]);
}
return cursor;
} else {
_optionalChain([span, 'optionalAccess', _13 => _13.end, 'call', _14 => _14()]);
return maybePromiseOrCursor;
}
}
// eslint-disable-next-line deprecation/deprecation
const span = _optionalChain([parentSpan, 'optionalAccess', _15 => _15.startChild, 'call', _16 => _16(getSpanContext(this, operation, args.slice(0, -1)))]);
return orig.call(this, ...args.slice(0, -1), function (err, result) {
_optionalChain([span, 'optionalAccess', _17 => _17.end, 'call', _18 => _18()]);
lastArg(err, result);
});
};
});
}
/**
* Form a SpanContext based on the user input to a given operation.
*/
_getSpanContextFromOperationArguments(
collection,
operation,
args,
sendDefaultPii = false,
) {
const data = {
'db.system': 'mongodb',
'db.name': collection.dbName,
'db.operation': operation,
'db.mongodb.collection': collection.collectionName,
};
const spanContext = {
op: 'db',
// TODO v8: Use `${collection.collectionName}.${operation}`
origin: 'auto.db.mongo',
description: operation,
data,
};
// If the operation takes no arguments besides `options` and `callback`, or if argument
// collection is disabled for this operation, just return early.
const signature = OPERATION_SIGNATURES[operation];
const shouldDescribe = Array.isArray(this._describeOperations)
? this._describeOperations.includes(operation)
: this._describeOperations;
if (!signature || !shouldDescribe || !sendDefaultPii) {
return spanContext;
}
try {
// Special case for `mapReduce`, as the only one accepting functions as arguments.
if (operation === 'mapReduce') {
const [map, reduce] = args ;
data[signature[0]] = typeof map === 'string' ? map : map.name || '<anonymous>';
data[signature[1]] = typeof reduce === 'string' ? reduce : reduce.name || '<anonymous>';
} else {
for (let i = 0; i < signature.length; i++) {
data[`db.mongodb.${signature[i]}`] = JSON.stringify(args[i]);
}
}
} catch (_oO) {
// no-empty
}
return spanContext;
}
}Mongo.__initStatic();
export { Mongo };
//# sourceMappingURL=mongo.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,131 @@
import { _optionalChain } from '@sentry/utils';
import { loadModule, logger, fill } from '@sentry/utils';
import { DEBUG_BUILD } from '../../common/debug-build.js';
import { shouldDisableAutoInstrumentation } from './utils/node-utils.js';
/** Tracing integration for node-mysql package */
class Mysql {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'Mysql';}
/**
* @inheritDoc
*/
constructor() {
this.name = Mysql.id;
}
/** @inheritdoc */
loadDependency() {
return (this._module = this._module || loadModule('mysql/lib/Connection.js'));
}
/**
* @inheritDoc
*/
// eslint-disable-next-line deprecation/deprecation
setupOnce(_, getCurrentHub) {
if (shouldDisableAutoInstrumentation(getCurrentHub)) {
DEBUG_BUILD && logger.log('Mysql Integration is skipped because of instrumenter configuration.');
return;
}
const pkg = this.loadDependency();
if (!pkg) {
DEBUG_BUILD && logger.error('Mysql Integration was unable to require `mysql` package.');
return;
}
let mySqlConfig = undefined;
try {
pkg.prototype.connect = new Proxy(pkg.prototype.connect, {
apply(wrappingTarget, thisArg, args) {
if (!mySqlConfig) {
mySqlConfig = thisArg.config;
}
return wrappingTarget.apply(thisArg, args);
},
});
} catch (e) {
DEBUG_BUILD && logger.error('Mysql Integration was unable to instrument `mysql` config.');
}
function spanDataFromConfig() {
if (!mySqlConfig) {
return {};
}
return {
'server.address': mySqlConfig.host,
'server.port': mySqlConfig.port,
'db.user': mySqlConfig.user,
};
}
function finishSpan(span) {
if (!span) {
return;
}
const data = spanDataFromConfig();
Object.keys(data).forEach(key => {
span.setAttribute(key, data[key]);
});
span.end();
}
// The original function will have one of these signatures:
// function (callback) => void
// function (options, callback) => void
// function (options, values, callback) => void
fill(pkg, 'createQuery', function (orig) {
return function ( options, values, callback) {
// eslint-disable-next-line deprecation/deprecation
const scope = getCurrentHub().getScope();
// eslint-disable-next-line deprecation/deprecation
const parentSpan = scope.getSpan();
// eslint-disable-next-line deprecation/deprecation
const span = _optionalChain([parentSpan, 'optionalAccess', _2 => _2.startChild, 'call', _3 => _3({
description: typeof options === 'string' ? options : (options ).sql,
op: 'db',
origin: 'auto.db.mysql',
data: {
'db.system': 'mysql',
},
})]);
if (typeof callback === 'function') {
return orig.call(this, options, values, function (err, result, fields) {
finishSpan(span);
callback(err, result, fields);
});
}
if (typeof values === 'function') {
return orig.call(this, options, function (err, result, fields) {
finishSpan(span);
values(err, result, fields);
});
}
// streaming, no callback!
const query = orig.call(this, options, values) ;
query.on('end', () => {
finishSpan(span);
});
return query;
};
});
}
}Mysql.__initStatic();
export { Mysql };
//# sourceMappingURL=mysql.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,126 @@
import { _optionalChain } from '@sentry/utils';
import { loadModule, logger, fill, isThenable } from '@sentry/utils';
import { DEBUG_BUILD } from '../../common/debug-build.js';
import { shouldDisableAutoInstrumentation } from './utils/node-utils.js';
/** Tracing integration for node-postgres package */
class Postgres {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'Postgres';}
/**
* @inheritDoc
*/
constructor(options = {}) {
this.name = Postgres.id;
this._usePgNative = !!options.usePgNative;
this._module = options.module;
}
/** @inheritdoc */
loadDependency() {
return (this._module = this._module || loadModule('pg'));
}
/**
* @inheritDoc
*/
// eslint-disable-next-line deprecation/deprecation
setupOnce(_, getCurrentHub) {
if (shouldDisableAutoInstrumentation(getCurrentHub)) {
DEBUG_BUILD && logger.log('Postgres Integration is skipped because of instrumenter configuration.');
return;
}
const pkg = this.loadDependency();
if (!pkg) {
DEBUG_BUILD && logger.error('Postgres Integration was unable to require `pg` package.');
return;
}
const Client = this._usePgNative ? _optionalChain([pkg, 'access', _2 => _2.native, 'optionalAccess', _3 => _3.Client]) : pkg.Client;
if (!Client) {
DEBUG_BUILD && logger.error("Postgres Integration was unable to access 'pg-native' bindings.");
return;
}
/**
* function (query, callback) => void
* function (query, params, callback) => void
* function (query) => Promise
* function (query, params) => Promise
* function (pg.Cursor) => pg.Cursor
*/
fill(Client.prototype, 'query', function (orig) {
return function ( config, values, callback) {
// eslint-disable-next-line deprecation/deprecation
const scope = getCurrentHub().getScope();
// eslint-disable-next-line deprecation/deprecation
const parentSpan = scope.getSpan();
const data = {
'db.system': 'postgresql',
};
try {
if (this.database) {
data['db.name'] = this.database;
}
if (this.host) {
data['server.address'] = this.host;
}
if (this.port) {
data['server.port'] = this.port;
}
if (this.user) {
data['db.user'] = this.user;
}
} catch (e) {
// ignore
}
// eslint-disable-next-line deprecation/deprecation
const span = _optionalChain([parentSpan, 'optionalAccess', _4 => _4.startChild, 'call', _5 => _5({
description: typeof config === 'string' ? config : (config ).text,
op: 'db',
origin: 'auto.db.postgres',
data,
})]);
if (typeof callback === 'function') {
return orig.call(this, config, values, function (err, result) {
_optionalChain([span, 'optionalAccess', _6 => _6.end, 'call', _7 => _7()]);
callback(err, result);
});
}
if (typeof values === 'function') {
return orig.call(this, config, function (err, result) {
_optionalChain([span, 'optionalAccess', _8 => _8.end, 'call', _9 => _9()]);
values(err, result);
});
}
const rv = typeof values !== 'undefined' ? orig.call(this, config, values) : orig.call(this, config);
if (isThenable(rv)) {
return rv.then((res) => {
_optionalChain([span, 'optionalAccess', _10 => _10.end, 'call', _11 => _11()]);
return res;
});
}
_optionalChain([span, 'optionalAccess', _12 => _12.end, 'call', _13 => _13()]);
return rv;
};
});
}
}Postgres.__initStatic();
export { Postgres };
//# sourceMappingURL=postgres.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,89 @@
import { startSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getCurrentHub } from '@sentry/core';
import { addNonEnumerableProperty, logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../../common/debug-build.js';
import { shouldDisableAutoInstrumentation } from './utils/node-utils.js';
function isValidPrismaClient(possibleClient) {
return !!possibleClient && !!(possibleClient )['$use'];
}
/** Tracing integration for @prisma/client package */
class Prisma {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'Prisma';}
/**
* @inheritDoc
*/
/**
* @inheritDoc
*/
constructor(options = {}) {
this.name = Prisma.id;
// We instrument the PrismaClient inside the constructor and not inside `setupOnce` because in some cases of server-side
// bundling (Next.js) multiple Prisma clients can be instantiated, even though users don't intend to. When instrumenting
// in setupOnce we can only ever instrument one client.
// https://github.com/getsentry/sentry-javascript/issues/7216#issuecomment-1602375012
// In the future we might explore providing a dedicated PrismaClient middleware instead of this hack.
if (isValidPrismaClient(options.client) && !options.client._sentryInstrumented) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addNonEnumerableProperty(options.client , '_sentryInstrumented', true);
const clientData = {};
try {
const engineConfig = (options.client )._engineConfig;
if (engineConfig) {
const { activeProvider, clientVersion } = engineConfig;
if (activeProvider) {
clientData['db.system'] = activeProvider;
}
if (clientVersion) {
clientData['db.prisma.version'] = clientVersion;
}
}
} catch (e) {
// ignore
}
options.client.$use((params, next) => {
// eslint-disable-next-line deprecation/deprecation
if (shouldDisableAutoInstrumentation(getCurrentHub)) {
return next(params);
}
const action = params.action;
const model = params.model;
return startSpan(
{
name: model ? `${model} ${action}` : action,
onlyIfParent: true,
op: 'db.prisma',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.prisma',
},
data: { ...clientData, 'db.operation': action },
},
() => next(params),
);
});
} else {
DEBUG_BUILD &&
logger.warn('Unsupported Prisma client provided to PrismaIntegration. Provided client:', options.client);
}
}
/**
* @inheritDoc
*/
setupOnce() {
// Noop - here for backwards compatibility
}
} Prisma.__initStatic();
export { Prisma };
//# sourceMappingURL=prisma.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
import { _optionalChain } from '@sentry/utils';
/**
* Check if Sentry auto-instrumentation should be disabled.
*
* @param getCurrentHub A method to fetch the current hub
* @returns boolean
*/
// eslint-disable-next-line deprecation/deprecation
function shouldDisableAutoInstrumentation(getCurrentHub) {
// eslint-disable-next-line deprecation/deprecation
const clientOptions = _optionalChain([getCurrentHub, 'call', _ => _(), 'access', _2 => _2.getClient, 'call', _3 => _3(), 'optionalAccess', _4 => _4.getOptions, 'call', _5 => _5()]);
const instrumenter = _optionalChain([clientOptions, 'optionalAccess', _6 => _6.instrumenter]) || 'sentry';
return instrumenter !== 'sentry';
}
export { shouldDisableAutoInstrumentation };
//# sourceMappingURL=node-utils.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node-utils.js","sources":["../../../../../src/node/integrations/utils/node-utils.ts"],"sourcesContent":["import type { Hub } from '@sentry/types';\n\n/**\n * Check if Sentry auto-instrumentation should be disabled.\n *\n * @param getCurrentHub A method to fetch the current hub\n * @returns boolean\n */\n// eslint-disable-next-line deprecation/deprecation\nexport function shouldDisableAutoInstrumentation(getCurrentHub: () => Hub): boolean {\n // eslint-disable-next-line deprecation/deprecation\n const clientOptions = getCurrentHub().getClient()?.getOptions();\n const instrumenter = clientOptions?.instrumenter || 'sentry';\n\n return instrumenter !== 'sentry';\n}\n"],"names":[],"mappings":";;AAEA,CAAA,CAAA;CACA,EAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;CACA;CACA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA;CACA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;CACA,CAAA;AACA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACO,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,EAAsB;EACpF,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;EACE,CAAA,CAAA,CAAA,CAAA,EAAM,cAAgB,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,EAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,EAAA,CAAA,EAAA,CAAA,EAAC,EAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,EAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAC,MAAA,EAAA,CAAA,KAAA,CAAA,CAAA,CAAA,CAAC,EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,GAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,oBAAE,CAAA,CAAA;EAC/D,MAAM,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,EAAA,cAAA,CAAA,CAAE,aAAa,EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,EAAA,CAAA,EAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,EAAgB,QAAQ;;EAE5D,CAAO,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAiB,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ;AAClC;;"}