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): create Interaction standalone spans on inp events #10709

Merged
merged 37 commits into from Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2d83f03
Merge branch 'egou/v7/feat/add-span-envelope-and-datacategory' of git…
edwardgou-sentry Feb 18, 2024
abe1bc2
Merge branch 'egou/v7/feat/add-exclusive-time-and-measurements-to-spa…
edwardgou-sentry Feb 18, 2024
9ae7f95
Creates interaction spans with inp when inp is detected
edwardgou-sentry Feb 18, 2024
84a6021
Adds sampling rate to inp spans
edwardgou-sentry Feb 19, 2024
6bd6ff8
export isValidSampleRate
edwardgou-sentry Feb 19, 2024
97ece4e
Merge branch 'v7' of github.com:getsentry/sentry-javascript into egou…
edwardgou-sentry Feb 21, 2024
ccd8ec1
Merge branch 'egou/v7/fix/browser-tracing-latest-route' of github.com…
edwardgou-sentry Feb 21, 2024
b2e4656
Merge branch 'egou/v7/feat/add-exclusive-time-and-measurements-to-spa…
edwardgou-sentry Feb 21, 2024
5d53fa4
Merge branch 'egou/v7/feat/add-span-envelope-and-datacategory' of git…
edwardgou-sentry Feb 21, 2024
c2aa318
Merge branch 'egou/v7/feat/create-interaction-spans-on-inp' of github…
edwardgou-sentry Feb 22, 2024
5ee3045
fix
edwardgou-sentry Feb 22, 2024
9e24b2b
Merge branch 'egou/v7/feat/add-span-envelope-and-datacategory' of git…
edwardgou-sentry Feb 26, 2024
690ecc1
snake case
edwardgou-sentry Feb 27, 2024
a937852
Merge branch 'egou/v7/feat/create-interaction-spans-on-inp' of github…
edwardgou-sentry Feb 27, 2024
0559eab
Adds profile id, replay id, and user to standalone INP spans if they …
edwardgou-sentry Feb 28, 2024
9b6d5a7
htmlTreeAsString span name
edwardgou-sentry Feb 28, 2024
6fddd84
Merge branch 'v7' of github.com:getsentry/sentry-javascript into egou…
edwardgou-sentry Feb 28, 2024
19cf74a
Merge branch 'egou/v7/feat/add-exclusive-time-and-measurements-to-spa…
edwardgou-sentry Feb 28, 2024
bbdc8f0
pull profile id from attributes into top level because relay expects …
edwardgou-sentry Feb 28, 2024
e5f21ab
refactor out some optional chaining
edwardgou-sentry Feb 29, 2024
9a1b12e
Merge branch 'egou/v7/feat/add-exclusive-time-and-measurements-to-spa…
edwardgou-sentry Feb 29, 2024
77f610b
update span creation
edwardgou-sentry Feb 29, 2024
05f55f1
Merge branch 'egou/v7/feat/create-interaction-spans-on-inp' of github…
edwardgou-sentry Feb 29, 2024
d943c8d
refactor optional check
edwardgou-sentry Feb 29, 2024
9e04846
Merge branch 'v7' of github.com:getsentry/sentry-javascript into egou…
edwardgou-sentry Feb 29, 2024
8b929bc
Merge branch 'egou/v7/feat/add-sampling-rate-to-inp-spans' of github.…
edwardgou-sentry Feb 29, 2024
059508c
increase size limit by 1 kb
edwardgou-sentry Feb 29, 2024
65bb1ac
todo comment and update interactionIdtoRouteNameMapping replay to rep…
edwardgou-sentry Feb 29, 2024
9782eaf
comment
edwardgou-sentry Feb 29, 2024
ac749fb
fix import
edwardgou-sentry Feb 29, 2024
3336ff3
feat(webvitals): Add profile id, replay id, and user to standalone IN…
edwardgou-sentry Feb 29, 2024
b515f8e
feat(performance): Add sampling rate to INP spans. Also add replay id…
edwardgou-sentry Feb 29, 2024
93f5c19
Merge branch 'v7' of github.com:getsentry/sentry-javascript into egou…
edwardgou-sentry Feb 29, 2024
a3f235f
move enableInp off experiment
edwardgou-sentry Feb 29, 2024
5807e2d
performanceeventtiming interface
edwardgou-sentry Feb 29, 2024
226c543
interactionId
edwardgou-sentry Feb 29, 2024
fc676fc
use more primitive types
edwardgou-sentry Feb 29, 2024
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
5 changes: 5 additions & 0 deletions packages/core/src/semanticAttributes.ts
Expand Up @@ -19,3 +19,8 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_OP = 'sentry.op';
* Use this attribute to represent the origin of a span.
*/
export const SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'sentry.origin';

