diff --git a/packages/browser/src/integrations/browsersession.ts b/packages/browser/src/integrations/browsersession.ts index 7d339baa42d5..eb149084343a 100644 --- a/packages/browser/src/integrations/browsersession.ts +++ b/packages/browser/src/integrations/browsersession.ts @@ -1,4 +1,11 @@ -import { captureSession, debug, defineIntegration, getIsolationScope, startSession } from '@sentry/core/browser'; +import { + captureSession, + debug, + defineIntegration, + getIsolationScope, + SEMANTIC_ATTRIBUTE_SESSION_ID, + startSession, +} from '@sentry/core/browser'; import { addHistoryInstrumentationHandler } from '@sentry-internal/browser-utils'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; @@ -29,6 +36,52 @@ export const browserSessionIntegration = defineIntegration((options: BrowserSess return { name: 'BrowserSession', + setup(client) { + function attachSessionId | undefined }>(telemetryItem: T): T { + const session = getIsolationScope().getSession(); + const attributes = telemetryItem.attributes ?? (telemetryItem.attributes = {}); + if (session?.sid && !attributes?.[SEMANTIC_ATTRIBUTE_SESSION_ID]) { + attributes[SEMANTIC_ATTRIBUTE_SESSION_ID] = session.sid; + } + return telemetryItem; + } + + client.on('processMetric', attachSessionId); + client.on('beforeCaptureLog', attachSessionId); + // only applies to streamed spans + client.on('processSpan', attachSessionId); + + // for errors and transactions (non-streamed spans) + client.addEventProcessor(event => { + if (event.type && event.type !== 'transaction') { + // ignore other events than errors and transactions for now + return event; + } + + const sessionId = getIsolationScope().getSession()?.sid; + if (!sessionId) { + return event; + } + + // set a session context on the event. Relay will extract the `session.id` + // tag from this context which will make it queryable in the UI. + event.contexts = { + session: { + id: sessionId, + }, + ...event.contexts, + }; + + event.spans?.forEach(span => { + span.data = { + [SEMANTIC_ATTRIBUTE_SESSION_ID]: sessionId, + ...span.data, + }; + }); + + return event; + }); + }, setupOnce() { if (typeof WINDOW.document === 'undefined') { DEBUG_BUILD && diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index fd3b0e0c4022..7ff72dee4a08 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -116,3 +116,5 @@ export const SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE = 'sentry.link.type'; * For LangGraph: configurable.thread_id */ export const GEN_AI_CONVERSATION_ID_ATTRIBUTE = 'gen_ai.conversation.id'; + +export const SEMANTIC_ATTRIBUTE_SESSION_ID = 'session.id'; diff --git a/packages/core/src/types/context.ts b/packages/core/src/types/context.ts index 9e52ff9d6834..76b24f155e56 100644 --- a/packages/core/src/types/context.ts +++ b/packages/core/src/types/context.ts @@ -16,6 +16,7 @@ export interface Contexts extends Record { state?: StateContext; profile?: ProfileContext; flags?: FeatureFlagContext; + session?: SessionContext; } export interface StateContext extends Record { @@ -139,3 +140,7 @@ export interface MissingInstrumentationContext extends Record { export interface FeatureFlagContext extends Record { values: FeatureFlag[]; } + +export interface SessionContext extends Record { + id?: string; +}