From 9ae7f95b3152d26ea2e4c7ceb7b471debdc95feb Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Sun, 18 Feb 2024 18:36:22 -0500 Subject: [PATCH 01/19] Creates interaction spans with inp when inp is detected --- .../src/browser/browserTracingIntegration.ts | 54 +++++++++++++++ .../src/browser/instrument.ts | 25 ++++++- .../src/browser/metrics/index.ts | 66 ++++++++++++++++++- .../src/browser/web-vitals/types.ts | 2 + 4 files changed, 143 insertions(+), 4 deletions(-) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 787ec09af256..1bd706c49106 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -29,8 +29,10 @@ import { import { DEBUG_BUILD } from '../common/debug-build'; import { registerBackgroundTabDetection } from './backgroundtab'; +import { addPerformanceInstrumentationHandler } from './instrument'; import { addPerformanceEntries, + startTrackingINP, startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals, @@ -38,6 +40,7 @@ import { import type { RequestInstrumentationOptions } from './request'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; import { WINDOW } from './types'; +import type { InteractionRouteNameMapping } from './web-vitals/types'; export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; @@ -127,6 +130,7 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { */ _experiments: Partial<{ enableInteractions: boolean; + enableInp: boolean; }>; /** @@ -181,6 +185,12 @@ export const browserTracingIntegration = ((_options: Partial { + for (const entry of entries) { + if (isPerformanceEventTiming(entry)) { + const duration = entry.duration; + const keys = Object.keys(interactionIdtoRouteNameMapping); + const minInteractionId = + keys.length > 0 + ? keys.reduce((a, b) => { + return interactionIdtoRouteNameMapping[a].duration < interactionIdtoRouteNameMapping[b].duration + ? a + : b; + }) + : undefined; + if (minInteractionId === undefined || duration > interactionIdtoRouteNameMapping[minInteractionId].duration) { + const interactionId = entry.interactionId; + const routeName = latestRoute.name; + if (interactionId && routeName) { + if (minInteractionId && Object.keys(interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete interactionIdtoRouteNameMapping[minInteractionId]; + } + interactionIdtoRouteNameMapping[interactionId] = { routeName, duration }; + } + } + } + } + }); +} + function getSource(context: TransactionContext): TransactionSource | undefined { const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // eslint-disable-next-line deprecation/deprecation diff --git a/packages/tracing-internal/src/browser/instrument.ts b/packages/tracing-internal/src/browser/instrument.ts index 2a4e7acaf3b1..085db9ca3a5d 100644 --- a/packages/tracing-internal/src/browser/instrument.ts +++ b/packages/tracing-internal/src/browser/instrument.ts @@ -3,12 +3,13 @@ import { getFunctionName, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../common/debug-build'; import { onCLS } from './web-vitals/getCLS'; import { onFID } from './web-vitals/getFID'; +import { onINP } from './web-vitals/getINP'; import { onLCP } from './web-vitals/getLCP'; import { observe } from './web-vitals/lib/observe'; type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource'; -type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid'; +type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'inp'; // We provide this here manually instead of relying on a global, as this is not available in non-browser environements // And we do not want to expose such types @@ -86,6 +87,7 @@ const instrumented: { [key in InstrumentHandlerType]?: boolean } = {}; let _previousCls: Metric | undefined; let _previousFid: Metric | undefined; let _previousLcp: Metric | undefined; +let _previousInp: Metric | undefined; /** * Add a callback that will be triggered when a CLS metric is available. @@ -123,9 +125,19 @@ export function addFidInstrumentationHandler(callback: (data: { metric: Metric } 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. + */ +export function addInpInstrumentationHandler( + callback: (data: { metric: Omit & { entries: PerformanceEventTiming[] } }) => void, +): CleanupHandlerCallback { + return addMetricObserver('inp', callback, instrumentInp, _previousInp); +} + export function addPerformanceInstrumentationHandler( type: 'event', - callback: (data: { entries: (PerformanceEntry & { target?: unknown | null })[] }) => void, + callback: (data: { entries: ((PerformanceEntry & { target?: unknown | null }) | PerformanceEventTiming)[] }) => void, ): CleanupHandlerCallback; export function addPerformanceInstrumentationHandler( type: InstrumentHandlerTypePerformanceObserver, @@ -199,6 +211,15 @@ function instrumentLcp(): StopListening { }); } +function instrumentInp(): void { + return onINP(metric => { + triggerHandlers('inp', { + metric, + }); + _previousInp = metric; + }); +} + function addMetricObserver( type: InstrumentHandlerTypeMetric, callback: InstrumentHandlerCallback, diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 4c9c25111e11..31106260dac1 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import type { IdleTransaction, Transaction } from '@sentry/core'; -import { getActiveTransaction, setMeasurement } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_MEASUREMENTS, Span, getActiveTransaction, getClient, setMeasurement } from '@sentry/core'; import type { Measurements, SpanContext } from '@sentry/types'; import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/utils'; @@ -9,14 +9,21 @@ import { DEBUG_BUILD } from '../../common/debug-build'; import { addClsInstrumentationHandler, addFidInstrumentationHandler, + addInpInstrumentationHandler, addLcpInstrumentationHandler, addPerformanceInstrumentationHandler, } from '../instrument'; import { WINDOW } from '../types'; import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher'; -import type { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; +import type { + InteractionRouteNameMapping, + NavigatorDeviceMemory, + NavigatorNetworkInformation, +} from '../web-vitals/types'; import { _startChild, isMeasurementValue } from './utils'; +import { createSpanEnvelope } from '@sentry/core'; + const MAX_INT_AS_BYTES = 2147483647; /** @@ -127,6 +134,22 @@ export function startTrackingInteractions(): void { }); } +/** + * Start tracking INP webvital events. + */ +export function startTrackingINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping): () => void { + const performance = getBrowserPerformanceAPI(); + if (performance && browserPerformanceTimeOrigin) { + const inpCallback = _trackINP(interactionIdtoRouteNameMapping); + + return (): void => { + inpCallback(); + }; + } + + return () => undefined; +} + /** Starts tracking the Cumulative Layout Shift on the current page. */ function _trackCLS(): () => void { return addClsInstrumentationHandler(({ metric }) => { @@ -171,6 +194,45 @@ function _trackFID(): () => void { }); } +/** Starts tracking the Interaction to Next Paint on the current page. */ +function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping): () => void { + return addInpInstrumentationHandler(({ metric }) => { + const entry = metric.entries.find(e => e.name === 'click'); + const client = getClient(); + if (!entry || !client) { + return; + } + const { release, environment } = client.getOptions(); + /** Build the INP span, create an envelope from the span, and then send the envelope */ + const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); + const duration = msToSec(metric.value); + const routeName = + entry.interactionId !== undefined ? interactionIdtoRouteNameMapping[entry.interactionId].routeName : undefined; + const span = new Span({ + startTimestamp: startTime, + endTimestamp: startTime + duration, + op: 'ui.interaction.click', + name: entry.target?.nodeName, + attributes: { + [SEMANTIC_ATTRIBUTE_MEASUREMENTS]: { + inp: { value: metric.value, unit: 'millisecond' }, + }, + release, + environment, + transaction: routeName, + }, + exclusiveTime: metric.value, + }); + const envelope = span ? createSpanEnvelope(span) : undefined; + const transport = client && client.getTransport(); + if (transport && envelope) { + transport.send(envelope).then(null, reason => { + DEBUG_BUILD && logger.error('Error while sending interaction:', reason); + }); + } + }); +} + /** Add performance related spans to a transaction */ export function addPerformanceEntries(transaction: Transaction): void { const performance = getBrowserPerformanceAPI(); diff --git a/packages/tracing-internal/src/browser/web-vitals/types.ts b/packages/tracing-internal/src/browser/web-vitals/types.ts index b4096b2678f6..fd4a31311074 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types.ts +++ b/packages/tracing-internal/src/browser/web-vitals/types.ts @@ -162,3 +162,5 @@ declare global { element?: Element; } } + +export type InteractionRouteNameMapping = { [key: string]: { routeName: string; duration: number } }; From 84a6021ab4f31be2df93e2b14603345d8245d450 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Mon, 19 Feb 2024 11:33:53 -0500 Subject: [PATCH 02/19] Adds sampling rate to inp spans --- .../src/browser/browserTracingIntegration.ts | 30 +++++-- .../src/browser/metrics/index.ts | 78 +++++++++++++++---- .../src/browser/web-vitals/types.ts | 5 +- 3 files changed, 91 insertions(+), 22 deletions(-) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 1bd706c49106..44870db9e7c5 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -198,9 +198,12 @@ export const browserTracingIntegration = ((_options: Partial { @@ -497,7 +503,7 @@ function registerInteractionListener( op, trimEnd: true, data: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.source || 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.context ? getSource(latestRoute.context) : undefined || 'url', }, }; @@ -527,7 +533,10 @@ const MAX_INTERACTIONS = 10; /** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */ function registerInpInteractionListener( interactionIdtoRouteNameMapping: InteractionRouteNameMapping, - latestRoute: { name: string | undefined; source: TransactionSource | undefined }, + latestRoute: { + name: string | undefined; + context: TransactionContext | undefined; + }, ): void { addPerformanceInstrumentationHandler('event', ({ entries }) => { for (const entry of entries) { @@ -545,12 +554,17 @@ function registerInpInteractionListener( if (minInteractionId === undefined || duration > interactionIdtoRouteNameMapping[minInteractionId].duration) { const interactionId = entry.interactionId; const routeName = latestRoute.name; - if (interactionId && routeName) { + const parentContext = latestRoute.context; + if (interactionId && 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 }; + interactionIdtoRouteNameMapping[interactionId] = { + routeName, + duration, + parentContext, + }; } } } diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 31106260dac1..df5f757e4ce7 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -1,7 +1,15 @@ /* eslint-disable max-lines */ import type { IdleTransaction, Transaction } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_MEASUREMENTS, Span, getActiveTransaction, getClient, setMeasurement } from '@sentry/core'; -import type { Measurements, SpanContext } from '@sentry/types'; +import { + SEMANTIC_ATTRIBUTE_MEASUREMENTS, + Span, + getActiveTransaction, + getClient, + hasTracingEnabled, + isValidSampleRate, + setMeasurement, +} from '@sentry/core'; +import type { ClientOptions, Measurements, SpanContext, TransactionContext } from '@sentry/types'; import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/utils'; import { spanToJSON } from '@sentry/core'; @@ -202,12 +210,14 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) if (!entry || !client) { return; } - const { release, environment } = client.getOptions(); + const options = client.getOptions(); /** Build the INP span, create an envelope from the span, and then send the envelope */ const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(metric.value); - const routeName = - entry.interactionId !== undefined ? interactionIdtoRouteNameMapping[entry.interactionId].routeName : undefined; + const { routeName, parentContext } = + entry.interactionId !== undefined + ? interactionIdtoRouteNameMapping[entry.interactionId] + : { routeName: undefined, parentContext: undefined }; const span = new Span({ startTimestamp: startTime, endTimestamp: startTime + duration, @@ -217,18 +227,28 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) [SEMANTIC_ATTRIBUTE_MEASUREMENTS]: { inp: { value: metric.value, unit: 'millisecond' }, }, - release, - environment, + release: options.release, + environment: options.environment, transaction: routeName, }, exclusiveTime: metric.value, }); - const envelope = span ? createSpanEnvelope(span) : undefined; - const transport = client && client.getTransport(); - if (transport && envelope) { - transport.send(envelope).then(null, reason => { - DEBUG_BUILD && logger.error('Error while sending interaction:', reason); - }); + + /** Check to see if the span should be sampled */ + const sampleRate = getSampleRate(parentContext, options); + if (!sampleRate) { + return; + } + + if (Math.random() < (sampleRate as number | boolean)) { + const envelope = span ? createSpanEnvelope(span) : 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; } }); } @@ -631,3 +651,35 @@ export function _addTtfbToMeasurements( } } } + +/** Taken from @sentry/core sampling.ts */ +function getSampleRate(transactionContext: TransactionContext | undefined, options: ClientOptions): number | boolean { + 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?.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 transaction because of invalid sample rate.'); + return false; + } + return sampleRate; +} diff --git a/packages/tracing-internal/src/browser/web-vitals/types.ts b/packages/tracing-internal/src/browser/web-vitals/types.ts index fd4a31311074..1f7d344401f6 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types.ts +++ b/packages/tracing-internal/src/browser/web-vitals/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import type { TransactionContext } from '@sentry/types'; import type { FirstInputPolyfillCallback } from './types/polyfills'; export * from './types/base'; @@ -163,4 +164,6 @@ declare global { } } -export type InteractionRouteNameMapping = { [key: string]: { routeName: string; duration: number } }; +export type InteractionRouteNameMapping = { + [key: string]: { routeName: string; duration: number; parentContext: TransactionContext }; +}; From 6bd6ff8845e1e8afc5af0351e3353ce779e422a0 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Mon, 19 Feb 2024 11:34:26 -0500 Subject: [PATCH 03/19] export isValidSampleRate --- packages/core/src/tracing/index.ts | 1 + packages/core/src/tracing/sampling.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index d1e1c7f65b44..998a73147822 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -27,3 +27,4 @@ export { } from './trace'; export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; +export { isValidSampleRate } from './sampling'; diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 427a7076d6d0..4b1bbef47d9d 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -103,7 +103,7 @@ export function sampleTransaction( /** * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). */ -function isValidSampleRate(rate: unknown): boolean { +export function isValidSampleRate(rate: unknown): boolean { // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck // eslint-disable-next-line @typescript-eslint/no-explicit-any if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) { From 5ee3045b92821c86c31d35135f9c554179f5a7a7 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 22 Feb 2024 16:38:59 -0500 Subject: [PATCH 04/19] fix --- packages/tracing-internal/src/browser/metrics/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 31106260dac1..d22584599e25 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -220,10 +220,10 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) release, environment, transaction: routeName, + exclusiveTime: metric.value, }, - exclusiveTime: metric.value, }); - const envelope = span ? createSpanEnvelope(span) : undefined; + const envelope = span ? createSpanEnvelope([span]) : undefined; const transport = client && client.getTransport(); if (transport && envelope) { transport.send(envelope).then(null, reason => { From 690ecc1df6822a044dbb36ce36864a115ff318c9 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Tue, 27 Feb 2024 10:03:11 -0500 Subject: [PATCH 05/19] snake case --- packages/tracing-internal/src/browser/metrics/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index d22584599e25..235228b139d5 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -220,7 +220,7 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) release, environment, transaction: routeName, - exclusiveTime: metric.value, + exclusive_time: metric.value, }, }); const envelope = span ? createSpanEnvelope([span]) : undefined; From 0559eabb3e0448f1c6e75bb46cdb08f923917ef9 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Wed, 28 Feb 2024 10:46:54 -0500 Subject: [PATCH 06/19] Adds profile id, replay id, and user to standalone INP spans if they exist --- packages/core/src/tracing/transaction.ts | 10 ++++++++++ .../src/browser/browserTracingIntegration.ts | 15 ++++++++++++++- .../src/browser/metrics/index.ts | 17 +++++++++++++++-- .../src/browser/web-vitals/types.ts | 11 +++++++++-- packages/types/src/transaction.ts | 6 ++++++ 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index a399137d1301..ee2b478e35a9 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -258,6 +258,16 @@ export class Transaction extends SpanClass implements TransactionInterface { this._hub = hub; } + /** + * Get the profile id of the transaction. + */ + public getProfileId(): string | undefined { + if (this._contexts !== undefined && this._contexts['profile'] !== undefined) { + return this._contexts['profile'].profile_id as string; + } + return undefined; + } + /** * Finish the transaction & prepare the event to send to Sentry. */ diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 24644569d071..11795a15a666 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import type { IdleTransaction } from '@sentry/core'; -import { getActiveSpan } from '@sentry/core'; +import { getActiveSpan, getClient, getCurrentScope } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -12,6 +12,7 @@ import { } from '@sentry/core'; import type { Client, + Integration, IntegrationFn, StartSpanOptions, Transaction, @@ -539,6 +540,15 @@ function registerInpInteractionListener( }, ): void { addPerformanceInstrumentationHandler('event', ({ 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?.getIntegrationByName?.('Replay') as + | (Integration & { getReplayId: () => string }) + | undefined; + // eslint-disable-next-line deprecation/deprecation + const activeTransaction = getActiveTransaction(); + const user = getCurrentScope()?.getUser(); for (const entry of entries) { if (isPerformanceEventTiming(entry)) { const duration = entry.duration; @@ -564,6 +574,9 @@ function registerInpInteractionListener( routeName, duration, parentContext, + user, + activeTransaction, + replay, }; } } diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 5a8144e2af6d..aaf73442129e 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -214,10 +214,20 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) /** Build the INP span, create an envelope from the span, and then send the envelope */ const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(metric.value); - const { routeName, parentContext } = + const { routeName, parentContext, activeTransaction, user, replay } = entry.interactionId !== undefined ? interactionIdtoRouteNameMapping[entry.interactionId] - : { routeName: undefined, parentContext: undefined }; + : { + routeName: undefined, + parentContext: undefined, + activeTransaction: undefined, + user: undefined, + replay: undefined, + }; + // eslint-disable-next-line deprecation/deprecation + const userDisplay = user?.email || user?.id || user?.ip_address; + const profileId = activeTransaction?.getProfileId(); + const replayId = replay?.getReplayId(); const span = new Span({ startTimestamp: startTime, endTimestamp: startTime + duration, @@ -231,6 +241,9 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) environment: options.environment, transaction: routeName, exclusive_time: metric.value, + ...(userDisplay !== undefined && userDisplay !== '' ? { user: userDisplay } : {}), + ...(profileId !== undefined ? { profile_id: profileId } : {}), + ...(replayId !== undefined ? { replay_id: replayId } : {}), }, }); diff --git a/packages/tracing-internal/src/browser/web-vitals/types.ts b/packages/tracing-internal/src/browser/web-vitals/types.ts index 1f7d344401f6..1e994b455f9a 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types.ts +++ b/packages/tracing-internal/src/browser/web-vitals/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { TransactionContext } from '@sentry/types'; +import type { Integration, Transaction, TransactionContext, User } from '@sentry/types'; import type { FirstInputPolyfillCallback } from './types/polyfills'; export * from './types/base'; @@ -165,5 +165,12 @@ declare global { } export type InteractionRouteNameMapping = { - [key: string]: { routeName: string; duration: number; parentContext: TransactionContext }; + [key: string]: { + routeName: string; + duration: number; + parentContext: TransactionContext; + user?: User; + activeTransaction?: Transaction; + replay?: Integration & { getReplayId: () => string }; + }; }; diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index fbcf8b38f02d..caae6c49027d 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -152,6 +152,12 @@ export interface Transaction extends TransactionContext, Omit; + + /** + * Get the profile id from the transaction + * @deprecated Use `toJSON()` or access the fields directly instead. + */ + getProfileId(): string | undefined; } /** From 9b6d5a73220351ee9267174521fbfb0de82a7559 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Wed, 28 Feb 2024 12:45:31 -0500 Subject: [PATCH 07/19] htmlTreeAsString span name --- packages/tracing-internal/src/browser/metrics/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 235228b139d5..c195d038af71 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -212,7 +212,7 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) startTimestamp: startTime, endTimestamp: startTime + duration, op: 'ui.interaction.click', - name: entry.target?.nodeName, + name: htmlTreeAsString(entry.target), attributes: { [SEMANTIC_ATTRIBUTE_MEASUREMENTS]: { inp: { value: metric.value, unit: 'millisecond' }, From bbdc8f032a64baa8be61cac87f5b8a9eb2986c9f Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Wed, 28 Feb 2024 17:39:12 -0500 Subject: [PATCH 08/19] pull profile id from attributes into top level because relay expects this --- packages/core/src/semanticAttributes.ts | 5 +++++ packages/core/src/tracing/span.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index 2cb2c59f473c..f595ef1aa39d 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -24,3 +24,8 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'sentry.origin'; * Use this attribute to represent measurements of a span. */ export const SEMANTIC_ATTRIBUTE_MEASUREMENTS = 'measurements'; + +/** + * The id of the profile that this span occured in. + */ +export const SEMANTIC_ATTRIBUTE_PROFILE_ID = 'profile_id'; diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index ca2c1f06cb57..2b18f38bbb18 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -20,6 +20,7 @@ import { DEBUG_BUILD } from '../debug-build'; import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_MEASUREMENTS, + SEMANTIC_ATTRIBUTE_PROFILE_ID, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, } from '../semanticAttributes'; @@ -632,6 +633,7 @@ export class Span implements SpanInterface { origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, _metrics_summary: getMetricSummaryJsonForSpan(this), measurements: this._attributes[SEMANTIC_ATTRIBUTE_MEASUREMENTS] as Measurements | undefined, + profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined, }); } From e5f21abaf06a610aeace98f2ed358360cff6c9ef Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 14:06:36 -0500 Subject: [PATCH 09/19] refactor out some optional chaining --- .../src/browser/browserTracingIntegration.ts | 10 ++++++---- packages/tracing-internal/src/browser/metrics/index.ts | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 11795a15a666..5e57e2b0243f 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -543,12 +543,14 @@ function registerInpInteractionListener( 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?.getIntegrationByName?.('Replay') as - | (Integration & { getReplayId: () => string }) - | undefined; + const replay = + client !== undefined && client.getIntegrationByName !== undefined + ? (client.getIntegrationByName('Replay') as Integration & { getReplayId: () => string }) + : undefined; // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); - const user = getCurrentScope()?.getUser(); + const currentScope = getCurrentScope(); + const user = currentScope !== undefined ? currentScope.getUser() : undefined; for (const entry of entries) { if (isPerformanceEventTiming(entry)) { const duration = entry.duration; diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index aaf73442129e..de069d6baa52 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -224,10 +224,10 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) user: undefined, replay: undefined, }; + const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined; // eslint-disable-next-line deprecation/deprecation - const userDisplay = user?.email || user?.id || user?.ip_address; - const profileId = activeTransaction?.getProfileId(); - const replayId = replay?.getReplayId(); + const profileId = activeTransaction !== undefined ? activeTransaction.getProfileId() : undefined; + const replayId = replay !== undefined ? replay.getReplayId() : undefined; const span = new Span({ startTimestamp: startTime, endTimestamp: startTime + duration, From 77f610b32aedbac0faf1c7d4ba6f5b633244d69e Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 14:33:10 -0500 Subject: [PATCH 10/19] update span creation --- packages/tracing-internal/src/browser/metrics/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index c195d038af71..26b86f8ef452 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import type { IdleTransaction, Transaction } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_MEASUREMENTS, Span, getActiveTransaction, getClient, setMeasurement } from '@sentry/core'; +import { Span, getActiveTransaction, getClient, setMeasurement } from '@sentry/core'; import type { Measurements, SpanContext } from '@sentry/types'; import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/utils'; @@ -214,13 +214,13 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) op: 'ui.interaction.click', name: htmlTreeAsString(entry.target), attributes: { - [SEMANTIC_ATTRIBUTE_MEASUREMENTS]: { - inp: { value: metric.value, unit: 'millisecond' }, - }, release, environment, transaction: routeName, - exclusive_time: metric.value, + }, + exclusiveTime: metric.value, + measurements: { + inp: { value: metric.value, unit: 'millisecond' }, }, }); const envelope = span ? createSpanEnvelope([span]) : undefined; From d943c8def648b43d119c75f61f61bf01505c937e Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 14:48:57 -0500 Subject: [PATCH 11/19] refactor optional check --- packages/tracing-internal/src/browser/metrics/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 03f5a7a0c2ff..d51a9dfbb7c4 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -669,7 +669,7 @@ function getSampleRate(transactionContext: TransactionContext | undefined, optio }, location: WINDOW.location, }); - } else if (transactionContext?.sampled !== undefined) { + } else if (transactionContext !== undefined && transactionContext.sampled !== undefined) { sampleRate = transactionContext.sampled; } else if (typeof options.tracesSampleRate !== 'undefined') { sampleRate = options.tracesSampleRate; From 059508c2e62deac96fbf5aa0206d55950667f747 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 15:20:39 -0500 Subject: [PATCH 12/19] increase size limit by 1 kb --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 51f1e0c711f7..ef140e160170 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -52,7 +52,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: '{ init, browserTracingIntegration }', gzip: true, - limit: '35 KB', + limit: '36 KB', }, { name: '@sentry/browser (incl. Feedback) - Webpack (gzipped)', From 65bb1acff2ebdf83ae766ad2c691bd1042176b62 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 15:39:52 -0500 Subject: [PATCH 13/19] todo comment and update interactionIdtoRouteNameMapping replay to replayid --- packages/browser/src/profiling/utils.ts | 3 +++ .../src/browser/browserTracingIntegration.ts | 3 ++- packages/tracing-internal/src/browser/metrics/index.ts | 5 ++--- packages/tracing-internal/src/browser/web-vitals/types.ts | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 9114884384b7..6ebec8511214 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -583,6 +583,9 @@ export function createProfilingEvent( return createProfilePayload(profile_id, start_timestamp, profile, event); } +// TODO (v8): We need to obtain profile ids in @sentry-internal/tracing, +// but we don't have access to this map because importing this map would +// cause a circular dependancy. We need to resolve this in v8. const PROFILE_MAP: Map = new Map(); /** * diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 5e57e2b0243f..47bad615586d 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -547,6 +547,7 @@ function registerInpInteractionListener( client !== undefined && client.getIntegrationByName !== undefined ? (client.getIntegrationByName('Replay') as Integration & { getReplayId: () => string }) : undefined; + const replayId = replay !== undefined ? replay.getReplayId() : undefined; // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); const currentScope = getCurrentScope(); @@ -578,7 +579,7 @@ function registerInpInteractionListener( parentContext, user, activeTransaction, - replay, + replayId, }; } } diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 617405606726..b9c08f7dffaf 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -213,7 +213,7 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) /** Build the INP span, create an envelope from the span, and then send the envelope */ const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(metric.value); - const { routeName, parentContext, activeTransaction, user, replay } = + const { routeName, parentContext, activeTransaction, user, replayId } = entry.interactionId !== undefined ? interactionIdtoRouteNameMapping[entry.interactionId] : { @@ -221,12 +221,11 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) parentContext: undefined, activeTransaction: undefined, user: undefined, - replay: undefined, + replayId: undefined, }; 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 replayId = replay !== undefined ? replay.getReplayId() : undefined; const span = new Span({ startTimestamp: startTime, endTimestamp: startTime + duration, diff --git a/packages/tracing-internal/src/browser/web-vitals/types.ts b/packages/tracing-internal/src/browser/web-vitals/types.ts index 1e994b455f9a..e82296157103 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types.ts +++ b/packages/tracing-internal/src/browser/web-vitals/types.ts @@ -171,6 +171,6 @@ export type InteractionRouteNameMapping = { parentContext: TransactionContext; user?: User; activeTransaction?: Transaction; - replay?: Integration & { getReplayId: () => string }; + replayId?: string; }; }; From 9782eafae36355c60c949a1a99c6af2005c0beb0 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 15:45:17 -0500 Subject: [PATCH 14/19] comment --- .../tracing-internal/src/browser/browserTracingIntegration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 068e545d5996..a6bf345ed8c4 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -522,6 +522,7 @@ function isPerformanceEventTiming(entry: PerformanceEntry): entry is Performance 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 */ From ac749fbb1e1489f8b41ab3fb93c1e28b6c9757e4 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 15:49:03 -0500 Subject: [PATCH 15/19] fix import --- packages/tracing-internal/src/browser/web-vitals/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tracing-internal/src/browser/web-vitals/types.ts b/packages/tracing-internal/src/browser/web-vitals/types.ts index e82296157103..adc95084bda9 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types.ts +++ b/packages/tracing-internal/src/browser/web-vitals/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Integration, Transaction, TransactionContext, User } from '@sentry/types'; +import type { Transaction, TransactionContext, User } from '@sentry/types'; import type { FirstInputPolyfillCallback } from './types/polyfills'; export * from './types/base'; From a3f235fe068c6cc2c0c302ccc8f279acba1c31bf Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 16:11:58 -0500 Subject: [PATCH 16/19] move enableInp off experiment --- .../src/browser/browserTracingIntegration.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 19a80a373984..01dda5bcdcfc 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -107,6 +107,13 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { */ enableLongTask: boolean; + /** + * If true, Sentry will capture INP web vitals as standalone spans . + * + * Default: false + */ + enableInp: boolean; + /** * _metricOptions allows the user to send options to change how metrics are collected. * @@ -131,7 +138,6 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { */ _experiments: Partial<{ enableInteractions: boolean; - enableInp: boolean; }>; /** @@ -147,6 +153,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { instrumentPageLoad: true, markBackgroundSpan: true, enableLongTask: true, + enableInp: false, _experiments: {}, ...defaultRequestInstrumentationOptions, }; @@ -188,7 +195,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Thu, 29 Feb 2024 16:26:00 -0500 Subject: [PATCH 17/19] performanceeventtiming interface --- packages/tracing-internal/src/browser/instrument.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/tracing-internal/src/browser/instrument.ts b/packages/tracing-internal/src/browser/instrument.ts index 085db9ca3a5d..79b03258de1c 100644 --- a/packages/tracing-internal/src/browser/instrument.ts +++ b/packages/tracing-internal/src/browser/instrument.ts @@ -20,6 +20,13 @@ interface PerformanceEntry { readonly startTime: number; toJSON(): Record; } +interface PerformanceEventTiming extends PerformanceEntry { + processingStart: DOMHighResTimeStamp; + processingEnd: DOMHighResTimeStamp; + duration: DOMHighResTimeStamp; + cancelable?: boolean; + target?: Element; +} interface Metric { /** From 226c5436660538ce307e282afb9ca9b04098d56f Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 16:30:09 -0500 Subject: [PATCH 18/19] interactionId --- packages/tracing-internal/src/browser/instrument.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tracing-internal/src/browser/instrument.ts b/packages/tracing-internal/src/browser/instrument.ts index 79b03258de1c..a6a1f2c12298 100644 --- a/packages/tracing-internal/src/browser/instrument.ts +++ b/packages/tracing-internal/src/browser/instrument.ts @@ -26,6 +26,7 @@ interface PerformanceEventTiming extends PerformanceEntry { duration: DOMHighResTimeStamp; cancelable?: boolean; target?: Element; + interactionId?: number; } interface Metric { From fc676fc82afcd213c8004ad8a303836aba8022f9 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Thu, 29 Feb 2024 16:43:43 -0500 Subject: [PATCH 19/19] use more primitive types --- packages/tracing-internal/src/browser/instrument.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tracing-internal/src/browser/instrument.ts b/packages/tracing-internal/src/browser/instrument.ts index a6a1f2c12298..10c96a0c5aa3 100644 --- a/packages/tracing-internal/src/browser/instrument.ts +++ b/packages/tracing-internal/src/browser/instrument.ts @@ -21,11 +21,11 @@ interface PerformanceEntry { toJSON(): Record; } interface PerformanceEventTiming extends PerformanceEntry { - processingStart: DOMHighResTimeStamp; - processingEnd: DOMHighResTimeStamp; - duration: DOMHighResTimeStamp; + processingStart: number; + processingEnd: number; + duration: number; cancelable?: boolean; - target?: Element; + target?: unknown | null; interactionId?: number; }