Skip to content

Commit b515f8e

Browse files
feat(performance): Add sampling rate to INP spans. Also add replay id, profile id, and user.
Updates INP spans to check for sampling rate, similar to transactions. Also Adds profile id, replay id, and user to standalone INP spans. User comes from the current scope. Replay Id is retrieved from the relay integration module and calling getReplayId(). Profile Id is retrieved from getting the active transaction at the time of the interaction Since profile id isn't added to the transaction until the transaction ends, we need to hold onto a reference to the transaction instead of trying to grab the profile id right away
2 parents 9782eaf + 3336ff3 commit b515f8e

File tree

10 files changed

+158
-25
lines changed

10 files changed

+158
-25
lines changed

packages/browser/src/profiling/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,9 @@ export function createProfilingEvent(
583583
return createProfilePayload(profile_id, start_timestamp, profile, event);
584584
}
585585

586+
// TODO (v8): We need to obtain profile ids in @sentry-internal/tracing,
587+
// but we don't have access to this map because importing this map would
588+
// cause a circular dependancy. We need to resolve this in v8.
586589
const PROFILE_MAP: Map<string, JSSelfProfile> = new Map();
587590
/**
588591
*

packages/core/src/semanticAttributes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_OP = 'sentry.op';
1919
* Use this attribute to represent the origin of a span.
2020
*/
2121
export const SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'sentry.origin';
22+
23+
/**
24+
* The id of the profile that this span occured in.
25+
*/
26+
export const SEMANTIC_ATTRIBUTE_PROFILE_ID = 'profile_id';

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ export {
2727
} from './trace';
2828
export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
2929
export { setMeasurement } from './measurement';
30+
export { isValidSampleRate } from './sampling';

