Skip to content

Commit 338234a

Browse files
allspainclaude
andcommitted
🐛 Fix #3935: Include pre-start context in first view event
Capture global, user, and account contexts set before init() and restore them after startRum() so the first view event includes pre-start context. Skip restoring empty account context to avoid triggering a spurious "property id of account is required" warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8d3e8a9 commit 338234a

5 files changed

Lines changed: 175 additions & 7 deletions

File tree

packages/rum-core/src/boot/preStartRum.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,69 @@ describe('preStartRum', () => {
549549
})
550550
})
551551

552+
describe('passes pre-start contexts to doStartRum', () => {
553+
function mockStartRumResult(): StartRumResult {
554+
return {
555+
globalContext: { setContext: noop } as any,
556+
userContext: { setContext: noop } as any,
557+
accountContext: { setContext: noop } as any,
558+
} as unknown as StartRumResult
559+
}
560+
561+
it('should pass global context set before init to doStartRum', () => {
562+
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
563+
doStartRumSpy.and.returnValue(mockStartRumResult())
564+
565+
strategy.globalContext.setContextProperty('foo', 'bar')
566+
strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API)
567+
568+
expect(doStartRumSpy).toHaveBeenCalledTimes(1)
569+
const initialContexts = doStartRumSpy.calls.mostRecent().args[5]
570+
expect(initialContexts).toBeDefined()
571+
expect(initialContexts.globalContext).toEqual({ foo: 'bar' })
572+
})
573+
574+
it('should pass user context set before init to doStartRum', () => {
575+
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
576+
doStartRumSpy.and.returnValue(mockStartRumResult())
577+
578+
strategy.userContext.setContextProperty('id', 'user-123')
579+
strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API)
580+
581+
expect(doStartRumSpy).toHaveBeenCalledTimes(1)
582+
const initialContexts = doStartRumSpy.calls.mostRecent().args[5]
583+
expect(initialContexts).toBeDefined()
584+
expect(initialContexts.userContext).toEqual({ id: 'user-123' })
585+
})
586+
587+
it('should pass account context set before init to doStartRum', () => {
588+
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
589+
doStartRumSpy.and.returnValue(mockStartRumResult())
590+
591+
strategy.accountContext.setContextProperty('id', 'account-456')
592+
strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API)
593+
594+
expect(doStartRumSpy).toHaveBeenCalledTimes(1)
595+
const initialContexts = doStartRumSpy.calls.mostRecent().args[5]
596+
expect(initialContexts).toBeDefined()
597+
expect(initialContexts.accountContext).toEqual({ id: 'account-456' })
598+
})
599+
600+
it('should pass empty contexts when nothing is set before init', () => {
601+
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
602+
doStartRumSpy.and.returnValue(mockStartRumResult())
603+
604+
strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API)
605+
606+
expect(doStartRumSpy).toHaveBeenCalledTimes(1)
607+
const initialContexts = doStartRumSpy.calls.mostRecent().args[5]
608+
expect(initialContexts).toBeDefined()
609+
expect(initialContexts.globalContext).toEqual({})
610+
expect(initialContexts.userContext).toEqual({})
611+
expect(initialContexts.accountContext).toEqual({})
612+
})
613+
})
614+
552615
describe('buffers API calls before starting RUM', () => {
553616
let strategy: Strategy
554617
let doStartRumSpy: jasmine.Spy<DoStartRum>

packages/rum-core/src/boot/preStartRum.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,19 @@ import { callPluginsMethod } from '../domain/plugins'
4949
import type { StartRumResult } from './startRum'
5050
import type { RumPublicApiOptions, Strategy } from './rumPublicApi'
5151

52+
export interface InitialContexts {
53+
globalContext: Context
54+
userContext: Context
55+
accountContext: Context
56+
}
57+
5258
export type DoStartRum = (
5359
configuration: RumConfiguration,
5460
deflateWorker: DeflateWorker | undefined,
5561
initialViewOptions: ViewOptions | undefined,
5662
telemetry: Telemetry,
57-
hooks: Hooks
63+
hooks: Hooks,
64+
initialContexts: InitialContexts
5865
) => StartRumResult
5966

6067
export function createPreStartStrategy(
@@ -117,7 +124,20 @@ export function createPreStartStrategy(
117124
initialViewOptions = firstStartViewCall.options
118125
}
119126

120-
const startRumResult = doStartRum(cachedConfiguration, deflateWorker, initialViewOptions, telemetry, hooks)
127+
const initialContexts: InitialContexts = {
128+
globalContext: globalContext.getContext(),
129+
userContext: userContext.getContext(),
130+
accountContext: accountContext.getContext(),
131+
}
132+
133+
const startRumResult = doStartRum(
134+
cachedConfiguration,
135+
deflateWorker,
136+
initialViewOptions,
137+
telemetry,
138+
hooks,
139+
initialContexts
140+
)
121141

122142
bufferApiCalls.drain(startRumResult)
123143
}

packages/rum-core/src/boot/rumPublicApi.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ export function makeRumPublicApi(
590590
options,
591591
trackingConsentState,
592592
customVitalsState,
593-
(configuration, deflateWorker, initialViewOptions, telemetry, hooks) => {
593+
(configuration, deflateWorker, initialViewOptions, telemetry, hooks, initialContexts) => {
594594
const createEncoder =
595595
deflateWorker && options.createDeflateEncoder
596596
? (streamId: DeflateEncoderStreamId) => options.createDeflateEncoder!(configuration, deflateWorker, streamId)
@@ -607,7 +607,8 @@ export function makeRumPublicApi(
607607
bufferedDataObservable,
608608
telemetry,
609609
hooks,
610-
options.sdkName
610+
options.sdkName,
611+
initialContexts
611612
)
612613

613614
recorderApi.onRumStart(

packages/rum-core/src/boot/startRum.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,75 @@ describe('rum session', () => {
104104
})
105105
})
106106

107+
describe('initial view event with pre-start contexts', () => {
108+
it('should include global context on the first view event when initial contexts are provided', () => {
109+
const lifeCycle = new LifeCycle()
110+
const sessionManager = createRumSessionManagerMock().setId('42')
111+
const hooks = createHooks()
112+
const serverRumEvents = collectServerEvents(lifeCycle)
113+
114+
const initialContexts = {
115+
globalContext: { foo: 'bar' },
116+
userContext: {},
117+
accountContext: {},
118+
}
119+
120+
const { stop } = startRumEventCollection(
121+
lifeCycle,
122+
hooks,
123+
mockRumConfiguration(),
124+
sessionManager,
125+
noopRecorderApi,
126+
undefined,
127+
createCustomVitalsState(),
128+
new Observable(),
129+
undefined,
130+
noop,
131+
initialContexts
132+
)
133+
134+
registerCleanupTask(stop)
135+
136+
// The first event should be a view with global context
137+
expect(serverRumEvents.length).toBeGreaterThanOrEqual(1)
138+
expect(serverRumEvents[0].type).toEqual('view')
139+
expect(serverRumEvents[0].context).toEqual({ foo: 'bar' })
140+
})
141+
142+
it('should include user context on the first view event when initial contexts are provided', () => {
143+
const lifeCycle = new LifeCycle()
144+
const sessionManager = createRumSessionManagerMock().setId('42')
145+
const hooks = createHooks()
146+
const serverRumEvents = collectServerEvents(lifeCycle)
147+
148+
const initialContexts = {
149+
globalContext: {},
150+
userContext: { id: 'user-123', name: 'Test User' },
151+
accountContext: {},
152+
}
153+
154+
const { stop } = startRumEventCollection(
155+
lifeCycle,
156+
hooks,
157+
mockRumConfiguration(),
158+
sessionManager,
159+
noopRecorderApi,
160+
undefined,
161+
createCustomVitalsState(),
162+
new Observable(),
163+
undefined,
164+
noop,
165+
initialContexts
166+
)
167+
168+
registerCleanupTask(stop)
169+
170+
expect(serverRumEvents.length).toBeGreaterThanOrEqual(1)
171+
expect(serverRumEvents[0].type).toEqual('view')
172+
expect(serverRumEvents[0].usr).toEqual(jasmine.objectContaining({ id: 'user-123', name: 'Test User' }))
173+
})
174+
})
175+
107176
describe('rum session keep alive', () => {
108177
let lifeCycle: LifeCycle
109178
let clock: Clock

packages/rum-core/src/boot/startRum.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
createPageMayExitObservable,
1414
canUseEventBridge,
1515
addTelemetryDebug,
16+
isEmptyObject,
1617
startAccountContext,
1718
startGlobalContext,
1819
startUserContext,
@@ -55,6 +56,7 @@ import { startEventCollection } from '../domain/event/eventCollection'
5556
import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry'
5657
import { startSourceCodeContext } from '../domain/contexts/sourceCodeContext'
5758
import type { RecorderApi, ProfilerApi } from './rumPublicApi'
59+
import type { InitialContexts } from './preStartRum'
5860

5961
export type StartRum = typeof startRum
6062
export type StartRumResult = ReturnType<StartRum>
@@ -74,7 +76,8 @@ export function startRum(
7476
bufferedDataObservable: BufferedObservable<BufferedData>,
7577
telemetry: Telemetry,
7678
hooks: Hooks,
77-
sdkName?: SdkName
79+
sdkName?: SdkName,
80+
initialContexts?: InitialContexts
7881
) {
7982
const cleanupTasks: Array<() => void> = []
8083
const lifeCycle = new LifeCycle()
@@ -127,7 +130,8 @@ export function startRum(
127130
customVitalsState,
128131
bufferedDataObservable,
129132
sdkName,
130-
reportError
133+
reportError,
134+
initialContexts
131135
)
132136
cleanupTasks.push(stopRumEventCollection)
133137
bufferedDataObservable.unbuffer()
@@ -158,7 +162,8 @@ export function startRumEventCollection(
158162
customVitalsState: CustomVitalsState,
159163
bufferedDataObservable: Observable<BufferedData>,
160164
sdkName: SdkName | undefined,
161-
reportError: (error: RawError) => void
165+
reportError: (error: RawError) => void,
166+
initialContexts?: InitialContexts
162167
) {
163168
const cleanupTasks: Array<() => void> = []
164169

@@ -181,6 +186,16 @@ export function startRumEventCollection(
181186
const userContext = startUserContext(hooks, configuration, session, 'rum')
182187
const accountContext = startAccountContext(hooks, configuration, 'rum')
183188

189+
// Initialize context managers with pre-start values so the first view event
190+
// includes any context set before init() was called (see #3935)
191+
if (initialContexts) {
192+
globalContext.setContext(initialContexts.globalContext)
193+
userContext.setContext(initialContexts.userContext)
194+
if (!isEmptyObject(initialContexts.accountContext)) {
195+
accountContext.setContext(initialContexts.accountContext)
196+
}
197+
}
198+
184199
const actionCollection = startActionCollection(
185200
lifeCycle,
186201
hooks,

0 commit comments

Comments
 (0)