diff --git a/packages/core/src/domain/configuration.ts b/packages/core/src/domain/configuration.ts index 907a646d94..5449aaf62a 100644 --- a/packages/core/src/domain/configuration.ts +++ b/packages/core/src/domain/configuration.ts @@ -2,7 +2,7 @@ import { BuildEnv } from '../boot/init' import { CookieOptions, getCurrentSite } from '../browser/cookie' import { catchUserErrors } from '../tools/catchUserErrors' import { includes, ONE_KILO_BYTE, ONE_SECOND } from '../tools/utils' -import { computeTransportConfiguration } from './transportConfiguration' +import { computeTransportConfiguration, TransportConfiguration } from './transportConfiguration' export const DEFAULT_CONFIGURATION = { allowedTracingOrigins: [] as Array, @@ -59,6 +59,8 @@ export interface InitConfiguration { version?: string useAlternateIntakeDomains?: boolean + intakeApiVersion?: 1 | 2 + useCrossSiteSessionCookie?: boolean useSecureSessionCookie?: boolean trackSessionAcrossSubdomains?: boolean @@ -86,36 +88,19 @@ export type Configuration = typeof DEFAULT_CONFIGURATION & isEnabled: (feature: string) => boolean } -export interface TransportConfiguration { - logsEndpoint: string - rumEndpoint: string - sessionReplayEndpoint: string - internalMonitoringEndpoint?: string - isIntakeUrl: (url: string) => boolean - - // only on staging build mode - replica?: ReplicaConfiguration -} - -interface ReplicaConfiguration { - applicationId?: string - logsEndpoint: string - rumEndpoint: string - internalMonitoringEndpoint: string -} - export function buildConfiguration(initConfiguration: InitConfiguration, buildEnv: BuildEnv): Configuration { const enableExperimentalFeatures = Array.isArray(initConfiguration.enableExperimentalFeatures) ? initConfiguration.enableExperimentalFeatures : [] + const isEnabled = (feature: string) => includes(enableExperimentalFeatures, feature) const configuration: Configuration = { beforeSend: initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), cookieOptions: buildCookieOptions(initConfiguration), - isEnabled: (feature: string) => includes(enableExperimentalFeatures, feature), + isEnabled, service: initConfiguration.service, - ...computeTransportConfiguration(initConfiguration, buildEnv), + ...computeTransportConfiguration(initConfiguration, buildEnv, isEnabled('support-intake-v2')), ...DEFAULT_CONFIGURATION, } diff --git a/packages/core/src/domain/endpointBuilder.ts b/packages/core/src/domain/endpointBuilder.ts new file mode 100644 index 0000000000..a81a91a14f --- /dev/null +++ b/packages/core/src/domain/endpointBuilder.ts @@ -0,0 +1,111 @@ +import { BuildEnv } from '../boot/init' +import { generateUUID, includes } from '../tools/utils' +import { InitConfiguration } from './configuration' + +export const ENDPOINTS = { + alternate: { + logs: 'logs', + rum: 'rum', + sessionReplay: 'session-replay', + }, + classic: { + logs: 'browser', + rum: 'rum', + // session-replay has no classic endpoint + sessionReplay: undefined, + }, +} + +const INTAKE_TRACKS = { + logs: 'logs', + rum: 'rum', + sessionReplay: 'session-replay', +} + +export const ENDPOINTS_TYPES = Object.keys(ENDPOINTS['alternate']) as EndpointType[] +export type EndpointType = keyof typeof ENDPOINTS[IntakeType] + +export const INTAKE_SITE_US = 'datadoghq.com' +const INTAKE_SITE_US3 = 'us3.datadoghq.com' +const INTAKE_SITE_GOV = 'ddog-gov.com' +const INTAKE_SITE_EU = 'datadoghq.eu' + +const CLASSIC_ALLOWED_SITES = [INTAKE_SITE_US, INTAKE_SITE_EU] +const INTAKE_V1_ALLOWED_SITES = [INTAKE_SITE_US, INTAKE_SITE_US3, INTAKE_SITE_EU, INTAKE_SITE_GOV] + +type IntakeType = keyof typeof ENDPOINTS + +type IntakeApiVersion = 1 | 2 + +export class EndpointBuilder { + private site: string + private clientToken: string + private env: string | undefined + private proxyHost: string | undefined + private sdkVersion: string + private service: string | undefined + private version: string | undefined + private intakeApiVersion: IntakeApiVersion + private useAlternateIntakeDomains: boolean + private isIntakeV2Enabled: boolean + constructor(initConfiguration: InitConfiguration, buildEnv: BuildEnv, isIntakeV2Enabled?: boolean) { + this.isIntakeV2Enabled = !!isIntakeV2Enabled + this.site = initConfiguration.site || INTAKE_SITE_US + this.clientToken = initConfiguration.clientToken + this.env = initConfiguration.env + this.proxyHost = initConfiguration.proxyHost + this.sdkVersion = buildEnv.sdkVersion + this.service = initConfiguration.service + this.version = initConfiguration.version + this.intakeApiVersion = initConfiguration.intakeApiVersion || 1 + this.useAlternateIntakeDomains = !!initConfiguration.useAlternateIntakeDomains + } + + supportIntakeV2 = (endpointType?: EndpointType): boolean => + this.isIntakeV2Enabled && + (this.intakeApiVersion === 2 || !includes(INTAKE_V1_ALLOWED_SITES, this.site) || endpointType === 'sessionReplay') + + supportAlternateDomain = (endpointType?: EndpointType): boolean => + this.useAlternateIntakeDomains || !includes(CLASSIC_ALLOWED_SITES, this.site) || endpointType === 'sessionReplay' + + build(endpointType: EndpointType, source?: string) { + const tags = + `sdk_version:${this.sdkVersion}` + + `${this.env ? `,env:${this.env}` : ''}` + + `${this.service ? `,service:${this.service}` : ''}` + + `${this.version ? `,version:${this.version}` : ''}` + const datadogHost = this.buildHost(endpointType) + const proxyParameter = this.proxyHost ? `ddhost=${datadogHost}&` : '' + const parameters = `${proxyParameter}ddsource=${source || 'browser'}&ddtags=${encodeURIComponent(tags)}` + const newIntakeParameters = this.supportIntakeV2(endpointType) + ? `?dd-api-key=${this.clientToken}&` + + `dd-evp-origin-version=${this.sdkVersion}&` + + `dd-evp-origin=browser&` + + `dd-request-id=${generateUUID()}&` + : `${this.clientToken}?` + + return `${this.buildIntakeUrl(endpointType)}${newIntakeParameters}${parameters}` + } + + buildIntakeUrl(endpointType: EndpointType): string { + const datadogHost = this.buildHost(endpointType) + const host = this.proxyHost ? this.proxyHost : datadogHost + return `https://${host}${this.buildPath(endpointType)}` + } + + private buildHost(endpointType: EndpointType) { + if (this.supportAlternateDomain(endpointType)) { + const endpoint = ENDPOINTS.alternate[endpointType] + const domainParts = this.site.split('.') + const extension = domainParts.pop() + const suffix = `${domainParts.join('-')}.${extension!}` + return `${endpoint}.browser-intake-${suffix}` + } + const endpoint = ENDPOINTS.classic[endpointType]! + return `${endpoint}-http-intake.logs.${this.site}` + } + + private buildPath(endpointType: EndpointType) { + return this.supportIntakeV2(endpointType) ? `/api/v2/${INTAKE_TRACKS[endpointType]}` : `/v1/input/` + } +} diff --git a/packages/core/src/domain/transportConfiguration.spec.ts b/packages/core/src/domain/transportConfiguration.spec.ts index 4e5ca8d47a..f9aeb83a00 100644 --- a/packages/core/src/domain/transportConfiguration.spec.ts +++ b/packages/core/src/domain/transportConfiguration.spec.ts @@ -89,7 +89,7 @@ describe('transportConfiguration', () => { }) }) - describe('isIntakeUrl', () => { + describe('isIntakeUrl when "support-intake-v2" disabled', () => { it('should not detect non intake request', () => { const configuration = computeTransportConfiguration({ clientToken }, buildEnv) expect(configuration.isIntakeUrl('https://www.foo.com')).toBe(false) @@ -191,5 +191,130 @@ describe('transportConfiguration', () => { expect(configuration.isIntakeUrl('https://rum.browser-intake-datadoghq.com/v1/input/xxx')).toBe(true) expect(configuration.isIntakeUrl('https://logs.browser-intake-datadoghq.com/v1/input/xxx')).toBe(true) }) + + it('should force intake v1 when "support-intake-v2" disabled', () => { + const configuration = computeTransportConfiguration({ clientToken, intakeApiVersion: 2 }, buildEnv) + expect(configuration.isIntakeUrl('https://rum-http-intake.logs.datadoghq.com/v1/input/xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://browser-http-intake.logs.datadoghq.com/v1/input/xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://session-replay.browser-intake-datadoghq.com/v1/input/xxx')).toBe(true) + }) + }) + + describe('isIntakeUrl when "support-intake-v2" enabled', () => { + describe('when RUM or Logs', () => { + describe('on us1 and eu1', () => { + it('should detect classic domains intake v2', () => { + let configuration = computeTransportConfiguration({ clientToken, intakeApiVersion: 2 }, buildEnv, true) + expect(configuration.isIntakeUrl('https://rum-http-intake.logs.datadoghq.com/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://browser-http-intake.logs.datadoghq.com/api/v2/logs?xxx')).toBe(true) + + configuration = computeTransportConfiguration( + { clientToken, site: 'datadoghq.eu', intakeApiVersion: 2 }, + buildEnv, + true + ) + expect(configuration.isIntakeUrl('https://rum-http-intake.logs.datadoghq.eu/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://browser-http-intake.logs.datadoghq.eu/api/v2/logs?xxx')).toBe(true) + }) + + it('should detect alternate domains intake v2', () => { + let configuration = computeTransportConfiguration( + { clientToken, useAlternateIntakeDomains: true, intakeApiVersion: 2 }, + buildEnv, + true + ) + expect(configuration.isIntakeUrl('https://rum.browser-intake-datadoghq.com/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://logs.browser-intake-datadoghq.com/api/v2/logs?xxx')).toBe(true) + + configuration = computeTransportConfiguration( + { clientToken, site: 'datadoghq.eu', useAlternateIntakeDomains: true, intakeApiVersion: 2 }, + buildEnv, + true + ) + expect(configuration.isIntakeUrl('https://rum.browser-intake-datadoghq.eu/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://logs.browser-intake-datadoghq.eu/api/v2/logs?xxx')).toBe(true) + }) + }) + + describe('on us3 and gov', () => { + it('should detect alternate domains intake v2', () => { + let configuration = computeTransportConfiguration( + { clientToken, site: 'us3.datadoghq.com', intakeApiVersion: 2 }, + buildEnv, + true + ) + expect(configuration.isIntakeUrl('https://rum.browser-intake-us3-datadoghq.com/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://logs.browser-intake-us3-datadoghq.com/api/v2/logs?xxx')).toBe(true) + + configuration = computeTransportConfiguration( + { clientToken, site: 'ddog-gov.com', intakeApiVersion: 2 }, + buildEnv, + true + ) + expect(configuration.isIntakeUrl('https://rum.browser-intake-ddog-gov.com/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://rum-http-intake.logs.ddog-gov.com/api/v2/logs?xxx')).toBe(false) + }) + }) + + describe('on us5', () => { + it('should force alternate domains intake v2', () => { + const configuration = computeTransportConfiguration( + { clientToken, site: 'us5.datadoghq.com' }, + buildEnv, + true + ) + expect(configuration.isIntakeUrl('https://rum.browser-intake-us5-datadoghq.com/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://logs.browser-intake-us5-datadoghq.com/api/v2/logs?xxx')).toBe(true) + }) + }) + }) + + describe('when session-replay on all env', () => { + it('should force alternate domains intake v2', () => { + let configuration = computeTransportConfiguration({ clientToken }, buildEnv, true) + expect( + configuration.isIntakeUrl('https://session-replay.browser-intake-datadoghq.com/api/v2/session-replay?xxx') + ).toBe(true) + + configuration = computeTransportConfiguration({ clientToken, site: 'datadoghq.eu' }, buildEnv, true) + expect( + configuration.isIntakeUrl('https://session-replay.browser-intake-datadoghq.eu/api/v2/session-replay?xxx') + ).toBe(true) + + configuration = computeTransportConfiguration({ clientToken, site: 'us3.datadoghq.com' }, buildEnv, true) + expect( + configuration.isIntakeUrl('https://session-replay.browser-intake-us3-datadoghq.com/api/v2/session-replay?xxx') + ).toBe(true) + + configuration = computeTransportConfiguration({ clientToken, site: 'ddog-gov.com' }, buildEnv, true) + expect( + configuration.isIntakeUrl('https://session-replay.browser-intake-ddog-gov.com/api/v2/session-replay?xxx') + ).toBe(true) + + configuration = computeTransportConfiguration({ clientToken, site: 'us5.datadoghq.com' }, buildEnv, true) + expect( + configuration.isIntakeUrl('https://session-replay.browser-intake-us5-datadoghq.com/api/v2/session-replay?xxx') + ).toBe(true) + }) + }) + + it('should detect replica intake request with alternate intake domains version 2', () => { + const configuration = computeTransportConfiguration( + { clientToken, site: 'foo.com', replica: { clientToken } }, + { ...buildEnv, buildMode: BuildMode.STAGING }, + true + ) + expect(configuration.isIntakeUrl('https://rum.browser-intake-foo.com/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://logs.browser-intake-foo.com/api/v2/logs?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://session-replay.browser-intake-foo.com/api/v2/session-replay?xxx')).toBe( + true + ) + + expect(configuration.isIntakeUrl('https://rum.browser-intake-datadoghq.com/api/v2/rum?xxx')).toBe(true) + expect(configuration.isIntakeUrl('https://logs.browser-intake-datadoghq.com/api/v2/logs?xxx')).toBe(true) + expect( + configuration.isIntakeUrl('https://session-replay.browser-intake-datadoghq.com/api/v2/session-replay?xxx') + ).toBe(true) + }) }) }) diff --git a/packages/core/src/domain/transportConfiguration.ts b/packages/core/src/domain/transportConfiguration.ts index 9adeac627f..21be6d5500 100644 --- a/packages/core/src/domain/transportConfiguration.ts +++ b/packages/core/src/domain/transportConfiguration.ts @@ -1,156 +1,72 @@ import { BuildEnv, BuildMode } from '../boot/init' -import { includes } from '../tools/utils' -import { TransportConfiguration, InitConfiguration } from './configuration' - -const ENDPOINTS = { - alternate: { - logs: 'logs', - rum: 'rum', - sessionReplay: 'session-replay', - }, - classic: { - logs: 'browser', - rum: 'rum', - // session-replay has no classic endpoint - sessionReplay: undefined, - }, +import { InitConfiguration } from './configuration' +import { EndpointBuilder, ENDPOINTS_TYPES, INTAKE_SITE_US } from './endpointBuilder' + +export interface TransportConfiguration { + logsEndpoint: string + rumEndpoint: string + sessionReplayEndpoint: string + internalMonitoringEndpoint?: string + isIntakeUrl: (url: string) => boolean + + // only on staging build mode + replica?: ReplicaConfiguration } -const INTAKE_SITE_US = 'datadoghq.com' -const INTAKE_SITE_EU = 'datadoghq.eu' - -const CLASSIC_ALLOWED_SITES = [INTAKE_SITE_US, INTAKE_SITE_EU] - -type IntakeType = keyof typeof ENDPOINTS -type EndpointType = keyof typeof ENDPOINTS[IntakeType] - -interface TransportSettings { - clientToken: string - site: string - buildMode: BuildMode - sdkVersion: string - proxyHost?: string - - service?: string - env?: string - version?: string +export interface ReplicaConfiguration { + applicationId?: string + logsEndpoint: string + rumEndpoint: string + internalMonitoringEndpoint: string } export function computeTransportConfiguration( initConfiguration: InitConfiguration, - buildEnv: BuildEnv + buildEnv: BuildEnv, + isIntakeV2Enabled?: boolean ): TransportConfiguration { - const transportSettings: TransportSettings = { - buildMode: buildEnv.buildMode, - clientToken: initConfiguration.clientToken, - env: initConfiguration.env, - proxyHost: initConfiguration.proxyHost, - sdkVersion: buildEnv.sdkVersion, - service: initConfiguration.service, - site: initConfiguration.site || INTAKE_SITE_US, - version: initConfiguration.version, - } - - const intakeType: IntakeType = getIntakeType(transportSettings.site, initConfiguration) - const intakeUrls = getIntakeUrls(intakeType, transportSettings, initConfiguration.replica !== undefined) + const endpointBuilder = new EndpointBuilder(initConfiguration, buildEnv, isIntakeV2Enabled) + const intakeUrls: string[] = ENDPOINTS_TYPES.map((endpointType) => endpointBuilder.buildIntakeUrl(endpointType)) const configuration: TransportConfiguration = { isIntakeUrl: (url: string) => intakeUrls.some((intakeUrl) => url.indexOf(intakeUrl) === 0), - logsEndpoint: getEndpoint(intakeType, 'logs', transportSettings), - rumEndpoint: getEndpoint(intakeType, 'rum', transportSettings), - sessionReplayEndpoint: getEndpoint(intakeType, 'sessionReplay', transportSettings), + logsEndpoint: endpointBuilder.build('logs'), + rumEndpoint: endpointBuilder.build('rum'), + sessionReplayEndpoint: endpointBuilder.build('sessionReplay'), } if (initConfiguration.internalMonitoringApiKey) { - configuration.internalMonitoringEndpoint = getEndpoint( - intakeType, - 'logs', - transportSettings, - 'browser-agent-internal-monitoring' - ) + configuration.internalMonitoringEndpoint = endpointBuilder.build('logs', 'browser-agent-internal-monitoring') } - if (transportSettings.buildMode === BuildMode.E2E_TEST) { + if (buildEnv.buildMode === BuildMode.E2E_TEST) { configuration.internalMonitoringEndpoint = '<<< E2E INTERNAL MONITORING ENDPOINT >>>' configuration.logsEndpoint = '<<< E2E LOGS ENDPOINT >>>' configuration.rumEndpoint = '<<< E2E RUM ENDPOINT >>>' configuration.sessionReplayEndpoint = '<<< E2E SESSION REPLAY ENDPOINT >>>' } - if (transportSettings.buildMode === BuildMode.STAGING) { - if (initConfiguration.replica !== undefined) { - const replicaTransportSettings = { - ...transportSettings, - applicationId: initConfiguration.replica.applicationId, - clientToken: initConfiguration.replica.clientToken, - site: INTAKE_SITE_US, - } - configuration.replica = { - applicationId: initConfiguration.replica.applicationId, - internalMonitoringEndpoint: getEndpoint( - intakeType, - 'logs', - replicaTransportSettings, - 'browser-agent-internal-monitoring' - ), - logsEndpoint: getEndpoint(intakeType, 'logs', replicaTransportSettings), - rumEndpoint: getEndpoint(intakeType, 'rum', replicaTransportSettings), - } + if (buildEnv.buildMode === BuildMode.STAGING && initConfiguration.replica !== undefined) { + const replicaConfiguration = { + ...initConfiguration, + site: INTAKE_SITE_US, + applicationId: initConfiguration.replica.applicationId, + clientToken: initConfiguration.replica.clientToken, + useAlternateIntakeDomains: endpointBuilder.supportAlternateDomain(), + intakeApiVersion: endpointBuilder.supportIntakeV2() ? 2 : (1 as 1 | 2), } - } - - return configuration -} - -function getIntakeType(site: string, initConfiguration: InitConfiguration) { - return !initConfiguration.useAlternateIntakeDomains && includes(CLASSIC_ALLOWED_SITES, site) ? 'classic' : 'alternate' -} + const replicaEndpointBuilder = new EndpointBuilder(replicaConfiguration, buildEnv, isIntakeV2Enabled) -function getIntakeUrls(intakeType: IntakeType, settings: TransportSettings, withReplica: boolean) { - if (settings.proxyHost) { - return [`https://${settings.proxyHost}/v1/input/`] - } - const sites = [settings.site] - if (settings.buildMode === BuildMode.STAGING && withReplica) { - sites.push(INTAKE_SITE_US) - } - const urls = [] - const endpointTypes = Object.keys(ENDPOINTS[intakeType]) as EndpointType[] - for (const site of sites) { - for (const endpointType of endpointTypes) { - urls.push(`https://${getHost(intakeType, endpointType, site)}/v1/input/`) + configuration.replica = { + applicationId: initConfiguration.replica.applicationId, + internalMonitoringEndpoint: replicaEndpointBuilder.build('logs', 'browser-agent-internal-monitoring'), + logsEndpoint: replicaEndpointBuilder.build('logs'), + rumEndpoint: replicaEndpointBuilder.build('rum'), } - } - return urls -} - -function getHost(intakeType: IntakeType, endpointType: EndpointType, site: string) { - return (intakeType === 'classic' && getClassicHost(endpointType, site)) || getAlternateHost(endpointType, site) -} -function getClassicHost(endpointType: EndpointType, site: string): string | undefined { - const endpoint = ENDPOINTS.classic[endpointType] - return endpoint && `${endpoint}-http-intake.logs.${site}` -} - -function getAlternateHost(endpointType: EndpointType, site: string): string { - const endpoint = ENDPOINTS.alternate[endpointType] - const domainParts = site.split('.') - const extension = domainParts.pop() - const suffix = `${domainParts.join('-')}.${extension!}` - return `${endpoint}.browser-intake-${suffix}` -} - -function getEndpoint(intakeType: IntakeType, endpointType: EndpointType, settings: TransportSettings, source?: string) { - const tags = - `sdk_version:${settings.sdkVersion}` + - `${settings.env ? `,env:${settings.env}` : ''}` + - `${settings.service ? `,service:${settings.service}` : ''}` + - `${settings.version ? `,version:${settings.version}` : ''}` - const datadogHost = getHost(intakeType, endpointType, settings.site) - const host = settings.proxyHost ? settings.proxyHost : datadogHost - const proxyParameter = settings.proxyHost ? `ddhost=${datadogHost}&` : '' - const parameters = `${proxyParameter}ddsource=${source || 'browser'}&ddtags=${encodeURIComponent(tags)}` + const replicaIntakeUrls = ENDPOINTS_TYPES.map((endpointType) => replicaEndpointBuilder.buildIntakeUrl(endpointType)) + replicaIntakeUrls.forEach((replicaIntakeUrl) => intakeUrls.push(replicaIntakeUrl)) + } - return `https://${host}/v1/input/${settings.clientToken}?${parameters}` + return configuration }