diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index 1b9638db8f..82791db72b 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -549,6 +549,69 @@ describe('preStartRum', () => { }) }) + describe('passes pre-start contexts to doStartRum', () => { + function mockStartRumResult(): StartRumResult { + return { + globalContext: { setContext: noop } as any, + userContext: { setContext: noop } as any, + accountContext: { setContext: noop } as any, + } as unknown as StartRumResult + } + + it('should pass global context set before init to doStartRum', () => { + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() + doStartRumSpy.and.returnValue(mockStartRumResult()) + + strategy.globalContext.setContextProperty('foo', 'bar') + strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + + expect(doStartRumSpy).toHaveBeenCalledTimes(1) + const initialContexts = doStartRumSpy.calls.mostRecent().args[5] + expect(initialContexts).toBeDefined() + expect(initialContexts.globalContext).toEqual({ foo: 'bar' }) + }) + + it('should pass user context set before init to doStartRum', () => { + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() + doStartRumSpy.and.returnValue(mockStartRumResult()) + + strategy.userContext.setContextProperty('id', 'user-123') + strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + + expect(doStartRumSpy).toHaveBeenCalledTimes(1) + const initialContexts = doStartRumSpy.calls.mostRecent().args[5] + expect(initialContexts).toBeDefined() + expect(initialContexts.userContext).toEqual({ id: 'user-123' }) + }) + + it('should pass account context set before init to doStartRum', () => { + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() + doStartRumSpy.and.returnValue(mockStartRumResult()) + + strategy.accountContext.setContextProperty('id', 'account-456') + strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + + expect(doStartRumSpy).toHaveBeenCalledTimes(1) + const initialContexts = doStartRumSpy.calls.mostRecent().args[5] + expect(initialContexts).toBeDefined() + expect(initialContexts.accountContext).toEqual({ id: 'account-456' }) + }) + + it('should pass empty contexts when nothing is set before init', () => { + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() + doStartRumSpy.and.returnValue(mockStartRumResult()) + + strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + + expect(doStartRumSpy).toHaveBeenCalledTimes(1) + const initialContexts = doStartRumSpy.calls.mostRecent().args[5] + expect(initialContexts).toBeDefined() + expect(initialContexts.globalContext).toEqual({}) + expect(initialContexts.userContext).toEqual({}) + expect(initialContexts.accountContext).toEqual({}) + }) + }) + describe('buffers API calls before starting RUM', () => { let strategy: Strategy let doStartRumSpy: jasmine.Spy diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index b7cfd8422d..e69cde7a54 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -49,12 +49,19 @@ import { callPluginsMethod } from '../domain/plugins' import type { StartRumResult } from './startRum' import type { RumPublicApiOptions, Strategy } from './rumPublicApi' +export interface InitialContexts { + globalContext: Context + userContext: Context + accountContext: Context +} + export type DoStartRum = ( configuration: RumConfiguration, deflateWorker: DeflateWorker | undefined, initialViewOptions: ViewOptions | undefined, telemetry: Telemetry, - hooks: Hooks + hooks: Hooks, + initialContexts: InitialContexts ) => StartRumResult export function createPreStartStrategy( @@ -117,7 +124,20 @@ export function createPreStartStrategy( initialViewOptions = firstStartViewCall.options } - const startRumResult = doStartRum(cachedConfiguration, deflateWorker, initialViewOptions, telemetry, hooks) + const initialContexts: InitialContexts = { + globalContext: globalContext.getContext(), + userContext: userContext.getContext(), + accountContext: accountContext.getContext(), + } + + const startRumResult = doStartRum( + cachedConfiguration, + deflateWorker, + initialViewOptions, + telemetry, + hooks, + initialContexts + ) bufferApiCalls.drain(startRumResult) } diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 57e93cf128..5a4158d3b1 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -590,7 +590,7 @@ export function makeRumPublicApi( options, trackingConsentState, customVitalsState, - (configuration, deflateWorker, initialViewOptions, telemetry, hooks) => { + (configuration, deflateWorker, initialViewOptions, telemetry, hooks, initialContexts) => { const createEncoder = deflateWorker && options.createDeflateEncoder ? (streamId: DeflateEncoderStreamId) => options.createDeflateEncoder!(configuration, deflateWorker, streamId) @@ -607,7 +607,8 @@ export function makeRumPublicApi( bufferedDataObservable, telemetry, hooks, - options.sdkName + options.sdkName, + initialContexts ) recorderApi.onRumStart( diff --git a/packages/rum-core/src/boot/startRum.spec.ts b/packages/rum-core/src/boot/startRum.spec.ts index b3aeca3325..40c6955198 100644 --- a/packages/rum-core/src/boot/startRum.spec.ts +++ b/packages/rum-core/src/boot/startRum.spec.ts @@ -104,6 +104,75 @@ describe('rum session', () => { }) }) +describe('initial view event with pre-start contexts', () => { + it('should include global context on the first view event when initial contexts are provided', () => { + const lifeCycle = new LifeCycle() + const sessionManager = createRumSessionManagerMock().setId('42') + const hooks = createHooks() + const serverRumEvents = collectServerEvents(lifeCycle) + + const initialContexts = { + globalContext: { foo: 'bar' }, + userContext: {}, + accountContext: {}, + } + + const { stop } = startRumEventCollection( + lifeCycle, + hooks, + mockRumConfiguration(), + sessionManager, + noopRecorderApi, + undefined, + createCustomVitalsState(), + new Observable(), + undefined, + noop, + initialContexts + ) + + registerCleanupTask(stop) + + // The first event should be a view with global context + expect(serverRumEvents.length).toBeGreaterThanOrEqual(1) + expect(serverRumEvents[0].type).toEqual('view') + expect(serverRumEvents[0].context).toEqual({ foo: 'bar' }) + }) + + it('should include user context on the first view event when initial contexts are provided', () => { + const lifeCycle = new LifeCycle() + const sessionManager = createRumSessionManagerMock().setId('42') + const hooks = createHooks() + const serverRumEvents = collectServerEvents(lifeCycle) + + const initialContexts = { + globalContext: {}, + userContext: { id: 'user-123', name: 'Test User' }, + accountContext: {}, + } + + const { stop } = startRumEventCollection( + lifeCycle, + hooks, + mockRumConfiguration(), + sessionManager, + noopRecorderApi, + undefined, + createCustomVitalsState(), + new Observable(), + undefined, + noop, + initialContexts + ) + + registerCleanupTask(stop) + + expect(serverRumEvents.length).toBeGreaterThanOrEqual(1) + expect(serverRumEvents[0].type).toEqual('view') + expect(serverRumEvents[0].usr).toEqual(jasmine.objectContaining({ id: 'user-123', name: 'Test User' })) + }) +}) + describe('rum session keep alive', () => { let lifeCycle: LifeCycle let clock: Clock diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index f278bbe4e3..d09b0fadf4 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -13,6 +13,7 @@ import { createPageMayExitObservable, canUseEventBridge, addTelemetryDebug, + isEmptyObject, startAccountContext, startGlobalContext, startUserContext, @@ -55,6 +56,7 @@ import { startEventCollection } from '../domain/event/eventCollection' import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry' import { startSourceCodeContext } from '../domain/contexts/sourceCodeContext' import type { RecorderApi, ProfilerApi } from './rumPublicApi' +import type { InitialContexts } from './preStartRum' export type StartRum = typeof startRum export type StartRumResult = ReturnType @@ -74,7 +76,8 @@ export function startRum( bufferedDataObservable: BufferedObservable, telemetry: Telemetry, hooks: Hooks, - sdkName?: SdkName + sdkName?: SdkName, + initialContexts?: InitialContexts ) { const cleanupTasks: Array<() => void> = [] const lifeCycle = new LifeCycle() @@ -127,7 +130,8 @@ export function startRum( customVitalsState, bufferedDataObservable, sdkName, - reportError + reportError, + initialContexts ) cleanupTasks.push(stopRumEventCollection) bufferedDataObservable.unbuffer() @@ -158,7 +162,8 @@ export function startRumEventCollection( customVitalsState: CustomVitalsState, bufferedDataObservable: Observable, sdkName: SdkName | undefined, - reportError: (error: RawError) => void + reportError: (error: RawError) => void, + initialContexts?: InitialContexts ) { const cleanupTasks: Array<() => void> = [] @@ -181,6 +186,16 @@ export function startRumEventCollection( const userContext = startUserContext(hooks, configuration, session, 'rum') const accountContext = startAccountContext(hooks, configuration, 'rum') + // Initialize context managers with pre-start values so the first view event + // includes any context set before init() was called (see #3935) + if (initialContexts) { + globalContext.setContext(initialContexts.globalContext) + userContext.setContext(initialContexts.userContext) + if (!isEmptyObject(initialContexts.accountContext)) { + accountContext.setContext(initialContexts.accountContext) + } + } + const actionCollection = startActionCollection( lifeCycle, hooks,