Skip to content

Commit 8640c84

Browse files
committed
✨ Deduplicate concurrent remote config fetches across SDK bundles
1 parent f7c8f90 commit 8640c84

5 files changed

Lines changed: 92 additions & 6 deletions

File tree

packages/browser-core/src/domain/remoteConfiguration/remoteConfigurationFetch.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,28 @@ describe('fetchRemoteConfiguration', () => {
5050
expect(result.ok).toBeFalse()
5151
expect((result as { ok: false; error: Error }).error).toBeInstanceOf(Error)
5252
})
53+
54+
it('removes the window registry entry after all fetches settle', async () => {
55+
const config = { rum: { applicationId: 'abc', sessionSampleRate: 50 } }
56+
interceptor.withFetch(() => Promise.resolve({ ok: true, json: () => Promise.resolve(config) }))
57+
58+
await fetchRemoteConfiguration(options)
59+
60+
expect((window as unknown as Record<string, unknown>).__ddRcInflight).toBeUndefined()
61+
})
62+
63+
it('deduplicates concurrent calls for the same endpoint', async () => {
64+
let fetchCount = 0
65+
const config = { rum: { applicationId: 'abc', sessionSampleRate: 50 } }
66+
interceptor.withFetch(() => {
67+
fetchCount++
68+
return Promise.resolve({ ok: true, json: () => Promise.resolve(config) })
69+
})
70+
71+
const [result1, result2] = await Promise.all([fetchRemoteConfiguration(options), fetchRemoteConfiguration(options)])
72+
73+
expect(fetchCount).toBe(1)
74+
expect(result1).toEqual({ ok: true, value: config })
75+
expect(result2).toEqual({ ok: true, value: config })
76+
})
5377
})

packages/browser-core/src/domain/remoteConfiguration/remoteConfigurationFetch.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,42 @@ export function buildEndpoint(options: RemoteConfigurationEndpointOptions): stri
3131
})
3232
}
3333

34-
export async function fetchRemoteConfiguration(
34+
// Use a window-level registry so deduplication works across separate SDK bundles
35+
// (e.g. RUM and Logs loaded as separate CDN scripts on the same page).
36+
const INFLIGHT_FETCHES_KEY = '__ddRcInflight'
37+
38+
function getInflightFetches(): Map<string, Promise<FetchRemoteConfigurationResult>> {
39+
const win = window as unknown as Record<string, unknown>
40+
if (!win[INFLIGHT_FETCHES_KEY]) {
41+
win[INFLIGHT_FETCHES_KEY] = new Map<string, Promise<FetchRemoteConfigurationResult>>()
42+
}
43+
return win[INFLIGHT_FETCHES_KEY] as Map<string, Promise<FetchRemoteConfigurationResult>>
44+
}
45+
46+
export function fetchRemoteConfiguration(
3547
options: RemoteConfigurationEndpointOptions
3648
): Promise<FetchRemoteConfigurationResult> {
49+
const endpoint = buildEndpoint(options)
50+
const inflightFetches = getInflightFetches()
51+
52+
if (!inflightFetches.has(endpoint)) {
53+
const win = window as unknown as Record<string, unknown>
54+
const promise = doFetchRemoteConfiguration(endpoint).finally(() => {
55+
inflightFetches.delete(endpoint)
56+
if (inflightFetches.size === 0) {
57+
delete win[INFLIGHT_FETCHES_KEY]
58+
}
59+
})
60+
inflightFetches.set(endpoint, promise)
61+
}
62+
63+
return inflightFetches.get(endpoint)!
64+
}
65+
66+
async function doFetchRemoteConfiguration(endpoint: string): Promise<FetchRemoteConfigurationResult> {
3767
let response: Response | undefined
3868
try {
39-
response = await fetch(buildEndpoint(options))
69+
response = await fetch(endpoint)
4070
} catch {
4171
response = undefined
4272
}

packages/browser-logs/src/domain/remoteConfiguration.spec.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { CACHE_VERSION, buildCacheKey } from '@datadog/browser-core'
2-
import { interceptRequests, registerCleanupTask } from '@datadog/browser-core/test'
1+
import { CACHE_VERSION, buildCacheKey, display } from '@datadog/browser-core'
2+
import { interceptRequests } from '@datadog/browser-core/test'
33
import type { LogsInitConfiguration } from './configuration'
44
import {
55
applyLogsRemoteConfiguration,
@@ -114,10 +114,12 @@ describe('fetchAndApplyLogsRemoteConfiguration', () => {
114114
expect(result!.forwardErrorsToLogs).toBeFalse()
115115
})
116116

117-
it('returns undefined when the fetch fails', async () => {
117+
it('returns undefined and displays an error when the fetch fails', async () => {
118+
spyOn(display, 'error')
118119
interceptor.withFetch(() => Promise.resolve({ ok: false, status: 500 }))
119120

120121
const result = await fetchAndApplyLogsRemoteConfiguration(DEFAULT_LOGS_INIT)
121122
expect(result).toBeUndefined()
123+
expect(display.error).toHaveBeenCalled()
122124
})
123125
})

packages/browser-logs/src/domain/remoteConfiguration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import type { RemoteConfiguration } from '@datadog/browser-core'
12
import {
2-
type RemoteConfiguration,
33
createConfigurationCache,
44
display,
55
fetchRemoteConfiguration,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Page } from '@playwright/test'
2+
import { test, expect } from '@playwright/test'
3+
import { createTest } from '../lib/framework'
4+
5+
const RC_ID = 'e2e'
6+
7+
test.describe('remote configuration', () => {
8+
createTest('should issue a single fetch when both RUM and Logs share the same remote config ID')
9+
.withRum({ remoteConfigurationId: RC_ID, sessionSampleRate: 100 })
10+
.withLogs({ remoteConfigurationId: RC_ID })
11+
.withRemoteConfiguration({
12+
rum: { applicationId: RC_ID, sessionSampleRate: 100 },
13+
logs: { forwardErrorsToLogs: true },
14+
})
15+
.run(async ({ page }) => {
16+
await waitForRumRemoteConfigurationApplied(page)
17+
18+
const configRequestCount = await page.evaluate(
19+
() => window.performance.getEntriesByType('resource').filter((e) => e.name.endsWith('/config')).length
20+
)
21+
22+
expect(configRequestCount).toBe(1)
23+
})
24+
})
25+
26+
async function waitForRumRemoteConfigurationApplied(page: Page) {
27+
await page.waitForFunction((rcId) => window.DD_RUM?.getInitConfiguration()?.applicationId === rcId, RC_ID, {
28+
timeout: 10000,
29+
})
30+
}

0 commit comments

Comments
 (0)