Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ generated-docs/
.env*
!.env.example
.rum-ai-toolkit/
.idea/

# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
.pnp.*
Expand Down
71 changes: 48 additions & 23 deletions packages/rum-core/src/boot/preStartRum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
mockEventBridge,
mockSyntheticsWorkerValues,
createFakeTelemetryObject,
registerCleanupTask,
replaceMockableWithSpy,
} from '@datadog/browser-core/test'
import type { HybridInitConfiguration, RumInitConfiguration } from '../domain/configuration'
Expand Down Expand Up @@ -402,32 +403,54 @@ describe('preStartRum', () => {
})

describe('remote configuration', () => {
const REMOTE_CONFIGURATION_ID = '123'
let interceptor: ReturnType<typeof interceptRequests>

beforeEach(() => {
interceptor = interceptRequests()
})
localStorage.clear()

it('should start with the remote configuration when a remoteConfigurationId is provided', (done) => {
interceptor = interceptRequests()
interceptor.withFetch(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ rum: { sessionSampleRate: 50 } }),
})
)
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
doStartRumSpy.and.callFake((configuration) => {
expect(configuration.sessionSampleRate).toEqual(50)
done()
return {} as StartRumResult

registerCleanupTask(() => {
localStorage.clear()
})
})

it('should start synchronously with init configuration on cache miss', () => {
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()

strategy.init(
{
...DEFAULT_INIT_CONFIGURATION,
remoteConfigurationId: REMOTE_CONFIGURATION_ID,
sessionSampleRate: 25,
},
PUBLIC_API
)

expect(doStartRumSpy).toHaveBeenCalledTimes(1)
expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toBe(25)
})

it('should trigger a background fetch to the remote configuration endpoint', async () => {
const { strategy } = createPreStartStrategyWithDefaults()

strategy.init(
{
...DEFAULT_INIT_CONFIGURATION,
remoteConfigurationId: '123',
remoteConfigurationId: REMOTE_CONFIGURATION_ID,
},
PUBLIC_API
)

await interceptor.waitForAllFetchCalls()
expect(interceptor.requests.some((r) => r.url.includes(REMOTE_CONFIGURATION_ID))).toBeTrue()
})
})

Expand Down Expand Up @@ -497,8 +520,14 @@ describe('preStartRum', () => {
let interceptor: ReturnType<typeof interceptRequests>

beforeEach(() => {
localStorage.clear()

interceptor = interceptRequests()
initConfiguration = { ...DEFAULT_INIT_CONFIGURATION, service: 'my-service', version: '1.4.2', env: 'dev' }

registerCleanupTask(() => {
localStorage.clear()
})
})

it('is undefined before init', () => {
Expand Down Expand Up @@ -526,26 +555,22 @@ describe('preStartRum', () => {
expect(strategy.initConfiguration).toEqual(initConfiguration)
})

it('returns the initConfiguration with the remote configuration when a remoteConfigurationId is provided', (done) => {
it('exposes the user configuration when a remoteConfigurationId is provided (cache miss)', () => {
interceptor.withFetch(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ rum: { sessionSampleRate: 50 } }),
})
)
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
doStartRumSpy.and.callFake(() => {
expect(strategy.initConfiguration?.sessionSampleRate).toEqual(50)
done()
return {} as StartRumResult
})
strategy.init(
{
...DEFAULT_INIT_CONFIGURATION,
remoteConfigurationId: '123',
},
PUBLIC_API
)

const { strategy } = createPreStartStrategyWithDefaults()
const userInitConfiguration: RumInitConfiguration = {
...DEFAULT_INIT_CONFIGURATION,
remoteConfigurationId: '123',
}
strategy.init(userInitConfiguration, PUBLIC_API)

expect(strategy.initConfiguration).toEqual(userInitConfiguration)
})
})

Expand Down
14 changes: 5 additions & 9 deletions packages/rum-core/src/boot/preStartRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
buildAccountContextManager,
buildGlobalContextManager,
buildUserContextManager,
monitorError,
sanitize,
startTelemetry,
TelemetryService,
Expand All @@ -33,9 +32,10 @@ import {
import type { Hooks } from '../domain/hooks'
import { createHooks } from '../domain/hooks'
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'

import {
getRemoteConfiguration,
validateAndBuildRumConfiguration,
fetchAndApplyRemoteConfiguration,
serializeRumConfiguration,
} from '../domain/configuration'
import type { ViewOptions } from '../domain/view/trackViews'
Expand Down Expand Up @@ -218,13 +218,9 @@ export function createPreStartStrategy(
callPluginsMethod(initConfiguration.plugins, 'onInit', { initConfiguration, publicApi })

if (initConfiguration.remoteConfigurationId) {
fetchAndApplyRemoteConfiguration(initConfiguration, { user: userContext, context: globalContext })
.then((initConfiguration) => {
if (initConfiguration) {
doInit(initConfiguration, errorStack)
}
})
.catch(monitorError)
const supportedContextManagers = { user: userContext, context: globalContext }

doInit(getRemoteConfiguration(initConfiguration, supportedContextManagers), errorStack)
} else {
doInit(initConfiguration, errorStack)
}
Expand Down
1 change: 1 addition & 0 deletions packages/rum-core/src/domain/configuration/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './configuration'
export * from './remoteConfiguration'
export * from './remoteConfigurationCache'
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
applyRemoteConfiguration,
buildEndpoint,
fetchRemoteConfiguration,
getRemoteConfiguration,
} from './remoteConfiguration'
import { buildCacheKey } from './remoteConfigurationCache'

const DEFAULT_INIT_CONFIGURATION: RumInitConfiguration = {
clientToken: 'xxx',
Expand Down Expand Up @@ -749,4 +751,121 @@ describe('remoteConfiguration', () => {
expect(buildEndpoint({ remoteConfigurationProxy: '/config' } as RumInitConfiguration)).toEqual('/config')
})
})

