diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 58913c33ed..37d17c152c 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -30,6 +30,10 @@ export interface InitConfiguration { trackLongTasks?: boolean | undefined // transport options + proxy?: string | undefined + /** + * @deprecated use `proxy` instead + */ proxyUrl?: string | undefined site?: string | undefined @@ -164,6 +168,7 @@ function mustUseSecureCookie(initConfiguration: InitConfiguration) { } export function serializeConfiguration(configuration: InitConfiguration): Partial { + const proxy = configuration.proxy ?? configuration.proxyUrl return { session_sample_rate: configuration.sessionSampleRate ?? configuration.sampleRate, telemetry_sample_rate: configuration.telemetrySampleRate, @@ -171,7 +176,7 @@ export function serializeConfiguration(configuration: InitConfiguration): Partia use_before_send: !!configuration.beforeSend, use_cross_site_session_cookie: configuration.useCrossSiteSessionCookie, use_secure_session_cookie: configuration.useSecureSessionCookie, - use_proxy: configuration.proxyUrl !== undefined ? !!configuration.proxyUrl : undefined, + use_proxy: proxy !== undefined ? !!proxy : undefined, silent_multiple_init: configuration.silentMultipleInit, track_session_across_subdomains: configuration.trackSessionAcrossSubdomains, track_resources: configuration.trackResources, diff --git a/packages/core/src/domain/configuration/endpointBuilder.spec.ts b/packages/core/src/domain/configuration/endpointBuilder.spec.ts index 3e9109423c..634b212bcd 100644 --- a/packages/core/src/domain/configuration/endpointBuilder.spec.ts +++ b/packages/core/src/domain/configuration/endpointBuilder.spec.ts @@ -1,4 +1,5 @@ import type { BuildEnvWindow } from '../../../test/specHelper' +import { startsWith } from '../../tools/utils' import type { InitConfiguration } from './configuration' import { createEndpointBuilder } from './endpointBuilder' @@ -36,7 +37,47 @@ describe('endpointBuilder', () => { }) }) - describe('proxyUrl', () => { + describe('proxy configuration', () => { + it('should replace the intake endpoint by the proxy and set the intake path and parameters in the attribute ddforward', () => { + expect( + createEndpointBuilder({ ...initConfiguration, proxy: 'https://proxy.io/path' }, 'rum', []).build('xhr') + ).toMatch( + `https://proxy.io/path\\?ddforward=${encodeURIComponent( + `/api/v2/rum?ddsource=(.*)&ddtags=(.*)&dd-api-key=${clientToken}` + + '&dd-evp-origin-version=(.*)&dd-evp-origin=browser&dd-request-id=(.*)&batch_time=(.*)' + )}` + ) + }) + + it('normalizes the proxy url', () => { + expect( + startsWith( + createEndpointBuilder({ ...initConfiguration, proxy: '/path' }, 'rum', []).build('xhr'), + `${location.origin}/path?ddforward` + ) + ).toBeTrue() + }) + + it('uses `proxy` over `proxyUrl`', () => { + expect( + createEndpointBuilder( + { ...initConfiguration, proxy: 'https://proxy.io/path', proxyUrl: 'https://legacy-proxy.io/path' }, + 'rum', + [] + ).build('xhr') + ).toMatch(/^https:\/\/proxy.io\/path\?/) + + expect( + createEndpointBuilder( + { ...initConfiguration, proxy: false as any, proxyUrl: 'https://legacy-proxy.io/path' }, + 'rum', + [] + ).build('xhr') + ).toMatch(/^https:\/\/rum.browser-intake-datadoghq.com\//) + }) + }) + + describe('deprecated proxyUrl configuration', () => { it('should replace the full intake endpoint by the proxyUrl and set it in the attribute ddforward', () => { expect( createEndpointBuilder({ ...initConfiguration, proxyUrl: 'https://proxy.io/path' }, 'rum', []).build('xhr') @@ -47,6 +88,15 @@ describe('endpointBuilder', () => { )}` ) }) + + it('normalizes the proxy url', () => { + expect( + startsWith( + createEndpointBuilder({ ...initConfiguration, proxyUrl: '/path' }, 'rum', []).build('xhr'), + `${location.origin}/path?ddforward` + ) + ).toBeTrue() + }) }) describe('tags', () => { diff --git a/packages/core/src/domain/configuration/endpointBuilder.ts b/packages/core/src/domain/configuration/endpointBuilder.ts index 608beba658..93773ab029 100644 --- a/packages/core/src/domain/configuration/endpointBuilder.ts +++ b/packages/core/src/domain/configuration/endpointBuilder.ts @@ -29,44 +29,47 @@ export function createEndpointBuilder( endpointType: EndpointType, configurationTags: string[] ) { - const { clientToken } = initConfiguration - - const host = buildEndpointHost(initConfiguration, endpointType) - const baseUrl = `https://${host}/api/v2/${INTAKE_TRACKS[endpointType]}` - const proxyUrl = initConfiguration.proxyUrl && normalizeUrl(initConfiguration.proxyUrl) + const buildUrlWithParameters = createEndpointUrlWithParametersBuilder(initConfiguration, endpointType) return { build(api: 'xhr' | 'fetch' | 'beacon', retry?: RetryInfo) { - const tags = [`sdk_version:${__BUILD_ENV__SDK_VERSION__}`, `api:${api}`].concat(configurationTags) - if (retry) { - tags.push(`retry_count:${retry.count}`, `retry_after:${retry.lastFailureStatus}`) - } - const parameters = [ - 'ddsource=browser', - `ddtags=${encodeURIComponent(tags.join(','))}`, - `dd-api-key=${clientToken}`, - `dd-evp-origin-version=${encodeURIComponent(__BUILD_ENV__SDK_VERSION__)}`, - 'dd-evp-origin=browser', - `dd-request-id=${generateUUID()}`, - ] - - if (endpointType === 'rum') { - parameters.push(`batch_time=${timeStampNow()}`) - } - if (initConfiguration.internalAnalyticsSubdomain) { - parameters.reverse() - } - const endpointUrl = `${baseUrl}?${parameters.join('&')}` - - return proxyUrl ? `${proxyUrl}?ddforward=${encodeURIComponent(endpointUrl)}` : endpointUrl - }, - buildIntakeUrl() { - return proxyUrl ? `${proxyUrl}?ddforward` : baseUrl + const parameters = buildEndpointParameters(initConfiguration, endpointType, configurationTags, api, retry) + return buildUrlWithParameters(parameters) }, + urlPrefix: buildUrlWithParameters(''), endpointType, } } +/** + * Create a function used to build a full endpoint url from provided parameters. The goal of this + * function is to pre-compute some parts of the URL to avoid re-computing everything on every + * request, as only parameters are changing. + */ +function createEndpointUrlWithParametersBuilder( + initConfiguration: InitConfiguration, + endpointType: EndpointType +): (parameters: string) => string { + const path = `/api/v2/${INTAKE_TRACKS[endpointType]}` + + const { proxy, proxyUrl } = initConfiguration + if (proxy) { + const normalizedProxyUrl = normalizeUrl(proxy) + return (parameters) => `${normalizedProxyUrl}?ddforward=${encodeURIComponent(`${path}?${parameters}`)}` + } + + const host = buildEndpointHost(initConfiguration, endpointType) + + if (proxy === undefined && proxyUrl) { + // TODO: remove this in a future major. + const normalizedProxyUrl = normalizeUrl(proxyUrl) + return (parameters) => + `${normalizedProxyUrl}?ddforward=${encodeURIComponent(`https://${host}${path}?${parameters}`)}` + } + + return (parameters) => `https://${host}${path}?${parameters}` +} + function buildEndpointHost(initConfiguration: InitConfiguration, endpointType: EndpointType) { const { site = INTAKE_SITE_US1, internalAnalyticsSubdomain } = initConfiguration @@ -79,3 +82,37 @@ function buildEndpointHost(initConfiguration: InitConfiguration, endpointType: E const subdomain = site !== INTAKE_SITE_AP1 ? `${ENDPOINTS[endpointType]}.` : '' return `${subdomain}browser-intake-${domainParts.join('-')}.${extension!}` } + +/** + * Build parameters to be used for an intake request. Parameters should be re-built for each + * request, as they change randomly. + */ +function buildEndpointParameters( + { clientToken, internalAnalyticsSubdomain }: InitConfiguration, + endpointType: EndpointType, + configurationTags: string[], + api: 'xhr' | 'fetch' | 'beacon', + retry: RetryInfo | undefined +) { + const tags = [`sdk_version:${__BUILD_ENV__SDK_VERSION__}`, `api:${api}`].concat(configurationTags) + if (retry) { + tags.push(`retry_count:${retry.count}`, `retry_after:${retry.lastFailureStatus}`) + } + const parameters = [ + 'ddsource=browser', + `ddtags=${encodeURIComponent(tags.join(','))}`, + `dd-api-key=${clientToken}`, + `dd-evp-origin-version=${encodeURIComponent(__BUILD_ENV__SDK_VERSION__)}`, + 'dd-evp-origin=browser', + `dd-request-id=${generateUUID()}`, + ] + + if (endpointType === 'rum') { + parameters.push(`batch_time=${timeStampNow()}`) + } + if (internalAnalyticsSubdomain) { + parameters.reverse() + } + + return parameters.join('&') +} diff --git a/packages/core/src/domain/configuration/transportConfiguration.spec.ts b/packages/core/src/domain/configuration/transportConfiguration.spec.ts index b8c949b840..a33da4f3b6 100644 --- a/packages/core/src/domain/configuration/transportConfiguration.spec.ts +++ b/packages/core/src/domain/configuration/transportConfiguration.spec.ts @@ -97,18 +97,45 @@ describe('transportConfiguration', () => { const configuration = computeTransportConfiguration({ clientToken }) expect(configuration.isIntakeUrl('https://www.foo.com')).toBe(false) }) + ;[ + { + proxyConfigurationName: 'proxy' as const, + intakeUrl: '/api/v2/rum', + }, + { + proxyConfigurationName: 'proxyUrl' as const, + intakeUrl: 'https://rum.browser-intake-datadoghq.com/api/v2/rum', + }, + ].forEach(({ proxyConfigurationName, intakeUrl }) => { + describe(`${proxyConfigurationName} configuration`, () => { + it('should detect proxy intake request', () => { + let configuration = computeTransportConfiguration({ + clientToken, + [proxyConfigurationName]: 'https://www.proxy.com', + }) + expect( + configuration.isIntakeUrl(`https://www.proxy.com/?ddforward=${encodeURIComponent(`${intakeUrl}?foo=bar`)}`) + ).toBe(true) + + configuration = computeTransportConfiguration({ + clientToken, + [proxyConfigurationName]: 'https://www.proxy.com/custom/path', + }) + expect( + configuration.isIntakeUrl( + `https://www.proxy.com/custom/path?ddforward=${encodeURIComponent(`${intakeUrl}?foo=bar`)}` + ) + ).toBe(true) + }) - it('should detect proxy intake request', () => { - let configuration = computeTransportConfiguration({ clientToken, proxyUrl: 'https://www.proxy.com' }) - expect(configuration.isIntakeUrl('https://www.proxy.com/?ddforward=xxx')).toBe(true) - - configuration = computeTransportConfiguration({ clientToken, proxyUrl: 'https://www.proxy.com/custom/path' }) - expect(configuration.isIntakeUrl('https://www.proxy.com/custom/path?ddforward=xxx')).toBe(true) - }) - - it('should not detect request done on the same host as the proxy', () => { - const configuration = computeTransportConfiguration({ clientToken, proxyUrl: 'https://www.proxy.com' }) - expect(configuration.isIntakeUrl('https://www.proxy.com/foo')).toBe(false) + it('should not detect request done on the same host as the proxy', () => { + const configuration = computeTransportConfiguration({ + clientToken, + [proxyConfigurationName]: 'https://www.proxy.com', + }) + expect(configuration.isIntakeUrl('https://www.proxy.com/foo')).toBe(false) + }) + }) }) ;[ { site: 'datadoghq.eu' }, diff --git a/packages/core/src/domain/configuration/transportConfiguration.ts b/packages/core/src/domain/configuration/transportConfiguration.ts index ed811b72dc..71ddf79ac5 100644 --- a/packages/core/src/domain/configuration/transportConfiguration.ts +++ b/packages/core/src/domain/configuration/transportConfiguration.ts @@ -24,13 +24,13 @@ export function computeTransportConfiguration(initConfiguration: InitConfigurati const tags = buildTags(initConfiguration) const endpointBuilders = computeEndpointBuilders(initConfiguration, tags) - const intakeEndpoints = objectValues(endpointBuilders).map((builder) => builder.buildIntakeUrl()) + const intakeUrlPrefixes = objectValues(endpointBuilders).map((builder) => builder.urlPrefix) - const replicaConfiguration = computeReplicaConfiguration(initConfiguration, intakeEndpoints, tags) + const replicaConfiguration = computeReplicaConfiguration(initConfiguration, intakeUrlPrefixes, tags) return assign( { - isIntakeUrl: (url: string) => intakeEndpoints.some((intakeEndpoint) => url.indexOf(intakeEndpoint) === 0), + isIntakeUrl: (url: string) => intakeUrlPrefixes.some((intakeEndpoint) => url.indexOf(intakeEndpoint) === 0), replica: replicaConfiguration, site: initConfiguration.site || INTAKE_SITE_US1, }, @@ -48,7 +48,7 @@ function computeEndpointBuilders(initConfiguration: InitConfiguration, tags: str function computeReplicaConfiguration( initConfiguration: InitConfiguration, - intakeEndpoints: string[], + intakeUrlPrefixes: string[], tags: string[] ): ReplicaConfiguration | undefined { if (!initConfiguration.replica) { @@ -65,7 +65,7 @@ function computeReplicaConfiguration( rumEndpointBuilder: createEndpointBuilder(replicaConfiguration, 'rum', tags), } - intakeEndpoints.push(...objectValues(replicaEndpointBuilders).map((builder) => builder.buildIntakeUrl())) + intakeUrlPrefixes.push(...objectValues(replicaEndpointBuilders).map((builder) => builder.urlPrefix)) return assign({ applicationId: initConfiguration.replica.applicationId }, replicaEndpointBuilders) } diff --git a/test/e2e/lib/framework/pageSetups.ts b/test/e2e/lib/framework/pageSetups.ts index 94bbb8f1c1..269f85de84 100644 --- a/test/e2e/lib/framework/pageSetups.ts +++ b/test/e2e/lib/framework/pageSetups.ts @@ -178,6 +178,14 @@ export function html(parts: readonly string[], ...vars: string[]) { function setupEventBridge(servers: Servers) { const baseHostname = new URL(servers.base.url).hostname + // Send EventBridge events to the intake so we can inspect them in our E2E test cases. The URL + // needs to be similar to the normal Datadog intake (through proxy) to make the SDK completely + // ignore them. + const eventBridgeIntake = `${servers.intake.url}/?${new URLSearchParams({ + ddforward: '/api/v2/rum?', + bridge: 'true', + }).toString()}` + return html`