packages/core/src/tracing/sampling.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export function sampleTransaction<T extends Transaction>(
103103
/**
104104
* Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
105105
*/
106-
function isValidSampleRate(rate: unknown): boolean {
106+
export function isValidSampleRate(rate: unknown): boolean {
107107
// we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck
108108
// eslint-disable-next-line @typescript-eslint/no-explicit-any
109109
if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) {

packages/core/src/tracing/span.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/ut
1818

1919
import { DEBUG_BUILD } from '../debug-build';
2020
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
21-
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
21+
import {
22+
SEMANTIC_ATTRIBUTE_PROFILE_ID,
23+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
24+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
25+
} from '../semanticAttributes';
2226
import { getRootSpan } from '../utils/getRootSpan';
2327
import {
2428
TRACE_FLAG_NONE,
@@ -634,6 +638,7 @@ export class Span implements SpanInterface {
634638
trace_id: this._traceId,
635639
origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined,
636640
_metrics_summary: getMetricSummaryJsonForSpan(this),
641+
profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined,
637642
exclusive_time: this._exclusiveTime,
638643
measurements: Object.keys(this._measurements).length > 0 ? this._measurements : undefined,
639644
});

packages/core/src/tracing/transaction.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,16 @@ export class Transaction extends SpanClass implements TransactionInterface {
254254
this._hub = hub;
255255
}
256256

257+
/**
258+
* Get the profile id of the transaction.
259+
*/
260+
public getProfileId(): string | undefined {
261+
if (this._contexts !== undefined && this._contexts['profile'] !== undefined) {
262+
return this._contexts['profile'].profile_id as string;
263+
}
264+
return undefined;
265+
}
266+
257267
/**
258268
* Finish the transaction & prepare the event to send to Sentry.
259269
*/

packages/tracing-internal/src/browser/browserTracingIntegration.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable max-lines */
22
import type { IdleTransaction } from '@sentry/core';
3-
import { getActiveSpan } from '@sentry/core';
3+
import { getActiveSpan, getClient, getCurrentScope } from '@sentry/core';
44
import { getCurrentHub } from '@sentry/core';
55
import {
66
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
@@ -12,6 +12,7 @@ import {
1212
} from '@sentry/core';
1313
import type {
1414
Client,
15+
Integration,
1516
IntegrationFn,
1617
StartSpanOptions,
1718
Transaction,
@@ -198,9 +199,12 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
198199
startTrackingInteractions();
199200
}
200201

201-
const latestRoute: { name: string | undefined; source: TransactionSource | undefined } = {
202+
const latestRoute: {
203+
name: string | undefined;
204+
context: TransactionContext | undefined;
205+
} = {
202206
name: undefined,
203-
source: undefined,
207+
context: undefined,
204208
};
205209

206210
/** Create routing idle transaction. */
@@ -248,7 +252,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
248252
finalContext.metadata;
249253

250254
latestRoute.name = finalContext.name;
251-
latestRoute.source = getSource(finalContext);
255+
latestRoute.context = finalContext;
252256

253257
if (finalContext.sampled === false) {
254258
DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
@@ -462,7 +466,10 @@ export function getMetaContent(metaName: string): string | undefined {
462466
/** Start listener for interaction transactions */
463467
function registerInteractionListener(
464468
options: BrowserTracingOptions,
465-
latestRoute: { name: string | undefined; source: TransactionSource | undefined },
469+
latestRoute: {
470+
name: string | undefined;
471+
context: TransactionContext | undefined;
472+
},
466473
): void {
467474
let inflightInteractionTransaction: IdleTransaction | undefined;
468475
const registerInteractionTransaction = (): void => {
@@ -497,7 +504,7 @@ function registerInteractionListener(
497504
op,
498505
trimEnd: true,
499506
data: {
500-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.source || 'url',
507+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.context ? getSource(latestRoute.context) : undefined || 'url',
501508
},
502509
};
503510

@@ -528,9 +535,24 @@ const MAX_INTERACTIONS = 10;
528535
/** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
529536
function registerInpInteractionListener(
530537
interactionIdtoRouteNameMapping: InteractionRouteNameMapping,
531-
latestRoute: { name: string | undefined; source: TransactionSource | undefined },
538+
latestRoute: {
539+
name: string | undefined;
540+
context: TransactionContext | undefined;
541+
},
532542
): void {
533543
addPerformanceInstrumentationHandler('event', ({ entries }) => {
544+
const client = getClient();
545+
// We need to get the replay, user, and activeTransaction from the current scope
546+
// so that we can associate replay id, profile id, and a user display to the span
547+
const replay =
548+
client !== undefined && client.getIntegrationByName !== undefined
549+
? (client.getIntegrationByName('Replay') as Integration & { getReplayId: () => string })
550+
: undefined;
551+
const replayId = replay !== undefined ? replay.getReplayId() : undefined;
552+
// eslint-disable-next-line deprecation/deprecation
553+
const activeTransaction = getActiveTransaction();
554+
const currentScope = getCurrentScope();
555+
const user = currentScope !== undefined ? currentScope.getUser() : undefined;
534556
for (const entry of entries) {
535557
if (isPerformanceEventTiming(entry)) {
536558
const duration = entry.duration;
@@ -546,12 +568,20 @@ function registerInpInteractionListener(
546568
if (minInteractionId === undefined || duration > interactionIdtoRouteNameMapping[minInteractionId].duration) {
547569
const interactionId = entry.interactionId;
548570
const routeName = latestRoute.name;
549-
if (interactionId && routeName) {
571+
const parentContext = latestRoute.context;
572+
if (interactionId && routeName && parentContext) {
550573
if (minInteractionId && Object.keys(interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) {
551574
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
552575
delete interactionIdtoRouteNameMapping[minInteractionId];
553576
}
554-
interactionIdtoRouteNameMapping[interactionId] = { routeName, duration };
577+
interactionIdtoRouteNameMapping[interactionId] = {
578+
routeName,
579+
duration,
580+
parentContext,
581+
user,
582+
activeTransaction,
583+
replayId,
584+
};
555585
}
556586
}
557587
}

packages/tracing-internal/src/browser/metrics/index.ts

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
/* eslint-disable max-lines */
22
import type { IdleTransaction, Transaction } from '@sentry/core';
3-
import { Span, getActiveTransaction, getClient, setMeasurement } from '@sentry/core';
4-
import type { Measurements, SpanContext } from '@sentry/types';
3+
import {
4+
Span,
5+
getActiveTransaction,
6+
getClient,
7+
hasTracingEnabled,
8+
isValidSampleRate,
9+
setMeasurement,
10+
} from '@sentry/core';
11+
import type { ClientOptions, Measurements, SpanContext, TransactionContext } from '@sentry/types';
512
import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/utils';
613

714
import { spanToJSON } from '@sentry/core';
@@ -202,33 +209,57 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping)
202209
if (!entry || !client) {
203210
return;
204211
}
205-
const { release, environment } = client.getOptions();
212+
const options = client.getOptions();
206213
/** Build the INP span, create an envelope from the span, and then send the envelope */
207214
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
208215
const duration = msToSec(metric.value);
209-
const routeName =
210-
entry.interactionId !== undefined ? interactionIdtoRouteNameMapping[entry.interactionId].routeName : undefined;
216+
const { routeName, parentContext, activeTransaction, user, replayId } =
217+
entry.interactionId !== undefined
218+
? interactionIdtoRouteNameMapping[entry.interactionId]
219+
: {
220+
routeName: undefined,
221+
parentContext: undefined,
222+
activeTransaction: undefined,
223+
user: undefined,
224+
replayId: undefined,
225+
};
226+
const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined;
227+
// eslint-disable-next-line deprecation/deprecation
228+
const profileId = activeTransaction !== undefined ? activeTransaction.getProfileId() : undefined;
211229
const span = new Span({
212230
startTimestamp: startTime,
213231
endTimestamp: startTime + duration,
214232
op: 'ui.interaction.click',
215233
name: htmlTreeAsString(entry.target),
216234
attributes: {
217-
release,
218-
environment,
235+
release: options.release,
236+
environment: options.environment,
219237
transaction: routeName,
238+
...(userDisplay !== undefined && userDisplay !== '' ? { user: userDisplay } : {}),
239+
...(profileId !== undefined ? { profile_id: profileId } : {}),
240+
...(replayId !== undefined ? { replay_id: replayId } : {}),
220241
},
221242
exclusiveTime: metric.value,
222243
measurements: {
223244
inp: { value: metric.value, unit: 'millisecond' },
224245
},
225246
});
226-
const envelope = span ? createSpanEnvelope([span]) : undefined;
227-
const transport = client && client.getTransport();
228-
if (transport && envelope) {
229-
transport.send(envelope).then(null, reason => {
230-
DEBUG_BUILD && logger.error('Error while sending interaction:', reason);
231-
});
247+
248+
/** Check to see if the span should be sampled */
249+
const sampleRate = getSampleRate(parentContext, options);
250+
if (!sampleRate) {
251+
return;
252+
}
253+
254+
if (Math.random() < (sampleRate as number | boolean)) {
255+
const envelope = span ? createSpanEnvelope([span]) : undefined;
256+
const transport = client && client.getTransport();
257+
if (transport && envelope) {
258+
transport.send(envelope).then(null, reason => {
259+
DEBUG_BUILD && logger.error('Error while sending interaction:', reason);
260+
});
261+
}
262+
return;
232263
}
233264
});
234265
}
@@ -631,3 +662,35 @@ export function _addTtfbToMeasurements(
631662
}
632663
}
633664
}
665+
666+
/** Taken from @sentry/core sampling.ts */
667+
function getSampleRate(transactionContext: TransactionContext | undefined, options: ClientOptions): number | boolean {
668+
if (!hasTracingEnabled(options)) {
669+
return false;
670+
}
671+
let sampleRate;
672+
if (transactionContext !== undefined && typeof options.tracesSampler === 'function') {
673+
sampleRate = options.tracesSampler({
674+
transactionContext,
675+
name: transactionContext.name,
676+
parentSampled: transactionContext.parentSampled,
677+
attributes: {
678+
// eslint-disable-next-line deprecation/deprecation
679+
...transactionContext.data,
680+
...transactionContext.attributes,
681+
},
682+
location: WINDOW.location,
683+
});
684+
} else if (transactionContext !== undefined && transactionContext.sampled !== undefined) {
685+
sampleRate = transactionContext.sampled;
686+
} else if (typeof options.tracesSampleRate !== 'undefined') {
687+
sampleRate = options.tracesSampleRate;
688+
} else {
689+
sampleRate = 1;
690+
}
691+
if (!isValidSampleRate(sampleRate)) {
692+
DEBUG_BUILD && logger.warn('[Tracing] Discarding transaction because of invalid sample rate.');
693+
return false;
694+
}
695+
return sampleRate;
696+
}

packages/tracing-internal/src/browser/web-vitals/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
import type { Transaction, TransactionContext, User } from '@sentry/types';
1718
import type { FirstInputPolyfillCallback } from './types/polyfills';
1819

1920
export * from './types/base';
@@ -163,4 +164,13 @@ declare global {
163164
}
164165
}
165166

166-
export type InteractionRouteNameMapping = { [key: string]: { routeName: string; duration: number } };
167+
export type InteractionRouteNameMapping = {
168+
[key: string]: {
169+
routeName: string;
170+
duration: number;
171+
parentContext: TransactionContext;
172+
user?: User;
173+
activeTransaction?: Transaction;
174+
replayId?: string;
175+
};
176+
};

packages/types/src/transaction.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ export interface Transaction extends TransactionContext, Omit<Span, 'setName' |
152152
* @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead.
153153
*/
154154
getDynamicSamplingContext(): Partial<DynamicSamplingContext>;
155+
156+
/**
157+
* Get the profile id from the transaction
158+
* @deprecated Use `toJSON()` or access the fields directly instead.
159+
*/
160+
getProfileId(): string | undefined;
155161
}
156162

157163
/**

0 commit comments

Comments
 (0)