Skip to content

Commit b775fec

Browse files
committed
feat: implement remote configuration caching mechanism
1 parent 9b44f2f commit b775fec

8 files changed

Lines changed: 504 additions & 52 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: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
mockEventBridge,
2525
mockSyntheticsWorkerValues,
2626
createFakeTelemetryObject,
27+
registerCleanupTask,
2728
replaceMockable,
2829
replaceMockableWithSpy,
2930
createStartSessionManagerMock,
@@ -422,29 +423,54 @@ describe('preStartRum', () => {
422423
})
423424

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

427429
beforeEach(() => {
428-
interceptor = interceptRequests()
429-
})
430+
localStorage.clear()
430431

431-
it('should start with the remote configuration when a remoteConfigurationId is provided', async () => {
432+
interceptor = interceptRequests()
432433
interceptor.withFetch(() =>
433434
Promise.resolve({
434435
ok: true,
435436
json: () => Promise.resolve({ rum: { sessionSampleRate: 50 } }),
436437
})
437438
)
439+
440+
registerCleanupTask(() => {
441+
localStorage.clear()
442+
})
443+
})
444+
445+
it('should start synchronously with init configuration on cache miss', async () => {
438446
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
447+
439448
strategy.init(
440449
{
441450
...DEFAULT_INIT_CONFIGURATION,
442-
remoteConfigurationId: '123',
451+
remoteConfigurationId: REMOTE_CONFIGURATION_ID,
452+
sessionSampleRate: 25,
443453
},
444454
PUBLIC_API
445455
)
456+
446457
await collectAsyncCalls(doStartRumSpy, 1)
447-
expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toEqual(50)
458+
expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toBe(25)
459+
})
460+
461+
it('should trigger a background fetch to the remote configuration endpoint', async () => {
462+
const { strategy } = createPreStartStrategyWithDefaults()
463+
464+
strategy.init(
465+
{
466+
...DEFAULT_INIT_CONFIGURATION,
467+
remoteConfigurationId: REMOTE_CONFIGURATION_ID,
468+
},
469+
PUBLIC_API
470+
)
471+
472+
await interceptor.waitForAllFetchCalls()
473+
expect(interceptor.requests.some((r) => r.url.includes(REMOTE_CONFIGURATION_ID))).toBeTrue()
448474
})
449475
})
450476

@@ -515,8 +541,14 @@ describe('preStartRum', () => {
515541
let interceptor: ReturnType<typeof interceptRequests>
516542

517543
beforeEach(() => {
544+
localStorage.clear()
545+
518546
interceptor = interceptRequests()
519547
initConfiguration = { ...DEFAULT_INIT_CONFIGURATION, service: 'my-service', version: '1.4.2', env: 'dev' }
548+
549+
registerCleanupTask(() => {
550+
localStorage.clear()
551+
})
520552
})
521553

522554
it('is undefined before init', () => {
@@ -544,26 +576,22 @@ describe('preStartRum', () => {
544576
expect(strategy.initConfiguration).toEqual(initConfiguration)
545577
})
546578

547-
it('returns the initConfiguration with the remote configuration when a remoteConfigurationId is provided', (done) => {
579+
it('exposes the user configuration when a remoteConfigurationId is provided (cache miss)', () => {
548580
interceptor.withFetch(() =>
549581
Promise.resolve({
550582
ok: true,
551583
json: () => Promise.resolve({ rum: { sessionSampleRate: 50 } }),
552584
})
553585
)
554-
const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()
555-
doStartRumSpy.and.callFake(() => {
556-
expect(strategy.initConfiguration?.sessionSampleRate).toEqual(50)
557-
done()
558-
return {} as StartRumResult
559-
})
560-
strategy.init(
561-
{
562-
...DEFAULT_INIT_CONFIGURATION,
563-
remoteConfigurationId: '123',
564-
},
565-
PUBLIC_API
566-
)
586+
587+
const { strategy } = createPreStartStrategyWithDefaults()
588+
const userInitConfiguration: RumInitConfiguration = {
589+
...DEFAULT_INIT_CONFIGURATION,
590+
remoteConfigurationId: '123',
591+
}
592+
strategy.init(userInitConfiguration, PUBLIC_API)
593+
594+
expect(strategy.initConfiguration).toEqual(userInitConfiguration)
567595
})
568596
})
569597

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ import {
3737
import type { Hooks } from '../domain/hooks'
3838
import { createHooks } from '../domain/hooks'
3939
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'
40+
4041
import {
42+
getRemoteConfiguration,
4143
validateAndBuildRumConfiguration,
42-
fetchAndApplyRemoteConfiguration,
4344
serializeRumConfiguration,
4445
} from '../domain/configuration'
4546
import type { ViewOptions } from '../domain/view/trackViews'
@@ -244,13 +245,9 @@ export function createPreStartStrategy(
244245
callPluginsMethod(initConfiguration.plugins, 'onInit', { initConfiguration, publicApi })
245246

246247
if (initConfiguration.remoteConfigurationId) {
247-
fetchAndApplyRemoteConfiguration(initConfiguration, { user: userContext, context: globalContext })
248-
.then((initConfiguration) => {
249-
if (initConfiguration) {
250-
doInit(initConfiguration, errorStack)
251-
}
252-
})
253-
.catch(monitorError)
248+
const supportedContextManagers = { user: userContext, context: globalContext }
249+
250+
doInit(getRemoteConfiguration(initConfiguration, supportedContextManagers), errorStack)
254251
} else {
255252
doInit(initConfiguration, errorStack)
256253
}
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)