Skip to content

Commit e1b928f

Browse files
♻️ unify endpoint URL building with buildEndpointUrl
route endpoint construction through a single helper for core intake, profiling quota, and remote configuration URLs, and expand unit tests to cover proxy normalization, subdomain forwarding, default site fallback, and empty parameters.
1 parent 9b6698b commit e1b928f

7 files changed

Lines changed: 158 additions & 48 deletions

File tree

packages/core/src/domain/configuration/endpointBuilder.spec.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Payload } from '../../transport'
22
import type { InitConfiguration } from './configuration'
3-
import { createEndpointBuilder } from './endpointBuilder'
3+
import { buildEndpointUrl, createEndpointBuilder } from './endpointBuilder'
44

55
const DEFAULT_PAYLOAD = {} as Payload
66

@@ -128,4 +128,94 @@ describe('endpointBuilder', () => {
128128
expect(endpoint).toContain('ddsource=unity')
129129
})
130130
})
131+
132+
describe('buildEndpointUrl', () => {
133+
it('builds proxy URL from a string proxy', () => {
134+
expect(
135+
buildEndpointUrl({
136+
proxy: 'https://proxy.io/path',
137+
site: undefined,
138+
path: '/api/v2/rum',
139+
parameters: 'foo=bar',
140+
})
141+
).toBe('https://proxy.io/path?ddforward=%2Fapi%2Fv2%2Frum%3Ffoo%3Dbar')
142+
})
143+
144+
it('normalizes a relative proxy URL', () => {
145+
expect(buildEndpointUrl({ proxy: '/path', site: undefined, path: '/api/v2/rum', parameters: 'foo=bar' })).toBe(
146+
`${location.origin}/path?ddforward=%2Fapi%2Fv2%2Frum%3Ffoo%3Dbar`
147+
)
148+
})
149+
150+
it('adds the subdomain forwarding hint when provided', () => {
151+
expect(
152+
buildEndpointUrl({
153+
proxy: 'https://proxy.io/path',
154+
site: undefined,
155+
path: '/api/v2/profiling/quota',
156+
parameters: 'session_id=abc',
157+
subdomain: 'quota',
158+
})
159+
).toBe(
160+
'https://proxy.io/path?ddforward=%2Fapi%2Fv2%2Fprofiling%2Fquota%3Fsession_id%3Dabc&ddforwardSubdomain=quota'
161+
)
162+
})
163+
164+
it('delegates URL creation to proxy function', () => {
165+
expect(
166+
buildEndpointUrl({
167+
proxy: ({ path, parameters, subdomain }) => `https://${subdomain}.proxy.test${path}?${parameters}`,
168+
site: undefined,
169+
path: '/api/v2/profiling/quota',
170+
parameters: 'session_id=abc',
171+
subdomain: 'quota',
172+
})
173+
).toBe('https://quota.proxy.test/api/v2/profiling/quota?session_id=abc')
174+
})
175+
176+
it('builds intake URL from the site when no proxy is configured', () => {
177+
expect(
178+
buildEndpointUrl({
179+
proxy: undefined,
180+
site: 'datadoghq.com',
181+
path: '/api/v2/rum',
182+
parameters: 'foo=bar',
183+
})
184+
).toBe('https://browser-intake-datadoghq.com/api/v2/rum?foo=bar')
185+
})
186+
187+
it('defaults to US1 site when site is undefined', () => {
188+
expect(
189+
buildEndpointUrl({
190+
proxy: undefined,
191+
site: undefined,
192+
path: '/api/v2/rum',
193+
parameters: 'foo=bar',
194+
})
195+
).toBe('https://browser-intake-datadoghq.com/api/v2/rum?foo=bar')
196+
})
197+
198+
it('omits the query separator when parameters are empty', () => {
199+
expect(
200+
buildEndpointUrl({
201+
proxy: undefined,
202+
site: 'datadoghq.com',
203+
path: '/api/v2/rum',
204+
parameters: '',
205+
})
206+
).toBe('https://browser-intake-datadoghq.com/api/v2/rum')
207+
})
208+
209+
it('adds subdomain to the intake host', () => {
210+
expect(
211+
buildEndpointUrl({
212+
proxy: undefined,
213+
site: 'datadoghq.com',
214+
path: '/api/v2/profiling/quota',
215+
parameters: 'session_id=abc',
216+
subdomain: 'quota',
217+
})
218+
).toBe('https://quota.browser-intake-datadoghq.com/api/v2/profiling/quota?session_id=abc')
219+
})
220+
})
131221
})

