diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 9114884384b7..6ebec8511214 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -583,6 +583,9 @@ export function createProfilingEvent( return createProfilePayload(profile_id, start_timestamp, profile, event); } +// TODO (v8): We need to obtain profile ids in @sentry-internal/tracing, +// but we don't have access to this map because importing this map would +// cause a circular dependancy. We need to resolve this in v8. const PROFILE_MAP: Map = new Map(); /** * diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index afd0d123090f..46d79a8bcc92 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -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'; + +/** + * The id of the profile that this span occured in. + */ +export const SEMANTIC_ATTRIBUTE_PROFILE_ID = 'profile_id'; diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 6937f5502804..bb4c8da90d97 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -18,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_PROFILE_ID, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, +} from '../semanticAttributes'; import { getRootSpan } from '../utils/getRootSpan'; import { TRACE_FLAG_NONE, @@ -634,6 +638,7 @@ export class Span implements SpanInterface { trace_id: this._traceId, origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, _metrics_summary: getMetricSummaryJsonForSpan(this), + profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined, exclusive_time: this._exclusiveTime, measurements: Object.keys(this._measurements).length > 0 ? this._measurements : undefined, }); diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 905aaf6c5040..e30eec8d8249 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -254,6 +254,16 @@ export class Transaction extends SpanClass implements TransactionInterface { this._hub = hub; } + /** + * Get the profile id of the transaction. + */ + public getProfileId(): string | undefined { + if (this._contexts !== undefined && this._contexts['profile'] !== undefined) { + return this._contexts['profile'].profile_id as string; + } + return undefined; + } + /** * Finish the transaction & prepare the event to send to Sentry. */ diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 24644569d071..47bad615586d 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import type { IdleTransaction } from '@sentry/core'; -import { getActiveSpan } from '@sentry/core'; +import { getActiveSpan, getClient, getCurrentScope } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -12,6 +12,7 @@ import { } from '@sentry/core'; import type { Client, + Integration, IntegrationFn, StartSpanOptions, Transaction, @@ -539,6 +540,18 @@ function 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; @@ -564,6 +577,9 @@ function registerInpInteractionListener( routeName, duration, parentContext, + user, + activeTransaction, + replayId, }; } } diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index d51a9dfbb7c4..b9c08f7dffaf 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -213,10 +213,19 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) /** 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, parentContext } = + const { routeName, parentContext, activeTransaction, user, replayId } = entry.interactionId !== undefined ? interactionIdtoRouteNameMapping[entry.interactionId] - : { routeName: undefined, parentContext: undefined }; + : { + routeName: undefined, + parentContext: undefined, + activeTransaction: undefined, + user: undefined, + replayId: undefined, + }; + const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined; + // eslint-disable-next-line deprecation/deprecation + const profileId = activeTransaction !== undefined ? activeTransaction.getProfileId() : undefined; const span = new Span({ startTimestamp: startTime, endTimestamp: startTime + duration, @@ -226,6 +235,9 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping) release: options.release, environment: options.environment, transaction: routeName, + ...(userDisplay !== undefined && userDisplay !== '' ? { user: userDisplay } : {}), + ...(profileId !== undefined ? { profile_id: profileId } : {}), + ...(replayId !== undefined ? { replay_id: replayId } : {}), }, exclusiveTime: metric.value, measurements: { diff --git a/packages/tracing-internal/src/browser/web-vitals/types.ts b/packages/tracing-internal/src/browser/web-vitals/types.ts index 1f7d344401f6..adc95084bda9 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types.ts +++ b/packages/tracing-internal/src/browser/web-vitals/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { TransactionContext } from '@sentry/types'; +import type { Transaction, TransactionContext, User } from '@sentry/types'; import type { FirstInputPolyfillCallback } from './types/polyfills'; export * from './types/base'; @@ -165,5 +165,12 @@ declare global { } export type InteractionRouteNameMapping = { - [key: string]: { routeName: string; duration: number; parentContext: TransactionContext }; + [key: string]: { + routeName: string; + duration: number; + parentContext: TransactionContext; + user?: User; + activeTransaction?: Transaction; + replayId?: string; + }; }; diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index fbcf8b38f02d..caae6c49027d 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -152,6 +152,12 @@ export interface Transaction extends TransactionContext, Omit; + + /** + * Get the profile id from the transaction + * @deprecated Use `toJSON()` or access the fields directly instead. + */ + getProfileId(): string | undefined; } /**