Skip to content
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

feat(performance): Port INP span instrumentation to old browser tracing #11085

Merged
merged 3 commits into from
Mar 13, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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