/**
* Use this attribute to represent measurements of a span.
*/
export const SEMANTIC_ATTRIBUTE_MEASUREMENTS = 'measurements';
13 changes: 12 additions & 1 deletion packages/core/src/tracing/span.ts
@@ -1,6 +1,7 @@
/* eslint-disable max-lines */
import type {
Instrumenter,
Measurements,
Primitive,
Span as SpanInterface,
SpanAttributeValue,
Expand All @@ -17,7 +18,11 @@ import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/ut

import { DEBUG_BUILD } from '../debug-build';
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
import {
SEMANTIC_ATTRIBUTE_MEASUREMENTS,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
} from '../semanticAttributes';
import { getRootSpan } from '../utils/getRootSpan';
import {
TRACE_FLAG_NONE,
Expand Down Expand Up @@ -115,6 +120,7 @@ export class Span implements SpanInterface {
protected _endTime?: number;
/** Internal keeper of the status */
protected _status?: SpanStatusType | string;
protected _exclusiveTime?: number;

private _logMessage?: string;

Expand Down Expand Up @@ -159,6 +165,9 @@ export class Span implements SpanInterface {
if (spanContext.endTimestamp) {
this._endTime = spanContext.endTimestamp;
}
if (spanContext.exclusiveTime) {
this._exclusiveTime = spanContext.exclusiveTime;
}
}

// This rule conflicts with another eslint rule :(
Expand Down Expand Up @@ -626,6 +635,8 @@ export class Span implements SpanInterface {
trace_id: this._traceId,
origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined,
_metrics_summary: getMetricSummaryJsonForSpan(this),
exclusive_time: this._exclusiveTime,
measurements: this._attributes[SEMANTIC_ATTRIBUTE_MEASUREMENTS] as Measurements | undefined,
});
}

Expand Down
75 changes: 65 additions & 10 deletions packages/tracing-internal/src/browser/browserTracingIntegration.ts
Expand Up @@ -29,15 +29,18 @@ import {

import { DEBUG_BUILD } from '../common/debug-build';
import { registerBackgroundTabDetection } from './backgroundtab';
import { addPerformanceInstrumentationHandler } from './instrument';
import {
addPerformanceEntries,
startTrackingINP,
startTrackingInteractions,
startTrackingLongTasks,
startTrackingWebVitals,
} from './metrics';
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';

Expand Down Expand Up @@ -127,6 +130,7 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
*/
_experiments: Partial<{
enableInteractions: boolean;
enableInp: boolean;
}>;

/**
Expand Down Expand Up @@ -181,15 +185,23 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio

const _collectWebVitals = startTrackingWebVitals();

/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
const interactionIdtoRouteNameMapping: InteractionRouteNameMapping = {};
if (options._experiments.enableInp) {
startTrackingINP(interactionIdtoRouteNameMapping);
}

if (options.enableLongTask) {
startTrackingLongTasks();
}
if (options._experiments.enableInteractions) {
startTrackingInteractions();
}

let latestRouteName: string | undefined;
let latestRouteSource: TransactionSource | undefined;
const latestRoute: { name: string | undefined; source: TransactionSource | undefined } = {
name: undefined,
source: undefined,
};

/** Create routing idle transaction. */
function _createRouteTransaction(context: TransactionContext): Transaction | undefined {
Expand Down Expand Up @@ -235,8 +247,8 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
: // eslint-disable-next-line deprecation/deprecation
finalContext.metadata;

latestRouteName = finalContext.name;
latestRouteSource = getSource(finalContext);
latestRoute.name = finalContext.name;
latestRoute.source = getSource(finalContext);

if (finalContext.sampled === false) {
DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
Expand Down Expand Up @@ -384,7 +396,11 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
}

if (_experiments.enableInteractions) {
registerInteractionListener(options, latestRouteName, latestRouteSource);
registerInteractionListener(options, latestRoute);
}

if (_experiments.enableInp) {
registerInpInteractionListener(interactionIdtoRouteNameMapping, latestRoute);
}

instrumentOutgoingRequests({
Expand Down Expand Up @@ -446,8 +462,7 @@ export function getMetaContent(metaName: string): string | undefined {
/** Start listener for interaction transactions */
function registerInteractionListener(
options: BrowserTracingOptions,
latestRouteName: string | undefined,
latestRouteSource: TransactionSource | undefined,
latestRoute: { name: string | undefined; source: TransactionSource | undefined },
): void {
let inflightInteractionTransaction: IdleTransaction | undefined;
const registerInteractionTransaction = (): void => {
Expand All @@ -470,19 +485,19 @@ function registerInteractionListener(
inflightInteractionTransaction = undefined;
}

if (!latestRouteName) {
if (!latestRoute.name) {
DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
return undefined;
}

const { location } = WINDOW;

const context: TransactionContext = {
name: latestRouteName,
name: latestRoute.name,
op,
trimEnd: true,
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRouteSource || 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.source || 'url',
},
};

Expand All @@ -503,6 +518,46 @@ function registerInteractionListener(
});
}

function isPerformanceEventTiming(entry: PerformanceEntry): entry is PerformanceEventTiming {
return 'duration' in entry;
}

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 },
): void {
addPerformanceInstrumentationHandler('event', ({ entries }) => {
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
Expand Down
25 changes: 23 additions & 2 deletions packages/tracing-internal/src/browser/instrument.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Metric, 'entries'> & { 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,
Expand Down Expand Up @@ -199,6 +211,15 @@ function instrumentLcp(): StopListening {
});
}

function instrumentInp(): void {
return onINP(metric => {
triggerHandlers('inp', {
metric,
});
_previousInp = metric;
});
}

function addMetricObserver(
type: InstrumentHandlerTypeMetric,
callback: InstrumentHandlerCallback,
Expand Down
66 changes: 64 additions & 2 deletions 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';

Expand All @@ -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;

/**
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think nodeName is correct. Need to update this to get some identifier for the target interaction element

attributes: {
[SEMANTIC_ATTRIBUTE_MEASUREMENTS]: {
inp: { value: metric.value, unit: 'millisecond' },
},
release,
environment,
transaction: routeName,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing the origin route name of the interaction as transaction

},
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();
Expand Down
2 changes: 2 additions & 0 deletions packages/tracing-internal/src/browser/web-vitals/types.ts
Expand Up @@ -162,3 +162,5 @@ declare global {
element?: Element;
}
}

export type InteractionRouteNameMapping = { [key: string]: { routeName: string; duration: number } };