describe('getRemoteConfiguration', () => {
const REMOTE_CONFIGURATION_ID = 'rc-test-id'
const CACHE_KEY = buildCacheKey(REMOTE_CONFIGURATION_ID)
const FRESH_RUM_CONFIG: RumRemoteConfiguration = { applicationId: 'fresh-app' }
const CACHED_RUM_CONFIG: RumRemoteConfiguration = { applicationId: 'cached-app' }

let initConfiguration: RumInitConfiguration
let supportedContextManagers: {
user: ReturnType<typeof createContextManager>
context: ReturnType<typeof createContextManager>
}
let interceptor: ReturnType<typeof interceptRequests>
let displaySpy: jasmine.Spy

function withCachedEntry(config: RumRemoteConfiguration) {
localStorage.setItem(CACHE_KEY, JSON.stringify({ version: 1, config, fetchedAt: 1000 }))
}

function withFetchSuccess(config: RumRemoteConfiguration = FRESH_RUM_CONFIG) {
interceptor.withFetch(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ rum: config }) }))
}

function withFetchFailure() {
interceptor.withFetch(() => Promise.reject(new Error('Network error')))
}

async function flushBackgroundSync() {
await interceptor.waitForAllFetchCalls()
await new Promise<void>((resolve) => setTimeout(resolve))
}

beforeEach(() => {
initConfiguration = {
...DEFAULT_INIT_CONFIGURATION,
applicationId: 'init-app',
remoteConfigurationId: REMOTE_CONFIGURATION_ID,
}
supportedContextManagers = { user: createContextManager(), context: createContextManager() }
interceptor = interceptRequests()
displaySpy = spyOn(display, 'error')

registerCleanupTask(() => {
localStorage.clear()
})
})

it('should return init configuration on cache miss', async () => {
withFetchSuccess()

const result = getRemoteConfiguration(initConfiguration, supportedContextManagers)

expect(result).toBe(initConfiguration)
await flushBackgroundSync()
})

it('should apply cached configuration to init on cache hit', async () => {
withCachedEntry(CACHED_RUM_CONFIG)
withFetchSuccess()

const result = getRemoteConfiguration(initConfiguration, supportedContextManagers)

expect(result.applicationId).toBe('cached-app')
expect(result.clientToken).toBe('xxx')
await flushBackgroundSync()
})

it('should return init configuration on cache error and remove the corrupted entry', async () => {
localStorage.setItem(CACHE_KEY, 'not-json')
withFetchSuccess()

const result = getRemoteConfiguration(initConfiguration, supportedContextManagers)

expect(result).toBe(initConfiguration)
expect(localStorage.getItem(CACHE_KEY)).toBeNull()
await flushBackgroundSync()
})

it('should write the fetched configuration to cache on background fetch success', async () => {
withFetchSuccess()

getRemoteConfiguration(initConfiguration, supportedContextManagers)
await flushBackgroundSync()

const stored = JSON.parse(localStorage.getItem(CACHE_KEY)!)
expect(stored.config).toEqual(FRESH_RUM_CONFIG)
expect(stored.version).toBe(1)
})

it('should not overwrite cache when background fetch fails', async () => {
withCachedEntry(CACHED_RUM_CONFIG)
withFetchFailure()

getRemoteConfiguration(initConfiguration, supportedContextManagers)
await flushBackgroundSync()

const stored = JSON.parse(localStorage.getItem(CACHE_KEY)!)
expect(stored.config).toEqual(CACHED_RUM_CONFIG)
expect(displaySpy).toHaveBeenCalled()
})

it('should always trigger a background fetch regardless of cache state', async () => {
withCachedEntry(CACHED_RUM_CONFIG)
const fetchSpy = withFetchSuccessReturningSpy()

getRemoteConfiguration(initConfiguration, supportedContextManagers)
await flushBackgroundSync()

expect(fetchSpy).toHaveBeenCalledTimes(1)

function withFetchSuccessReturningSpy() {
return interceptor.withFetch(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve({ rum: FRESH_RUM_CONFIG }) })
)
}
})
})
})
Loading
Loading