diff --git a/packages/core/src/boot/init.ts b/packages/core/src/boot/init.ts index 260ded52c8..ae59d7fac7 100644 --- a/packages/core/src/boot/init.ts +++ b/packages/core/src/boot/init.ts @@ -1,7 +1,5 @@ -import { buildConfiguration, InitConfiguration } from '../domain/configuration' -import { setDebugMode, startInternalMonitoring } from '../domain/internalMonitoring' +import { setDebugMode } from '../domain/internalMonitoring' import { catchUserErrors } from '../tools/catchUserErrors' -import { updateExperimentalFeatures } from '../domain/configuration/experimentalFeatures' export function makePublicApi(stub: T): T & { onReady(callback: () => void): void } { const publicApi = { @@ -46,14 +44,3 @@ export interface BuildEnv { buildMode: BuildMode sdkVersion: string } - -export function commonInit(initConfiguration: InitConfiguration, buildEnv: BuildEnv) { - updateExperimentalFeatures(initConfiguration.enableExperimentalFeatures) - const configuration = buildConfiguration(initConfiguration, buildEnv) - const internalMonitoring = startInternalMonitoring(configuration) - - return { - configuration, - internalMonitoring, - } -} diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index 354114445e..da652e642b 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -1,9 +1,10 @@ import { RumEvent } from '../../../../rum-core/src/rumEvent.types' import { BuildEnv, BuildMode } from '../../boot/init' import { display } from '../../tools/display' -import { buildConfiguration } from './configuration' +import { buildConfiguration, InitConfiguration, validateAndBuildConfiguration } from './configuration' +import { isExperimentalFeatureEnabled, updateExperimentalFeatures } from './experimentalFeatures' -describe('configuration', () => { +describe('buildConfiguration', () => { const clientToken = 'some_client_token' const buildEnv: BuildEnv = { buildMode: BuildMode.RELEASE, @@ -61,3 +62,99 @@ describe('configuration', () => { }) }) }) + +describe('validateAndBuildConfiguration', () => { + const clientToken = 'some_client_token' + const buildEnv: BuildEnv = { + buildMode: BuildMode.RELEASE, + sdkVersion: 'some_version', + } + + beforeEach(() => { + updateExperimentalFeatures([]) + }) + + it('updates experimental feature flags', () => { + validateAndBuildConfiguration({ clientToken, enableExperimentalFeatures: ['foo'] }, buildEnv) + expect(isExperimentalFeatureEnabled('foo')).toBeTrue() + }) + + describe('validate init configuration', () => { + let displaySpy: jasmine.Spy + + beforeEach(() => { + displaySpy = spyOn(display, 'error') + }) + + it('requires the InitConfiguration to be defined', () => { + expect(validateAndBuildConfiguration((undefined as unknown) as InitConfiguration, buildEnv)).toBeUndefined() + expect(displaySpy).toHaveBeenCalledOnceWith('Client Token is not configured, we will not send any data.') + }) + + it('requires clientToken to be defined', () => { + expect(validateAndBuildConfiguration(({} as unknown) as InitConfiguration, buildEnv)).toBeUndefined() + expect(displaySpy).toHaveBeenCalledOnceWith('Client Token is not configured, we will not send any data.') + }) + + it('requires sampleRate to be a percentage', () => { + expect( + validateAndBuildConfiguration(({ clientToken, sampleRate: 'foo' } as unknown) as InitConfiguration, buildEnv) + ).toBeUndefined() + expect(displaySpy).toHaveBeenCalledOnceWith('Sample Rate should be a number between 0 and 100') + }) + }) + + describe('cookie options', () => { + it('should not be secure nor crossSite by default', () => { + const configuration = validateAndBuildConfiguration({ clientToken }, buildEnv)! + expect(configuration.cookieOptions).toEqual({ secure: false, crossSite: false }) + }) + + it('should be secure when `useSecureSessionCookie` is truthy', () => { + const configuration = validateAndBuildConfiguration({ clientToken, useSecureSessionCookie: true }, buildEnv)! + expect(configuration.cookieOptions).toEqual({ secure: true, crossSite: false }) + }) + + it('should be secure and crossSite when `useCrossSiteSessionCookie` is truthy', () => { + const configuration = validateAndBuildConfiguration({ clientToken, useCrossSiteSessionCookie: true }, buildEnv)! + expect(configuration.cookieOptions).toEqual({ secure: true, crossSite: true }) + }) + + it('should have domain when `trackSessionAcrossSubdomains` is truthy', () => { + const configuration = validateAndBuildConfiguration( + { clientToken, trackSessionAcrossSubdomains: true }, + buildEnv + )! + expect(configuration.cookieOptions).toEqual({ secure: false, crossSite: false, domain: jasmine.any(String) }) + }) + }) + + describe('beforeSend', () => { + it('should be undefined when beforeSend is missing on user configuration', () => { + const configuration = validateAndBuildConfiguration({ clientToken }, buildEnv)! + expect(configuration.beforeSend).toBeUndefined() + }) + + it('should return the same result as the original', () => { + const beforeSend = (event: RumEvent) => { + if (event.view.url === '/foo') { + return false + } + } + const configuration = validateAndBuildConfiguration({ clientToken, beforeSend }, buildEnv)! + expect(configuration.beforeSend!({ view: { url: '/foo' } }, {})).toBeFalse() + expect(configuration.beforeSend!({ view: { url: '/bar' } }, {})).toBeUndefined() + }) + + it('should catch errors and log them', () => { + const myError = 'Ooops!' + const beforeSend = () => { + throw myError + } + const configuration = validateAndBuildConfiguration({ clientToken, beforeSend }, buildEnv)! + const displaySpy = spyOn(display, 'error') + expect(configuration.beforeSend!(null, {})).toBeUndefined() + expect(displaySpy).toHaveBeenCalledWith('beforeSend threw an error:', myError) + }) + }) +}) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index c7596b1762..88a645f02a 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -1,7 +1,9 @@ import { BuildEnv } from '../../boot/init' import { CookieOptions, getCurrentSite } from '../../browser/cookie' import { catchUserErrors } from '../../tools/catchUserErrors' -import { objectHasValue, ONE_KILO_BYTE, ONE_SECOND } from '../../tools/utils' +import { display } from '../../tools/display' +import { isPercentage, objectHasValue, ONE_KILO_BYTE, ONE_SECOND } from '../../tools/utils' +import { updateExperimentalFeatures } from './experimentalFeatures' import { computeTransportConfiguration, TransportConfiguration } from './transportConfiguration' export const DefaultPrivacyLevel = { @@ -95,12 +97,44 @@ export type Configuration = typeof DEFAULT_CONFIGURATION & TransportConfiguration & { cookieOptions: CookieOptions - service?: string - beforeSend?: BeforeSendCallback + service: string | undefined + beforeSend: BeforeSendCallback | undefined actionNameAttribute?: string } +export function validateAndBuildConfiguration( + initConfiguration: InitConfiguration, + buildEnv: BuildEnv +): Configuration | undefined { + if (!initConfiguration || !initConfiguration.clientToken) { + display.error('Client Token is not configured, we will not send any data.') + return + } + + // Set the experimental feature flags as early as possible so we can use them in most places + updateExperimentalFeatures(initConfiguration.enableExperimentalFeatures) + + const configuration: Configuration = { + beforeSend: + initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), + cookieOptions: buildCookieOptions(initConfiguration), + service: initConfiguration.service, + ...computeTransportConfiguration(initConfiguration, buildEnv), + ...DEFAULT_CONFIGURATION, + } + + if (initConfiguration.sampleRate !== undefined) { + if (!isPercentage(initConfiguration.sampleRate)) { + display.error('Sample Rate should be a number between 0 and 100') + return + } + configuration.sampleRate = initConfiguration.sampleRate + } + + return configuration +} + export function buildConfiguration(initConfiguration: InitConfiguration, buildEnv: BuildEnv): Configuration { const configuration: Configuration = { beforeSend: diff --git a/packages/core/src/domain/configuration/index.ts b/packages/core/src/domain/configuration/index.ts index 3a3c448b47..70f16fadce 100644 --- a/packages/core/src/domain/configuration/index.ts +++ b/packages/core/src/domain/configuration/index.ts @@ -6,4 +6,5 @@ export { BeforeSendCallback, DefaultPrivacyLevel, buildConfiguration, + validateAndBuildConfiguration, } from './configuration' diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0912b998e2..a3779cc3ce 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,7 +2,9 @@ export { DEFAULT_CONFIGURATION, Configuration, InitConfiguration, + buildConfiguration, buildCookieOptions, + validateAndBuildConfiguration, BeforeSendCallback, DefaultPrivacyLevel, } from './domain/configuration' @@ -14,8 +16,9 @@ export { export { trackConsoleError } from './domain/error/trackConsoleError' export { trackRuntimeError } from './domain/error/trackRuntimeError' export { computeStackTrace, StackTrace } from './domain/tracekit' -export { BuildEnv, BuildMode, defineGlobal, makePublicApi, commonInit } from './boot/init' +export { BuildEnv, BuildMode, defineGlobal, makePublicApi } from './boot/init' export { + startInternalMonitoring, InternalMonitoring, MonitoringMessage, monitored, diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index a70cd55717..b00e1cc15e 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -1,7 +1,6 @@ import { areCookiesAuthorized, combine, - commonInit, Configuration, Context, createEventRateLimiter, @@ -15,6 +14,9 @@ import { canUseEventBridge, getEventBridge, getRelativeTime, + updateExperimentalFeatures, + buildConfiguration, + startInternalMonitoring, } from '@datadog/browser-core' import { trackNetworkError } from '../domain/trackNetworkError' import { Logger, LogsMessage, StatusType } from '../domain/logger' @@ -29,7 +31,10 @@ export interface LogsInitConfiguration extends InitConfiguration { } export function startLogs(initConfiguration: LogsInitConfiguration, errorLogger: Logger) { - const { configuration, internalMonitoring } = commonInit(initConfiguration, buildEnv) + updateExperimentalFeatures(initConfiguration.enableExperimentalFeatures) + const configuration = buildConfiguration(initConfiguration, buildEnv) + const internalMonitoring = startInternalMonitoring(configuration) + const errorObservable = new Observable() if (initConfiguration.forwardErrorsToLogs !== false) { diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 0491c9c0c5..952ccee9be 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -11,7 +11,6 @@ import { clocksNow, timeStampNow, display, - commonInit, Configuration, InternalMonitoring, callMonitored, @@ -21,6 +20,9 @@ import { RelativeTime, canUseEventBridge, areCookiesAuthorized, + updateExperimentalFeatures, + buildConfiguration, + startInternalMonitoring, } from '@datadog/browser-core' import { LifeCycle } from '../domain/lifeCycle' import { ParentContexts } from '../domain/parentContexts' @@ -123,7 +125,10 @@ export function makeRumPublicApi( return } - const { configuration, internalMonitoring } = commonInit(initConfiguration, buildEnv) + updateExperimentalFeatures(initConfiguration.enableExperimentalFeatures) + const configuration = buildConfiguration(initConfiguration, buildEnv) + const internalMonitoring = startInternalMonitoring(configuration) + if (!configuration.trackViewsManually) { doStartRum(initConfiguration, configuration, internalMonitoring) } else {