@sentry-internal/tracing
Advanced tools
Comparing version 7.107.0 to 7.108.0
@@ -89,6 +89,6 @@ Object.defineProperty(exports, '__esModule', { value: true }); | ||
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */ | ||
this._interactionIdtoRouteNameMapping = {}; | ||
this._interactionIdToRouteNameMapping = {}; | ||
if (this.options.enableInp) { | ||
index.startTrackingINP(this._interactionIdtoRouteNameMapping); | ||
index.startTrackingINP(this._interactionIdToRouteNameMapping); | ||
} | ||
@@ -344,3 +344,3 @@ if (this.options.enableLongTask) { | ||
_registerInpInteractionListener() { | ||
instrument.addPerformanceInstrumentationHandler('event', ({ entries }) => { | ||
const handleEntries = ({ entries }) => { | ||
const client = core.getClient(); | ||
@@ -358,11 +358,17 @@ // We need to get the replay, user, and activeTransaction from the current scope | ||
const user = currentScope !== undefined ? currentScope.getUser() : undefined; | ||
for (const entry of entries) { | ||
entries.forEach(entry => { | ||
if (isPerformanceEventTiming(entry)) { | ||
const interactionId = entry.interactionId; | ||
if (interactionId === undefined) { | ||
return; | ||
} | ||
const existingInteraction = this._interactionIdToRouteNameMapping[interactionId]; | ||
const duration = entry.duration; | ||
const keys = Object.keys(this._interactionIdtoRouteNameMapping); | ||
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 | ||
return this._interactionIdToRouteNameMapping[a].duration < | ||
this._interactionIdToRouteNameMapping[b].duration | ||
? a | ||
@@ -372,15 +378,35 @@ : b; | ||
: undefined; | ||
if ( | ||
// 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 | ||
duration > this._interactionIdToRouteNameMapping[minInteractionId].duration | ||
) { | ||
const interactionId = entry.interactionId; | ||
// 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 (interactionId && routeName && parentContext) { | ||
if (minInteractionId && Object.keys(this._interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) { | ||
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]; | ||
delete this._interactionIdToRouteNameMapping[minInteractionId]; | ||
} | ||
this._interactionIdtoRouteNameMapping[interactionId] = { | ||
this._interactionIdToRouteNameMapping[interactionId] = { | ||
routeName, | ||
@@ -392,2 +418,3 @@ duration, | ||
replayId, | ||
startTime, | ||
}; | ||
@@ -397,4 +424,6 @@ } | ||
} | ||
} | ||
}); | ||
}); | ||
}; | ||
instrument.addPerformanceInstrumentationHandler('event', handleEntries); | ||
instrument.addPerformanceInstrumentationHandler('first-input', handleEntries); | ||
} | ||
@@ -401,0 +430,0 @@ } |
@@ -63,5 +63,5 @@ Object.defineProperty(exports, '__esModule', { value: true }); | ||
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */ | ||
const interactionIdtoRouteNameMapping = {}; | ||
const interactionIdToRouteNameMapping = {}; | ||
if (options.enableInp) { | ||
index.startTrackingINP(interactionIdtoRouteNameMapping); | ||
index.startTrackingINP(interactionIdToRouteNameMapping); | ||
} | ||
@@ -280,3 +280,3 @@ | ||
if (options.enableInp) { | ||
registerInpInteractionListener(interactionIdtoRouteNameMapping, latestRoute); | ||
registerInpInteractionListener(interactionIdToRouteNameMapping, latestRoute); | ||
} | ||
@@ -410,3 +410,3 @@ | ||
function registerInpInteractionListener( | ||
interactionIdtoRouteNameMapping, | ||
interactionIdToRouteNameMapping, | ||
latestRoute | ||
@@ -416,3 +416,3 @@ | ||
) { | ||
instrument.addPerformanceInstrumentationHandler('event', ({ entries }) => { | ||
const handleEntries = ({ entries }) => { | ||
const client = core.getClient(); | ||
@@ -430,10 +430,16 @@ // We need to get the replay, user, and activeTransaction from the current scope | ||
const user = currentScope !== undefined ? currentScope.getUser() : undefined; | ||
for (const entry of entries) { | ||
entries.forEach(entry => { | ||
if (isPerformanceEventTiming(entry)) { | ||
const interactionId = entry.interactionId; | ||
if (interactionId === undefined) { | ||
return; | ||
} | ||
const existingInteraction = interactionIdToRouteNameMapping[interactionId]; | ||
const duration = entry.duration; | ||
const keys = Object.keys(interactionIdtoRouteNameMapping); | ||
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 | ||
return interactionIdToRouteNameMapping[a].duration < interactionIdToRouteNameMapping[b].duration | ||
? a | ||
@@ -443,12 +449,35 @@ : b; | ||
: undefined; | ||
if (minInteractionId === undefined || duration > interactionIdtoRouteNameMapping[minInteractionId].duration) { | ||
const interactionId = entry.interactionId; | ||
// 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 (interactionId && routeName && parentContext) { | ||
if (minInteractionId && Object.keys(interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) { | ||
if (routeName && parentContext) { | ||
if (minInteractionId && Object.keys(interactionIdToRouteNameMapping).length >= MAX_INTERACTIONS) { | ||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete | ||
delete interactionIdtoRouteNameMapping[minInteractionId]; | ||
delete interactionIdToRouteNameMapping[minInteractionId]; | ||
} | ||
interactionIdtoRouteNameMapping[interactionId] = { | ||
interactionIdToRouteNameMapping[interactionId] = { | ||
routeName, | ||
@@ -460,2 +489,3 @@ duration, | ||
replayId, | ||
startTime, | ||
}; | ||
@@ -465,4 +495,6 @@ } | ||
} | ||
} | ||
}); | ||
}); | ||
}; | ||
instrument.addPerformanceInstrumentationHandler('event', handleEntries); | ||
instrument.addPerformanceInstrumentationHandler('first-input', handleEntries); | ||
} | ||
@@ -469,0 +501,0 @@ |
@@ -10,2 +10,3 @@ Object.defineProperty(exports, '__esModule', { value: true }); | ||
const observe = require('./web-vitals/lib/observe.js'); | ||
const onTTFB = require('./web-vitals/onTTFB.js'); | ||
@@ -18,2 +19,3 @@ const handlers = {}; | ||
let _previousLcp; | ||
let _previousTtfb; | ||
let _previousInp; | ||
@@ -51,2 +53,9 @@ | ||
* 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. | ||
@@ -135,2 +144,11 @@ */ | ||
function instrumentTtfb() { | ||
return onTTFB.onTTFB(metric => { | ||
triggerHandlers('ttfb', { | ||
metric, | ||
}); | ||
_previousTtfb = metric; | ||
}); | ||
} | ||
function instrumentInp() { | ||
@@ -219,2 +237,3 @@ return getINP.onINP(metric => { | ||
exports.addPerformanceInstrumentationHandler = addPerformanceInstrumentationHandler; | ||
exports.addTtfbInstrumentationHandler = addTtfbInstrumentationHandler; | ||
//# sourceMappingURL=instrument.js.map |
@@ -10,2 +10,3 @@ Object.defineProperty(exports, '__esModule', { value: true }); | ||
const utils$1 = require('./utils.js'); | ||
const getNavigationEntry = require('../web-vitals/lib/getNavigationEntry.js'); | ||
@@ -49,2 +50,3 @@ const MAX_INT_AS_BYTES = 2147483647; | ||
const lcpCallback = _trackLCP(); | ||
const ttfbCallback = _trackTtfb(); | ||
@@ -55,2 +57,3 @@ return () => { | ||
lcpCallback(); | ||
ttfbCallback(); | ||
}; | ||
@@ -184,6 +187,52 @@ } | ||
function _trackTtfb() { | ||
return instrument.addTtfbInstrumentationHandler(({ metric }) => { | ||
const entry = metric.entries[metric.entries.length - 1]; | ||
if (!entry) { | ||
return; | ||
} | ||
debugBuild.DEBUG_BUILD && utils.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) { | ||
function _trackINP(interactionIdToRouteNameMapping) { | ||
return instrument.addInpInstrumentationHandler(({ metric }) => { | ||
const entry = metric.entries.find(e => e.name === 'click' || e.name === 'pointerdown'); | ||
if (metric.value === undefined) { | ||
return; | ||
} | ||
const entry = metric.entries.find( | ||
entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name] !== undefined, | ||
); | ||
const client = core.getClient(); | ||
@@ -193,2 +242,3 @@ if (!entry || !client) { | ||
} | ||
const interactionType = INP_ENTRY_MAP[entry.name]; | ||
const options = client.getOptions(); | ||
@@ -198,12 +248,8 @@ /** Build the INP span, create an envelope from the span, and then send the envelope */ | ||
const duration = msToSec(metric.value); | ||
const { routeName, parentContext, activeTransaction, user, replayId } = | ||
entry.interactionId !== undefined | ||
? interactionIdtoRouteNameMapping[entry.interactionId] | ||
: { | ||
routeName: undefined, | ||
parentContext: undefined, | ||
activeTransaction: undefined, | ||
user: undefined, | ||
replayId: undefined, | ||
}; | ||
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; | ||
@@ -215,3 +261,3 @@ // eslint-disable-next-line deprecation/deprecation | ||
endTimestamp: startTime + duration, | ||
op: 'ui.interaction.click', | ||
op: `ui.interaction.${interactionType}`, | ||
name: utils.htmlTreeAsString(entry.target), | ||
@@ -264,5 +310,2 @@ attributes: { | ||
let responseStartTimestamp; | ||
let requestStartTimestamp; | ||
const { op, start_timestamp: transactionStartTime } = core.spanToJSON(transaction); | ||
@@ -283,4 +326,2 @@ | ||
_addNavigationSpans(transaction, entry, timeOrigin); | ||
responseStartTimestamp = timeOrigin + msToSec(entry.responseStart); | ||
requestStartTimestamp = timeOrigin + msToSec(entry.requestStart); | ||
break; | ||
@@ -322,3 +363,3 @@ } | ||
if (op === 'pageload') { | ||
_addTtfbToMeasurements(_measurements, responseStartTimestamp, requestStartTimestamp, transactionStartTime); | ||
_addTtfbRequestTimeToMeasurements(_measurements); | ||
@@ -607,36 +648,16 @@ ['fcp', 'fp', 'lcp'].forEach(name => { | ||
/** | ||
* Add ttfb information to measurements | ||
* Add ttfb request time information to measurements. | ||
* | ||
* Exported for tests | ||
* ttfb information is added via vendored web vitals library. | ||
*/ | ||
function _addTtfbToMeasurements( | ||
_measurements, | ||
responseStartTimestamp, | ||
requestStartTimestamp, | ||
transactionStartTime, | ||
) { | ||
// Generate TTFB (Time to First Byte), which measured as the time between the beginning of the transaction and the | ||
// start of the response in milliseconds | ||
if (typeof responseStartTimestamp === 'number' && transactionStartTime) { | ||
debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding TTFB'); | ||
_measurements['ttfb'] = { | ||
// As per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStart, | ||
// responseStart can be 0 if the request is coming straight from the cache. | ||
// This might lead us to calculate a negative ttfb if we don't use Math.max here. | ||
// | ||
// This logic is the same as what is in the web-vitals library to calculate ttfb | ||
// https://github.com/GoogleChrome/web-vitals/blob/2301de5015e82b09925238a228a0893635854587/src/onTTFB.ts#L92 | ||
// TODO(abhi): We should use the web-vitals library instead of this custom calculation. | ||
value: Math.max(responseStartTimestamp - transactionStartTime, 0) * 1000, | ||
function _addTtfbRequestTimeToMeasurements(_measurements) { | ||
const navEntry = getNavigationEntry.getNavigationEntry() ; | ||
const { responseStart, requestStart } = navEntry; | ||
if (requestStart <= responseStart) { | ||
debugBuild.DEBUG_BUILD && utils.logger.log('[Measurements] Adding TTFB Request Time'); | ||
_measurements['ttfb.requestTime'] = { | ||
value: responseStart - requestStart, | ||
unit: 'millisecond', | ||
}; | ||
if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) { | ||
// Capture the time spent making the request and receiving the first byte of the response. | ||
// This is the time between the start of the request and the start of the response in milliseconds. | ||
_measurements['ttfb.requestTime'] = { | ||
value: (responseStartTimestamp - requestStartTimestamp) * 1000, | ||
unit: 'millisecond', | ||
}; | ||
} | ||
} | ||
@@ -679,3 +700,2 @@ } | ||
exports._addResourceSpans = _addResourceSpans; | ||
exports._addTtfbToMeasurements = _addTtfbToMeasurements; | ||
exports.addPerformanceEntries = addPerformanceEntries; | ||
@@ -682,0 +702,0 @@ exports.startTrackingINP = startTrackingINP; |
@@ -87,6 +87,6 @@ import { TRACING_DEFAULTS, addTracingExtensions, startIdleTransaction, getActiveTransaction, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getClient, getCurrentScope } from '@sentry/core'; | ||
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */ | ||
this._interactionIdtoRouteNameMapping = {}; | ||
this._interactionIdToRouteNameMapping = {}; | ||
if (this.options.enableInp) { | ||
startTrackingINP(this._interactionIdtoRouteNameMapping); | ||
startTrackingINP(this._interactionIdToRouteNameMapping); | ||
} | ||
@@ -342,3 +342,3 @@ if (this.options.enableLongTask) { | ||
_registerInpInteractionListener() { | ||
addPerformanceInstrumentationHandler('event', ({ entries }) => { | ||
const handleEntries = ({ entries }) => { | ||
const client = getClient(); | ||
@@ -356,11 +356,17 @@ // We need to get the replay, user, and activeTransaction from the current scope | ||
const user = currentScope !== undefined ? currentScope.getUser() : undefined; | ||
for (const entry of entries) { | ||
entries.forEach(entry => { | ||
if (isPerformanceEventTiming(entry)) { | ||
const interactionId = entry.interactionId; | ||
if (interactionId === undefined) { | ||
return; | ||
} | ||
const existingInteraction = this._interactionIdToRouteNameMapping[interactionId]; | ||
const duration = entry.duration; | ||
const keys = Object.keys(this._interactionIdtoRouteNameMapping); | ||
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 | ||
return this._interactionIdToRouteNameMapping[a].duration < | ||
this._interactionIdToRouteNameMapping[b].duration | ||
? a | ||
@@ -370,15 +376,35 @@ : b; | ||
: undefined; | ||
if ( | ||
// 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 | ||
duration > this._interactionIdToRouteNameMapping[minInteractionId].duration | ||
) { | ||
const interactionId = entry.interactionId; | ||
// 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 (interactionId && routeName && parentContext) { | ||
if (minInteractionId && Object.keys(this._interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) { | ||
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]; | ||
delete this._interactionIdToRouteNameMapping[minInteractionId]; | ||
} | ||
this._interactionIdtoRouteNameMapping[interactionId] = { | ||
this._interactionIdToRouteNameMapping[interactionId] = { | ||
routeName, | ||
@@ -390,2 +416,3 @@ duration, | ||
replayId, | ||
startTime, | ||
}; | ||
@@ -395,4 +422,6 @@ } | ||
} | ||
} | ||
}); | ||
}); | ||
}; | ||
addPerformanceInstrumentationHandler('event', handleEntries); | ||
addPerformanceInstrumentationHandler('first-input', handleEntries); | ||
} | ||
@@ -399,0 +428,0 @@ } |
@@ -61,5 +61,5 @@ import { TRACING_DEFAULTS, addTracingExtensions, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getCurrentHub, startIdleTransaction, getActiveTransaction, getClient, getCurrentScope } from '@sentry/core'; | ||
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */ | ||
const interactionIdtoRouteNameMapping = {}; | ||
const interactionIdToRouteNameMapping = {}; | ||
if (options.enableInp) { | ||
startTrackingINP(interactionIdtoRouteNameMapping); | ||
startTrackingINP(interactionIdToRouteNameMapping); | ||
} | ||
@@ -278,3 +278,3 @@ | ||
if (options.enableInp) { | ||
registerInpInteractionListener(interactionIdtoRouteNameMapping, latestRoute); | ||
registerInpInteractionListener(interactionIdToRouteNameMapping, latestRoute); | ||
} | ||
@@ -408,3 +408,3 @@ | ||
function registerInpInteractionListener( | ||
interactionIdtoRouteNameMapping, | ||
interactionIdToRouteNameMapping, | ||
latestRoute | ||
@@ -414,3 +414,3 @@ | ||
) { | ||
addPerformanceInstrumentationHandler('event', ({ entries }) => { | ||
const handleEntries = ({ entries }) => { | ||
const client = getClient(); | ||
@@ -428,10 +428,16 @@ // We need to get the replay, user, and activeTransaction from the current scope | ||
const user = currentScope !== undefined ? currentScope.getUser() : undefined; | ||
for (const entry of entries) { | ||
entries.forEach(entry => { | ||
if (isPerformanceEventTiming(entry)) { | ||
const interactionId = entry.interactionId; | ||
if (interactionId === undefined) { | ||
return; | ||
} | ||
const existingInteraction = interactionIdToRouteNameMapping[interactionId]; | ||
const duration = entry.duration; | ||
const keys = Object.keys(interactionIdtoRouteNameMapping); | ||
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 | ||
return interactionIdToRouteNameMapping[a].duration < interactionIdToRouteNameMapping[b].duration | ||
? a | ||
@@ -441,12 +447,35 @@ : b; | ||
: undefined; | ||
if (minInteractionId === undefined || duration > interactionIdtoRouteNameMapping[minInteractionId].duration) { | ||
const interactionId = entry.interactionId; | ||
// 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 (interactionId && routeName && parentContext) { | ||
if (minInteractionId && Object.keys(interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) { | ||
if (routeName && parentContext) { | ||
if (minInteractionId && Object.keys(interactionIdToRouteNameMapping).length >= MAX_INTERACTIONS) { | ||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete | ||
delete interactionIdtoRouteNameMapping[minInteractionId]; | ||
delete interactionIdToRouteNameMapping[minInteractionId]; | ||
} | ||
interactionIdtoRouteNameMapping[interactionId] = { | ||
interactionIdToRouteNameMapping[interactionId] = { | ||
routeName, | ||
@@ -458,2 +487,3 @@ duration, | ||
replayId, | ||
startTime, | ||
}; | ||
@@ -463,4 +493,6 @@ } | ||
} | ||
} | ||
}); | ||
}); | ||
}; | ||
addPerformanceInstrumentationHandler('event', handleEntries); | ||
addPerformanceInstrumentationHandler('first-input', handleEntries); | ||
} | ||
@@ -467,0 +499,0 @@ |
@@ -8,2 +8,3 @@ import { logger, getFunctionName } from '@sentry/utils'; | ||
import { observe } from './web-vitals/lib/observe.js'; | ||
import { onTTFB } from './web-vitals/onTTFB.js'; | ||
@@ -16,2 +17,3 @@ const handlers = {}; | ||
let _previousLcp; | ||
let _previousTtfb; | ||
let _previousInp; | ||
@@ -49,2 +51,9 @@ | ||
* 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. | ||
@@ -133,2 +142,11 @@ */ | ||
function instrumentTtfb() { | ||
return onTTFB(metric => { | ||
triggerHandlers('ttfb', { | ||
metric, | ||
}); | ||
_previousTtfb = metric; | ||
}); | ||
} | ||
function instrumentInp() { | ||
@@ -212,3 +230,3 @@ return onINP(metric => { | ||
export { addClsInstrumentationHandler, addFidInstrumentationHandler, addInpInstrumentationHandler, addLcpInstrumentationHandler, addPerformanceInstrumentationHandler }; | ||
export { addClsInstrumentationHandler, addFidInstrumentationHandler, addInpInstrumentationHandler, addLcpInstrumentationHandler, addPerformanceInstrumentationHandler, addTtfbInstrumentationHandler }; | ||
//# sourceMappingURL=instrument.js.map |
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, addInpInstrumentationHandler } from '../instrument.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'; | ||
@@ -46,2 +47,3 @@ const MAX_INT_AS_BYTES = 2147483647; | ||
const lcpCallback = _trackLCP(); | ||
const ttfbCallback = _trackTtfb(); | ||
@@ -52,2 +54,3 @@ return () => { | ||
lcpCallback(); | ||
ttfbCallback(); | ||
}; | ||
@@ -181,6 +184,52 @@ } | ||
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) { | ||
function _trackINP(interactionIdToRouteNameMapping) { | ||
return addInpInstrumentationHandler(({ metric }) => { | ||
const entry = metric.entries.find(e => e.name === 'click' || e.name === 'pointerdown'); | ||
if (metric.value === undefined) { | ||
return; | ||
} | ||
const entry = metric.entries.find( | ||
entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name] !== undefined, | ||
); | ||
const client = getClient(); | ||
@@ -190,2 +239,3 @@ if (!entry || !client) { | ||
} | ||
const interactionType = INP_ENTRY_MAP[entry.name]; | ||
const options = client.getOptions(); | ||
@@ -195,12 +245,8 @@ /** Build the INP span, create an envelope from the span, and then send the envelope */ | ||
const duration = msToSec(metric.value); | ||
const { routeName, parentContext, activeTransaction, user, replayId } = | ||
entry.interactionId !== undefined | ||
? interactionIdtoRouteNameMapping[entry.interactionId] | ||
: { | ||
routeName: undefined, | ||
parentContext: undefined, | ||
activeTransaction: undefined, | ||
user: undefined, | ||
replayId: undefined, | ||
}; | ||
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; | ||
@@ -212,3 +258,3 @@ // eslint-disable-next-line deprecation/deprecation | ||
endTimestamp: startTime + duration, | ||
op: 'ui.interaction.click', | ||
op: `ui.interaction.${interactionType}`, | ||
name: htmlTreeAsString(entry.target), | ||
@@ -261,5 +307,2 @@ attributes: { | ||
let responseStartTimestamp; | ||
let requestStartTimestamp; | ||
const { op, start_timestamp: transactionStartTime } = spanToJSON(transaction); | ||
@@ -280,4 +323,2 @@ | ||
_addNavigationSpans(transaction, entry, timeOrigin); | ||
responseStartTimestamp = timeOrigin + msToSec(entry.responseStart); | ||
requestStartTimestamp = timeOrigin + msToSec(entry.requestStart); | ||
break; | ||
@@ -319,3 +360,3 @@ } | ||
if (op === 'pageload') { | ||
_addTtfbToMeasurements(_measurements, responseStartTimestamp, requestStartTimestamp, transactionStartTime); | ||
_addTtfbRequestTimeToMeasurements(_measurements); | ||
@@ -604,36 +645,16 @@ ['fcp', 'fp', 'lcp'].forEach(name => { | ||
/** | ||
* Add ttfb information to measurements | ||
* Add ttfb request time information to measurements. | ||
* | ||
* Exported for tests | ||
* ttfb information is added via vendored web vitals library. | ||
*/ | ||
function _addTtfbToMeasurements( | ||
_measurements, | ||
responseStartTimestamp, | ||
requestStartTimestamp, | ||
transactionStartTime, | ||
) { | ||
// Generate TTFB (Time to First Byte), which measured as the time between the beginning of the transaction and the | ||
// start of the response in milliseconds | ||
if (typeof responseStartTimestamp === 'number' && transactionStartTime) { | ||
DEBUG_BUILD && logger.log('[Measurements] Adding TTFB'); | ||
_measurements['ttfb'] = { | ||
// As per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStart, | ||
// responseStart can be 0 if the request is coming straight from the cache. | ||
// This might lead us to calculate a negative ttfb if we don't use Math.max here. | ||
// | ||
// This logic is the same as what is in the web-vitals library to calculate ttfb | ||
// https://github.com/GoogleChrome/web-vitals/blob/2301de5015e82b09925238a228a0893635854587/src/onTTFB.ts#L92 | ||
// TODO(abhi): We should use the web-vitals library instead of this custom calculation. | ||
value: Math.max(responseStartTimestamp - transactionStartTime, 0) * 1000, | ||
function _addTtfbRequestTimeToMeasurements(_measurements) { | ||
const navEntry = getNavigationEntry() ; | ||
const { responseStart, requestStart } = navEntry; | ||
if (requestStart <= responseStart) { | ||
DEBUG_BUILD && logger.log('[Measurements] Adding TTFB Request Time'); | ||
_measurements['ttfb.requestTime'] = { | ||
value: responseStart - requestStart, | ||
unit: 'millisecond', | ||
}; | ||
if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) { | ||
// Capture the time spent making the request and receiving the first byte of the response. | ||
// This is the time between the start of the request and the start of the response in milliseconds. | ||
_measurements['ttfb.requestTime'] = { | ||
value: (responseStartTimestamp - requestStartTimestamp) * 1000, | ||
unit: 'millisecond', | ||
}; | ||
} | ||
} | ||
@@ -674,3 +695,3 @@ } | ||
export { _addMeasureSpans, _addResourceSpans, _addTtfbToMeasurements, addPerformanceEntries, startTrackingINP, startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals }; | ||
export { _addMeasureSpans, _addResourceSpans, addPerformanceEntries, startTrackingINP, startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals }; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@sentry-internal/tracing", | ||
"version": "7.107.0", | ||
"version": "7.108.0", | ||
"description": "Sentry Internal Tracing Package", | ||
@@ -32,5 +32,5 @@ "repository": "git://github.com/getsentry/sentry-javascript.git", | ||
"dependencies": { | ||
"@sentry/core": "7.107.0", | ||
"@sentry/types": "7.107.0", | ||
"@sentry/utils": "7.107.0" | ||
"@sentry/core": "7.108.0", | ||
"@sentry/types": "7.108.0", | ||
"@sentry/utils": "7.108.0" | ||
}, | ||
@@ -37,0 +37,0 @@ "devDependencies": { |
@@ -128,3 +128,3 @@ import { Hub } from '@sentry/core'; | ||
private _hasSetTracePropagationTargets; | ||
private _interactionIdtoRouteNameMapping; | ||
private _interactionIdToRouteNameMapping; | ||
private _latestRoute; | ||
@@ -131,0 +131,0 @@ constructor(_options?: Partial<BrowserTracingOptions>); |
@@ -6,3 +6,3 @@ export * from '../exports'; | ||
export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request'; | ||
export { addPerformanceInstrumentationHandler, addClsInstrumentationHandler, addFidInstrumentationHandler, addLcpInstrumentationHandler, } from './instrument'; | ||
export { addPerformanceInstrumentationHandler, addClsInstrumentationHandler, addFidInstrumentationHandler, addLcpInstrumentationHandler, addTtfbInstrumentationHandler, addInpInstrumentationHandler, } from './instrument'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,2 +0,2 @@ | ||
type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource'; | ||
type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource' | 'first-input'; | ||
interface PerformanceEntry { | ||
@@ -84,2 +84,8 @@ readonly duration: number; | ||
* Add a callback that will be triggered when a FID metric is available. | ||
*/ | ||
export declare function addTtfbInstrumentationHandler(callback: (data: { | ||
metric: Metric; | ||
}) => void): CleanupHandlerCallback; | ||
/** | ||
* 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. | ||
@@ -99,3 +105,3 @@ */ | ||
}) => void): CleanupHandlerCallback; | ||
export declare function addPerformanceInstrumentationHandler(type: 'event', callback: (data: { | ||
export declare function addPerformanceInstrumentationHandler(type: 'event' | 'first-input', callback: (data: { | ||
entries: ((PerformanceEntry & { | ||
@@ -102,0 +108,0 @@ target?: unknown | null; |
import { Transaction } from '@sentry/core'; | ||
import { Measurements } from '@sentry/types'; | ||
import { InteractionRouteNameMapping } from '../web-vitals/types'; | ||
@@ -36,8 +35,2 @@ /** | ||
export declare function _addResourceSpans(transaction: Transaction, entry: ResourceEntry, resourceUrl: string, startTime: number, duration: number, timeOrigin: number): void; | ||
/** | ||
* Add ttfb information to measurements | ||
* | ||
* Exported for tests | ||
*/ | ||
export declare function _addTtfbToMeasurements(_measurements: Measurements, responseStartTimestamp: number | undefined, requestStartTimestamp: number | undefined, transactionStartTime: number | undefined): void; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -97,4 +97,5 @@ import { Transaction, TransactionContext, User } from '@sentry/types'; | ||
replayId?: string; | ||
startTime: number; | ||
}; | ||
}; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -128,3 +128,3 @@ import type { Hub } from '@sentry/core'; | ||
private _hasSetTracePropagationTargets; | ||
private _interactionIdtoRouteNameMapping; | ||
private _interactionIdToRouteNameMapping; | ||
private _latestRoute; | ||
@@ -131,0 +131,0 @@ constructor(_options?: Partial<BrowserTracingOptions>); |
@@ -6,3 +6,3 @@ export * from '../exports'; | ||
export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request'; | ||
export { addPerformanceInstrumentationHandler, addClsInstrumentationHandler, addFidInstrumentationHandler, addLcpInstrumentationHandler, } from './instrument'; | ||
export { addPerformanceInstrumentationHandler, addClsInstrumentationHandler, addFidInstrumentationHandler, addLcpInstrumentationHandler, addTtfbInstrumentationHandler, addInpInstrumentationHandler, } from './instrument'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,2 +0,2 @@ | ||
type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource'; | ||
type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource' | 'first-input'; | ||
interface PerformanceEntry { | ||
@@ -84,2 +84,8 @@ readonly duration: number; | ||
* Add a callback that will be triggered when a FID metric is available. | ||
*/ | ||
export declare function addTtfbInstrumentationHandler(callback: (data: { | ||
metric: Metric; | ||
}) => void): CleanupHandlerCallback; | ||
/** | ||
* 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. | ||
@@ -99,3 +105,3 @@ */ | ||
}) => void): CleanupHandlerCallback; | ||
export declare function addPerformanceInstrumentationHandler(type: 'event', callback: (data: { | ||
export declare function addPerformanceInstrumentationHandler(type: 'event' | 'first-input', callback: (data: { | ||
entries: ((PerformanceEntry & { | ||
@@ -102,0 +108,0 @@ target?: unknown | null; |
import type { Transaction } from '@sentry/core'; | ||
import type { Measurements } from '@sentry/types'; | ||
import type { InteractionRouteNameMapping } from '../web-vitals/types'; | ||
@@ -36,8 +35,2 @@ /** | ||
export declare function _addResourceSpans(transaction: Transaction, entry: ResourceEntry, resourceUrl: string, startTime: number, duration: number, timeOrigin: number): void; | ||
/** | ||
* Add ttfb information to measurements | ||
* | ||
* Exported for tests | ||
*/ | ||
export declare function _addTtfbToMeasurements(_measurements: Measurements, responseStartTimestamp: number | undefined, requestStartTimestamp: number | undefined, transactionStartTime: number | undefined): void; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -97,4 +97,5 @@ import type { Transaction, TransactionContext, User } from '@sentry/types'; | ||
replayId?: string; | ||
startTime: number; | ||
}; | ||
}; | ||
//# sourceMappingURL=types.d.ts.map |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 3 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 2 instances in 1 package
1369529
291
11825
20
61
+ Added@sentry/core@7.108.0(transitive)
+ Added@sentry/types@7.108.0(transitive)
+ Added@sentry/utils@7.108.0(transitive)
- Removed@sentry/core@7.107.0(transitive)
- Removed@sentry/types@7.107.0(transitive)
- Removed@sentry/utils@7.107.0(transitive)
Updated@sentry/core@7.108.0
Updated@sentry/types@7.108.0
Updated@sentry/utils@7.108.0