Skip to content

fix(browser): Handle more edge cases with INP #12378

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/browser-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {
startTrackingLongTasks,
startTrackingWebVitals,
startTrackingINP,
registerInpInteractionListener,
} from './metrics/browserMetrics';

export { addClickKeypressInstrumentationHandler } from './instrument/dom';
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/metrics/browserMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export function startTrackingInteractions(): void {
});
}

export { startTrackingINP } from './inp';
export { startTrackingINP, registerInpInteractionListener } from './inp';

/** Starts tracking the Cumulative Layout Shift on the current page. */
function _trackCLS(): () => void {
Expand Down
63 changes: 59 additions & 4 deletions packages/browser-utils/src/metrics/inp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,21 @@ import {
} from '@sentry/core';
import type { Integration, SpanAttributes } from '@sentry/types';
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString } from '@sentry/utils';
import { addInpInstrumentationHandler } from './instrument';
import {
addInpInstrumentationHandler,
addPerformanceInstrumentationHandler,
isPerformanceEventTiming,
} from './instrument';
import { getBrowserPerformanceAPI, msToSec } from './utils';

// We only care about name here
interface PartialRouteInfo {
name: string | undefined;
}

const LAST_INTERACTIONS: number[] = [];
const INTERACTIONS_ROUTE_MAP = new Map<number, string>();

/**
* Start tracking INP webvital events.
*/
Expand Down Expand Up @@ -74,6 +86,7 @@ function _trackINP(): () => void {
return;
}

const { interactionId } = entry;
const interactionType = INP_ENTRY_MAP[entry.name];

const options = client.getOptions();
Expand All @@ -84,9 +97,15 @@ function _trackINP(): () => void {
const activeSpan = getActiveSpan();
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;

// If there is no active span, we fall back to look at the transactionName on the scope
// This is set if the pageload/navigation span is already finished,
const routeName = rootSpan ? spanToJSON(rootSpan).description : scope.getScopeData().transactionName;
// We first try to lookup the route name from our INTERACTIONS_ROUTE_MAP,
// where we cache the route per interactionId
const cachedRouteName = interactionId != null ? INTERACTIONS_ROUTE_MAP.get(interactionId) : undefined;

// Else, we try to use the active span.
// Finally, we fall back to look at the transactionName on the scope
const routeName =
cachedRouteName || (rootSpan ? spanToJSON(rootSpan).description : scope.getScopeData().transactionName);

const user = scope.getUser();

// We need to get the replay, user, and activeTransaction from the current scope
Expand Down Expand Up @@ -134,3 +153,39 @@ function _trackINP(): () => void {
span.end(startTime + duration);
});
}

/** Register a listener to cache route information for INP interactions. */
export function registerInpInteractionListener(latestRoute: PartialRouteInfo): void {
const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => {
entries.forEach(entry => {
if (!isPerformanceEventTiming(entry) || !latestRoute.name) {
return;
}

const interactionId = entry.interactionId;
if (interactionId == null) {
return;
}

// If the interaction was already recorded before, nothing more to do
if (INTERACTIONS_ROUTE_MAP.has(interactionId)) {
return;
}

// We keep max. 10 interactions in the list, then remove the oldest one & clean up
if (LAST_INTERACTIONS.length > 10) {
const last = LAST_INTERACTIONS.shift() as number;
INTERACTIONS_ROUTE_MAP.delete(last);
}

// We add the interaction to the list of recorded interactions
// and store the route information for this interaction
// (we clone the object because it is mutated when it changes)
LAST_INTERACTIONS.push(interactionId);
INTERACTIONS_ROUTE_MAP.set(interactionId, latestRoute.name);
});
};

addPerformanceInstrumentationHandler('event', handleEntries);
addPerformanceInstrumentationHandler('first-input', handleEntries);
}
15 changes: 14 additions & 1 deletion packages/browser-utils/src/metrics/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { onLCP } from './web-vitals/getLCP';
import { observe } from './web-vitals/lib/observe';
import { onTTFB } from './web-vitals/onTTFB';

type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource';
type InstrumentHandlerTypePerformanceObserver =
| 'longtask'
| 'event'
| 'navigation'
| 'paint'
| 'resource'
| 'first-input';

type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'ttfb' | 'inp';

Expand Down Expand Up @@ -319,3 +325,10 @@ function getCleanupCallback(
}
};
}

/**
* Check if a PerformanceEntry is a PerformanceEventTiming by checking for the `duration` property.
*/
export function isPerformanceEventTiming(entry: PerformanceEntry): entry is PerformanceEventTiming {
return 'duration' in entry;
}
14 changes: 12 additions & 2 deletions packages/browser/src/tracing/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
addHistoryInstrumentationHandler,
addPerformanceEntries,
registerInpInteractionListener,
startTrackingINP,
startTrackingInteractions,
startTrackingLongTasks,
Expand Down Expand Up @@ -40,6 +41,11 @@ import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from

export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';

interface RouteInfo {
name: string | undefined;
source: TransactionSource | undefined;
}

/** Options for Browser Tracing integration */
export interface BrowserTracingOptions {
/**
Expand Down Expand Up @@ -204,7 +210,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
startTrackingInteractions();
}

const latestRoute: { name: string | undefined; source: TransactionSource | undefined } = {
const latestRoute: RouteInfo = {
name: undefined,
source: undefined,
};
Expand Down Expand Up @@ -375,6 +381,10 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
registerInteractionListener(idleTimeout, finalTimeout, childSpanTimeout, latestRoute);
}

if (enableInp) {
registerInpInteractionListener(latestRoute);
}

instrumentOutgoingRequests({
traceFetch,
traceXHR,
Expand Down Expand Up @@ -439,7 +449,7 @@ function registerInteractionListener(
idleTimeout: BrowserTracingOptions['idleTimeout'],
finalTimeout: BrowserTracingOptions['finalTimeout'],
childSpanTimeout: BrowserTracingOptions['childSpanTimeout'],
latestRoute: { name: string | undefined; source: TransactionSource | undefined },
latestRoute: RouteInfo,
): void {
let inflightInteractionSpan: Span | undefined;
const registerInteractionTransaction = (): void => {
Expand Down
Loading