Skip to content

Commit 13884a7

Browse files
committed
feat: implement remote configuration caching mechanism
1 parent 5f1d903 commit 13884a7

8 files changed

Lines changed: 505 additions & 57 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ generated-docs/
1717
.env*
1818
!.env.example
1919
.rum-ai-toolkit/
20+
.idea/
2021

2122
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
2223
.pnp.*

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

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
mockEventBridge,
2121
mockSyntheticsWorkerValues,
2222
createFakeTelemetryObject,
23+
registerCleanupTask,
2324
replaceMockableWithSpy,
2425
} from '@datadog/browser-core/test'
2526
import type { HybridInitConfiguration, RumInitConfiguration } from '../domain/configuration'
@@ -402,32 +403,54 @@ describe('preStartRum', () => {
402403
})
403404

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

407409
beforeEach(() => {
408-
interceptor = interceptRequests()
409-
})
410+
localStorage.clear()
410411

411-
it('should start with the remote configuration when a remoteConfigurationId is provided', (done) => {
412+
interceptor = interceptRequests()
412413
interceptor.withFetch(() =>
413414
Promise.resolve({
414415
ok: true,
415416
json: () => Promise.resolve({ rum: { sessionSampleRate: 50 } }),
416417
})
417418
)
418-
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
419-
doStartRumSpy.and.callFake((configuration) => {
420-
expect(configuration.sessionSampleRate).toEqual(50)
421-
done()
422-
return {} as StartRumResult
419+
420+
registerCleanupTask(() => {
421+
localStorage.clear()
423422
})
423+
})
424+
425+
it('should start synchronously with init configuration on cache miss', () => {
426+
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
427+
428+
strategy.init(
429+
{
430+
...DEFAULT_INIT_CONFIGURATION,
431+
remoteConfigurationId: REMOTE_CONFIGURATION_ID,
432+
sessionSampleRate: 25,
433+
},
434+
PUBLIC_API
435+
)
436+
437+
expect(doStartRumSpy).toHaveBeenCalledTimes(1)
438+
expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toBe(25)
439+
})
440+
441+
it('should trigger a background fetch to the remote configuration endpoint', async () => {
442+
const { strategy } = createPreStartStrategyWithDefaults()
443+
424444
strategy.init(
425445
{
426446
...DEFAULT_INIT_CONFIGURATION,
427-
remoteConfigurationId: '123',
447+
remoteConfigurationId: REMOTE_CONFIGURATION_ID,
428448
},
429449
PUBLIC_API
430450
)
451+
452+
await interceptor.waitForAllFetchCalls()
453+
expect(interceptor.requests.some((r) => r.url.includes(REMOTE_CONFIGURATION_ID))).toBeTrue()
431454
})
432455
})
433456

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

499522
beforeEach(() => {
523+
localStorage.clear()
524+
500525
interceptor = interceptRequests()
501526
initConfiguration = { ...DEFAULT_INIT_CONFIGURATION, service: 'my-service', version: '1.4.2', env: 'dev' }
527+
528+
registerCleanupTask(() => {
529+
localStorage.clear()
530+
})
502531
})
503532