packages/core/src/domain/configuration/endpointBuilder.ts

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { Payload } from '../../transport'
22
import { timeStampNow } from '../../tools/utils/timeUtils'
33
import { normalizeUrl } from '../../tools/utils/urlPolyfill'
44
import { generateUUID } from '../../tools/utils/stringUtils'
5+
import type { Site } from '../intakeSites'
56
import { INTAKE_SITE_US1 } from '../intakeSites'
6-
import type { InitConfiguration } from './configuration'
7+
import type { InitConfiguration, ProxyFn } from './configuration'
78

89
// replaced at build time
910
declare const __BUILD_ENV__SDK_VERSION__: string
@@ -39,45 +40,59 @@ export function createEndpointBuilder(
3940
trackType: TrackType,
4041
extraParameters?: string[]
4142
) {
42-
const buildUrlWithParameters = createEndpointUrlWithParametersBuilder(initConfiguration, trackType)
43-
4443
return {
4544
build(api: ApiType, payload: Payload) {
46-
const parameters = buildEndpointParameters(initConfiguration, trackType, api, payload, extraParameters)
47-
return buildUrlWithParameters(parameters)
45+
return buildEndpointUrl({
46+
proxy: initConfiguration.proxy,
47+
site: initConfiguration.site,
48+
path: `/api/v2/${trackType}`,
49+
parameters: buildEndpointParameters(initConfiguration, trackType, api, payload, extraParameters),
50+
})
4851
},
4952
trackType,
5053
}
5154
}
5255

