Skip to content

Commit

Permalink
feat(performance): Port INP span instrumentation to old browser traci…
Browse files Browse the repository at this point in the history
…ng (#11085)

Adds INP span instrumentation to the old `BrowserTracing` integration.
  • Loading branch information
edwardgou-sentry committed Mar 13, 2024
1 parent a443a55 commit f8bad8d
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .size-limit.js
Expand Up @@ -45,7 +45,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: '{ init, BrowserTracing }',
gzip: true,
limit: '35 KB',
limit: '37 KB',
},
{
name: '@sentry/browser (incl. browserTracingIntegration) - Webpack (gzipped)',
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/span.ts
@@ -1,15 +1,19 @@
import type { SpanEnvelope, SpanItem } from '@sentry/types';
import type { DsnComponents, SpanEnvelope, SpanItem } from '@sentry/types';
import type { Span } from '@sentry/types';
import { createEnvelope } from '@sentry/utils';
import { createEnvelope, dsnToString } from '@sentry/utils';

/**
* Create envelope from Span item.
*/
export function createSpanEnvelope(spans: Span[]): SpanEnvelope {
export function createSpanEnvelope(spans: Span[], dsn?: DsnComponents): SpanEnvelope {
const headers: SpanEnvelope[0] = {
sent_at: new Date().toISOString(),
};

if (dsn) {
headers.dsn = dsnToString(dsn);
}

const items = spans.map(createSpanItem);
return createEnvelope<SpanEnvelope>(headers, items);
}
Expand Down
109 changes: 101 additions & 8 deletions packages/tracing-internal/src/browser/browsertracing.ts
@@ -1,5 +1,6 @@
/* eslint-disable max-lines */
import type { Hub, IdleTransaction } from '@sentry/core';
import { getClient, getCurrentScope } from '@sentry/core';
import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
TRACING_DEFAULTS,
Expand All @@ -12,8 +13,10 @@ import { getDomElement, logger, propagationContextFromHeaders } from '@sentry/ut

import { DEBUG_BUILD } from '../common/debug-build';
import { registerBackgroundTabDetection } from './backgroundtab';
import { addPerformanceInstrumentationHandler } from './instrument';
import {
addPerformanceEntries,
startTrackingINP,
startTrackingInteractions,
startTrackingLongTasks,
startTrackingWebVitals,
Expand All @@ -22,6 +25,7 @@ import type { RequestInstrumentationOptions } from './request';
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request';
import { instrumentRoutingWithDefaults } from './router';
import { WINDOW } from './types';
import type { InteractionRouteNameMapping } from './web-vitals/types';

export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';

Expand Down Expand Up @@ -87,6 +91,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.
*
Expand Down Expand Up @@ -146,10 +157,14 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
startTransactionOnLocationChange: true,
startTransactionOnPageLoad: true,
enableLongTask: true,
enableInp: false,
_experiments: {},
...defaultRequestInstrumentationOptions,
};

/** We store up to 10 interaction candidates max to cap memory usage. This is the same cap as getINP from web-vitals */
const MAX_INTERACTIONS = 10;

/**
* The Browser Tracing integration automatically instruments browser pageload/navigation
* actions as transactions, and captures requests, metrics and errors as spans.
Expand All @@ -175,12 +190,14 @@ export class BrowserTracing implements Integration {

private _getCurrentHub?: () => Hub;

private _latestRouteName?: string;
private _latestRouteSource?: TransactionSource;

private _collectWebVitals: () => void;

private _hasSetTracePropagationTargets: boolean;
private _interactionIdtoRouteNameMapping: InteractionRouteNameMapping;
private _latestRoute: {
name: string | undefined;
context: TransactionContext | undefined;
};

public constructor(_options?: Partial<BrowserTracingOptions>) {
this.name = BROWSER_TRACING_INTEGRATION_ID;
Expand Down Expand Up @@ -217,12 +234,23 @@ export class BrowserTracing implements Integration {
}

this._collectWebVitals = startTrackingWebVitals();
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
this._interactionIdtoRouteNameMapping = {};

if (this.options.enableInp) {
startTrackingINP(this._interactionIdtoRouteNameMapping);
}
if (this.options.enableLongTask) {
startTrackingLongTasks();
}
if (this.options._experiments.enableInteractions) {
startTrackingInteractions();
}

this._latestRoute = {
name: undefined,
context: undefined,
};
}

/**
Expand Down Expand Up @@ -287,6 +315,10 @@ export class BrowserTracing implements Integration {
this._registerInteractionListener();
}

if (this.options.enableInp) {
this._registerInpInteractionListener();
}

instrumentOutgoingRequests({
traceFetch,
traceXHR,
Expand Down Expand Up @@ -349,8 +381,8 @@ export class BrowserTracing implements Integration {
: // eslint-disable-next-line deprecation/deprecation
finalContext.metadata;

this._latestRouteName = finalContext.name;
this._latestRouteSource = getSource(finalContext);
this._latestRoute.name = finalContext.name;
this._latestRoute.context = finalContext;

// eslint-disable-next-line deprecation/deprecation
if (finalContext.sampled === false) {
Expand Down Expand Up @@ -420,7 +452,7 @@ export class BrowserTracing implements Integration {
return undefined;
}

if (!this._latestRouteName) {
if (!this._latestRoute.name) {
DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
return undefined;
}
Expand All @@ -429,11 +461,13 @@ export class BrowserTracing implements Integration {
const { location } = WINDOW;

const context: TransactionContext = {
name: this._latestRouteName,
name: this._latestRoute.name,
op,
trimEnd: true,
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: this._latestRouteSource || 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: this._latestRoute.context
? getSource(this._latestRoute.context)
: undefined || 'url',
},
};

Expand All @@ -452,6 +486,61 @@ export class BrowserTracing implements Integration {
addEventListener(type, registerInteractionTransaction, { once: false, capture: true });
});
}

/** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
private _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 !== 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();
const user = currentScope !== undefined ? currentScope.getUser() : undefined;
for (const entry of entries) {
if (isPerformanceEventTiming(entry)) {
const duration = entry.duration;
const keys = Object.keys(this._interactionIdtoRouteNameMapping);
const minInteractionId =
keys.length > 0
? keys.reduce((a, b) => {
return this._interactionIdtoRouteNameMapping[a].duration <
this._interactionIdtoRouteNameMapping[b].duration
? a
: b;
})
: undefined;
if (
minInteractionId === undefined ||
duration > this._interactionIdtoRouteNameMapping[minInteractionId].duration
) {
const interactionId = entry.interactionId;
const routeName = this._latestRoute.name;
const parentContext = this._latestRoute.context;
if (interactionId && routeName && parentContext) {
if (minInteractionId && Object.keys(this._interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this._interactionIdtoRouteNameMapping[minInteractionId];
}
this._interactionIdtoRouteNameMapping[interactionId] = {
routeName,
duration,
parentContext,
user,
activeTransaction,
replayId,
};
}
}
}
}
});
}
}

/** Returns the value of a meta tag */
Expand All @@ -473,3 +562,7 @@ function getSource(context: TransactionContext): TransactionSource | undefined {

return sourceFromAttributes || sourceFromData || sourceFromMetadata;
}

function isPerformanceEventTiming(entry: PerformanceEntry): entry is PerformanceEventTiming {
return 'duration' in entry;
}
4 changes: 2 additions & 2 deletions packages/tracing-internal/src/browser/metrics/index.ts
Expand Up @@ -204,7 +204,7 @@ 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 entry = metric.entries.find(e => e.name === 'click' || e.name === 'pointerdown');
const client = getClient();
if (!entry || !client) {
return;
Expand Down Expand Up @@ -252,7 +252,7 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping)
}

if (Math.random() < (sampleRate as number | boolean)) {
const envelope = span ? createSpanEnvelope([span]) : undefined;
const envelope = span ? createSpanEnvelope([span], client.getDsn()) : undefined;
const transport = client && client.getTransport();
if (transport && envelope) {
transport.send(envelope).then(null, reason => {
Expand Down
3 changes: 3 additions & 0 deletions packages/tracing-internal/test/browser/browsertracing.test.ts
Expand Up @@ -91,6 +91,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => {
const browserTracing = createBrowserTracing();

expect(browserTracing.options).toEqual({
enableInp: false,
enableLongTask: true,
_experiments: {},
...TRACING_DEFAULTS,
Expand All @@ -110,6 +111,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => {
});

expect(browserTracing.options).toEqual({
enableInp: false,
enableLongTask: false,
...TRACING_DEFAULTS,
markBackgroundTransactions: true,
Expand All @@ -129,6 +131,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => {
});

expect(browserTracing.options).toEqual({
enableInp: false,
enableLongTask: false,
_experiments: {},
...TRACING_DEFAULTS,
Expand Down

0 comments on commit f8bad8d

Please sign in to comment.