504533
it('is undefined before init', () => {
@@ -526,26 +555,22 @@ describe('preStartRum', () => {
526555
expect(strategy.initConfiguration).toEqual(initConfiguration)
527556
})
528557

529-
it('returns the initConfiguration with the remote configuration when a remoteConfigurationId is provided', (done) => {
558+
it('exposes the user configuration when a remoteConfigurationId is provided (cache miss)', () => {
530559
interceptor.withFetch(() =>
531560
Promise.resolve({
532561
ok: true,
533562
json: () => Promise.resolve({ rum: { sessionSampleRate: 50 } }),
534563
})
535564
)
536-
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
537-
doStartRumSpy.and.callFake(() => {
538-
expect(strategy.initConfiguration?.sessionSampleRate).toEqual(50)
539-
done()
540-
return {} as StartRumResult
541-
})
542-
strategy.init(
543-
{
544-
...DEFAULT_INIT_CONFIGURATION,
545-
remoteConfigurationId: '123',
546-
},
547-
PUBLIC_API
548-
)
565+
566+
const { strategy } = createPreStartStrategyWithDefaults()
567+
const userInitConfiguration: RumInitConfiguration = {
568+
...DEFAULT_INIT_CONFIGURATION,
569+
remoteConfigurationId: '123',
570+
}
571+
strategy.init(userInitConfiguration, PUBLIC_API)
572+
573+
expect(strategy.initConfiguration).toEqual(userInitConfiguration)
549574
})
550575
})
551576

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
buildAccountContextManager,
2525
buildGlobalContextManager,
2626
buildUserContextManager,
27-
monitorError,
2827
sanitize,
2928
startTelemetry,
3029
TelemetryService,
@@ -33,9 +32,10 @@ import {
3332
import type { Hooks } from '../domain/hooks'
3433
import { createHooks } from '../domain/hooks'
3534
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'
35+
3636
import {
37+
getRemoteConfiguration,
3738
validateAndBuildRumConfiguration,
38-
fetchAndApplyRemoteConfiguration,
3939
serializeRumConfiguration,
4040
} from '../domain/configuration'
4141
import type { ViewOptions } from '../domain/view/trackViews'
@@ -218,13 +218,9 @@ export function createPreStartStrategy(
218218
callPluginsMethod(initConfiguration.plugins, 'onInit', { initConfiguration, publicApi })
219219

220220
if (initConfiguration.remoteConfigurationId) {
221-
fetchAndApplyRemoteConfiguration(initConfiguration, { user: userContext, context: globalContext })
222-
.then((initConfiguration) => {
223-
if (initConfiguration) {
224-
doInit(initConfiguration, errorStack)
225-
}
226-
})
227-
.catch(monitorError)
221+
const supportedContextManagers = { user: userContext, context: globalContext }
222+
223+
doInit(getRemoteConfiguration(initConfiguration, supportedContextManagers), errorStack)
228224
} else {
229225
doInit(initConfiguration, errorStack)
230226
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './configuration'
22
export * from './remoteConfiguration'
3+
export * from './remoteConfigurationCache'

packages/rum-core/src/domain/configuration/remoteConfiguration.spec.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
applyRemoteConfiguration,
1717
buildEndpoint,
1818
fetchRemoteConfiguration,
19+
getRemoteConfiguration,
1920
} from './remoteConfiguration'
21+
import { buildCacheKey } from './remoteConfigurationCache'
2022

2123
const DEFAULT_INIT_CONFIGURATION: RumInitConfiguration = {
2224
clientToken: 'xxx',
@@ -749,4 +751,121 @@ describe('remoteConfiguration', () => {
749751
expect(buildEndpoint({ remoteConfigurationProxy: '/config' } as RumInitConfiguration)).toEqual('/config')
750752
})
751753
})
754+
755+
describe('getRemoteConfiguration', () => {
756+
const REMOTE_CONFIGURATION_ID = 'rc-test-id'
757+
const CACHE_KEY = buildCacheKey(REMOTE_CONFIGURATION_ID)
758+
const FRESH_RUM_CONFIG: RumRemoteConfiguration = { applicationId: 'fresh-app' }
759+
const CACHED_RUM_CONFIG: RumRemoteConfiguration = { applicationId: 'cached-app' }
760+
761+
let initConfiguration: RumInitConfiguration
762+
let supportedContextManagers: {
763+
user: ReturnType<typeof createContextManager>
764+
context: ReturnType<typeof createContextManager>
765+
}
766+
let interceptor: ReturnType<typeof interceptRequests>
767+
let displaySpy: jasmine.Spy
768+
769+
function withCachedEntry(config: RumRemoteConfiguration) {
770+
localStorage.setItem(CACHE_KEY, JSON.stringify({ version: 1, config, fetchedAt: 1000 }))
771+
}
772+
773+
function withFetchSuccess(config: RumRemoteConfiguration = FRESH_RUM_CONFIG) {
774+
interceptor.withFetch(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ rum: config }) }))
775+
}
776+
777+
function withFetchFailure() {
778+
interceptor.withFetch(() => Promise.reject(new Error('Network error')))
779+
}
780+
781+
async function flushBackgroundSync() {
782+
await interceptor.waitForAllFetchCalls()
783+
await new Promise<void>((resolve) => setTimeout(resolve))
784+
}
785+
786+
beforeEach(() => {
787+
initConfiguration = {
788+
...DEFAULT_INIT_CONFIGURATION,
789+
applicationId: 'init-app',
790+
remoteConfigurationId: REMOTE_CONFIGURATION_ID,
791+
}
792+
supportedContextManagers = { user: createContextManager(), context: createContextManager() }
793+
interceptor = interceptRequests()
794+
displaySpy = spyOn(display, 'error')
795+
796+
registerCleanupTask(() => {
797+
localStorage.clear()
798+
})
799+
})
800+
801+
it('should return init configuration on cache miss', async () => {
802+
withFetchSuccess()
803+
804+
const result = getRemoteConfiguration(initConfiguration, supportedContextManagers)
805+
806+
expect(result).toBe(initConfiguration)
807+
await flushBackgroundSync()
808+
})
809+
810+
it('should apply cached configuration to init on cache hit', async () => {
811+
withCachedEntry(CACHED_RUM_CONFIG)
812+
withFetchSuccess()
813+
814+
const result = getRemoteConfiguration(initConfiguration, supportedContextManagers)
815+
816+
expect(result.applicationId).toBe('cached-app')
817+
expect(result.clientToken).toBe('xxx')
818+
await flushBackgroundSync()
819+
})
820+
821+
it('should return init configuration on cache error and remove the corrupted entry', async () => {
822+
localStorage.setItem(CACHE_KEY, 'not-json')
823+
withFetchSuccess()
824+
825+
const result = getRemoteConfiguration(initConfiguration, supportedContextManagers)
826+
827+
expect(result).toBe(initConfiguration)
828+
expect(localStorage.getItem(CACHE_KEY)).toBeNull()
829+
await flushBackgroundSync()
830+
})
831+
832+
it('should write the fetched configuration to cache on background fetch success', async () => {
833+
withFetchSuccess()
834+
835+
getRemoteConfiguration(initConfiguration, supportedContextManagers)
836+
await flushBackgroundSync()
837+
838+
const stored = JSON.parse(localStorage.getItem(CACHE_KEY)!)
839+
expect(stored.config).toEqual(FRESH_RUM_CONFIG)
840+
expect(stored.version).toBe(1)
841+
})
842+
843+
it('should not overwrite cache when background fetch fails', async () => {
844+
withCachedEntry(CACHED_RUM_CONFIG)
845+
withFetchFailure()
846+
847+
getRemoteConfiguration(initConfiguration, supportedContextManagers)
848+
await flushBackgroundSync()
849+
850+
const stored = JSON.parse(localStorage.getItem(CACHE_KEY)!)
851+
expect(stored.config).toEqual(CACHED_RUM_CONFIG)
852+
expect(displaySpy).toHaveBeenCalled()
853+
})
854+
855+
it('should always trigger a background fetch regardless of cache state', async () => {
856+
withCachedEntry(CACHED_RUM_CONFIG)
857+
const fetchSpy = withFetchSuccessReturningSpy()
858+
859+
getRemoteConfiguration(initConfiguration, supportedContextManagers)
860+
await flushBackgroundSync()
861+
862+
expect(fetchSpy).toHaveBeenCalledTimes(1)
863+
864+
function withFetchSuccessReturningSpy() {
865+
return interceptor.withFetch(() =>
866+
Promise.resolve({ ok: true, json: () => Promise.resolve({ rum: FRESH_RUM_CONFIG }) })
867+
)
868+
}
869+
})
870+
})
752871
})

0 commit comments

Comments
 (0)