53-
/**
54-
* Create a function used to build a full endpoint url from provided parameters. The goal of this
55-
* function is to pre-compute some parts of the URL to avoid re-computing everything on every
56-
* request, as only parameters are changing.
57-
*/
58-
function createEndpointUrlWithParametersBuilder(
59-
initConfiguration: EndpointBuilderInitConfiguration,
60-
trackType: TrackType
61-
): (parameters: string) => string {
62-
const path = `/api/v2/${trackType}`
63-
const proxy = initConfiguration.proxy
56+
export interface BuildEndpointUrlOptions {
57+
proxy?: string | ProxyFn
58+
site: Site | undefined
59+
subdomain?: string
60+
path: string
61+
parameters: string
62+
}
63+
64+
export function buildEndpointUrl({
65+
proxy,
66+
site = INTAKE_SITE_US1,
67+
path,
68+
parameters,
69+
subdomain,
70+
}: BuildEndpointUrlOptions): string {
71+
let pathAndParameters = path
72+
if (parameters) {
73+
pathAndParameters += `?${parameters}`
74+
}
75+
6476
if (typeof proxy === 'string') {
65-
const normalizedProxyUrl = normalizeUrl(proxy)
66-
return (parameters) => `${normalizedProxyUrl}?ddforward=${encodeURIComponent(`${path}?${parameters}`)}`
77+
let url = `${normalizeUrl(proxy)}?ddforward=${encodeURIComponent(pathAndParameters)}`
78+
if (subdomain) {
79+
url += `&ddforwardSubdomain=${subdomain}`
80+
}
81+
return url
6782
}
83+
6884
if (typeof proxy === 'function') {
69-
return (parameters) => proxy({ path, parameters })
85+
return proxy({ path, parameters, subdomain })
7086
}
71-
const host = buildEndpointHost(initConfiguration)
72-
return (parameters) => `https://${host}${path}?${parameters}`
73-
}
74-
75-
export function buildEndpointHost(initConfiguration: Omit<InitConfiguration, 'source'>) {
76-
const { site = INTAKE_SITE_US1 } = initConfiguration
7787

7888
const domainParts = site.split('.')
7989
const extension = domainParts.pop()
80-
return `browser-intake-${domainParts.join('-')}.${extension!}`
90+
let domain = `browser-intake-${domainParts.join('-')}.${extension!}`
91+
if (subdomain) {
92+
domain = `${subdomain}.${domain}`
93+
}
94+
95+
return `https://${domain}${pathAndParameters}`
8196
}
8297

8398
/**

packages/core/src/domain/configuration/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ export {
77
serializeConfiguration,
88
} from './configuration'
99
export type { EndpointBuilder, TrackType } from './endpointBuilder'
10-
export { createEndpointBuilder, buildEndpointHost } from './endpointBuilder'
10+
export { createEndpointBuilder, buildEndpointUrl } from './endpointBuilder'
1111
export { computeTransportConfiguration, isIntakeUrl } from './transportConfiguration'

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export {
55
TraceContextInjection,
66
serializeConfiguration,
77
isSampleRate,
8-
buildEndpointHost,
8+
buildEndpointUrl,
99
isIntakeUrl,
1010
computeTransportConfiguration,
1111
} from './domain/configuration'

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { createContextManager, Context } from '@datadog/browser-core'
22
import {
33
display,
4-
buildEndpointHost,
54
mapValues,
65
getCookie,
76
addTelemetryMetrics,
87
TelemetryMetrics,
98
monitorError,
109
isIndexableObject,
1110
fetch,
11+
buildEndpointUrl,
1212
} from '@datadog/browser-core'
1313
import { extractRegexMatch } from '../extractRegexMatch'
1414
import type { RumInitConfiguration } from './configuration'
@@ -321,7 +321,12 @@ export function buildEndpoint(configuration: RumInitConfiguration) {
321321
return configuration.remoteConfigurationProxy
322322
}
323323
const id = getRemoteConfigurationId(configuration)!
324-
return `https://sdk-configuration.${buildEndpointHost(configuration)}/${REMOTE_CONFIGURATION_VERSION}/${encodeURIComponent(id)}.json`
324+
return buildEndpointUrl({
325+
site: configuration.site,
326+
path: `/${REMOTE_CONFIGURATION_VERSION}/${encodeURIComponent(id)}.json`,
327+
subdomain: 'sdk-configuration',
328+
parameters: '',
329+
})
325330
}
326331

327332
function doBackgroundCacheSync(

packages/rum/src/domain/profiling/quotaCheck.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,15 @@ describe('checkProfilingQuota', () => {
117117
interceptor.withFetch(backendResponse(true, 'quota_ok'))
118118
await checkProfilingQuota(mockRumConfiguration({ proxy: 'http://proxy.example.com' }), 'session-abc')
119119
expect(interceptor.requests[0].url).toBe(
120-
'http://proxy.example.com?ddforward=%2Fapi%2Fv2%2Fprofiling%2Fquota%3Fsession_id%3Dsession-abc&ddforwardSubdomain=quota'
120+
'http://proxy.example.com/?ddforward=%2Fapi%2Fv2%2Fprofiling%2Fquota%3Fsession_id%3Dsession-abc&ddforwardSubdomain=quota'
121+
)
122+
})
123+
124+
it('normalizes string proxy URLs', async () => {
125+
interceptor.withFetch(backendResponse(true, 'quota_ok'))
126+
await checkProfilingQuota(mockRumConfiguration({ proxy: '/proxy' }), 'session-abc')
127+
expect(interceptor.requests[0].url).toBe(
128+
`${location.origin}/proxy?ddforward=%2Fapi%2Fv2%2Fprofiling%2Fquota%3Fsession_id%3Dsession-abc&ddforwardSubdomain=quota`
121129
)
122130
})
123131

packages/rum/src/domain/profiling/quotaCheck.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fetch, setTimeout, clearTimeout, buildEndpointHost } from '@datadog/browser-core'
1+
import { fetch, setTimeout, clearTimeout, buildEndpointUrl } from '@datadog/browser-core'
22
import type { RumConfiguration } from '@datadog/browser-rum-core'
33

44
// Reason strings surfaced on RUM events. Backend reasons backend_unavailable and
@@ -29,21 +29,13 @@ function parseQuotaResult(body: unknown, httpStatusFallback: QuotaResult): Quota
2929
}
3030

3131
function buildQuotaUrl(configuration: RumConfiguration, sessionId: string): string {
32-
const path = '/api/v2/profiling/quota'
33-
const parameters = `session_id=${sessionId}`
34-
const proxy = configuration.proxy
35-
const host = `quota.${buildEndpointHost({ site: configuration.site, clientToken: configuration.clientToken })}`
36-
37-
if (typeof proxy === 'string') {
38-
// ddforwardSubdomain tells the proxy which intake subdomain to target.
39-
return `${proxy}?ddforward=${encodeURIComponent(`${path}?${parameters}`)}&ddforwardSubdomain=quota`
40-
}
41-
42-
if (typeof proxy === 'function') {
43-
return proxy({ path, parameters, subdomain: 'quota' })
44-
}
45-
46-
return `https://${host}${path}?${parameters}`
32+
return buildEndpointUrl({
33+
proxy: configuration.proxy,
34+
site: configuration.site,
35+
path: '/api/v2/profiling/quota',
36+
parameters: `session_id=${sessionId}`,
37+
subdomain: 'quota',
38+
})
4739
}
4840

4941
export function checkProfilingQuota(

0 commit comments

Comments
 (0)