Skip to content

Commit

Permalink
✨ Open Telemetry header support for APM
Browse files Browse the repository at this point in the history
  • Loading branch information
yannickadam committed Dec 7, 2022
1 parent 22e641c commit 427e90a
Show file tree
Hide file tree
Showing 6 changed files with 481 additions and 47 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,10 @@ export function removeDuplicates<T>(array: T[]) {
}

export type MatchOption = string | RegExp | ((value: string) => boolean)
export function isMatchOption(item: unknown): item is MatchOption {
const itemType = getType(item)
return itemType === 'string' || itemType === 'function' || item instanceof RegExp
}
export function matchList(list: MatchOption[], value: string): boolean {
return list.some((item) => {
if (typeof item === 'function') {
Expand Down
84 changes: 72 additions & 12 deletions packages/rum-core/src/domain/configuration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,44 +166,104 @@ describe('validateAndBuildRumConfiguration', () => {
})

describe('allowedTracingOrigins', () => {
it('is set to provided value', () => {
expect(
validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedTracingOrigins: ['foo'],
service: 'bar',
})!.configureTracingUrls
).toEqual([{ match: 'foo', headerTypes: ['dd'] }])
})

it('accepts functions', () => {
const customOriginFunction = (origin: string): boolean => origin === 'https://my.origin.com'

const func = validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedTracingOrigins: [customOriginFunction],
service: 'bar',
})!.configureTracingUrls[0].match as (url: string) => boolean

expect(typeof func).toBe('function')
// Replicating behavior from allowedTracingOrigins, new function will treat the origin part of the URL
expect(func('https://my.origin.com/api')).toEqual(customOriginFunction('https://my.origin.com'))
})

it('does not validate the configuration if a value is provided and service is undefined', () => {
expect(
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, allowedTracingOrigins: ['foo'] })
).toBeUndefined()
expect(displayErrorSpy).toHaveBeenCalledOnceWith('Service needs to be configured when tracing is enabled')
})

it('does not validate the configuration if an incorrect value is provided', () => {
expect(
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, allowedTracingOrigins: 'foo' as any })
).toBeUndefined()
expect(displayErrorSpy).toHaveBeenCalledOnceWith('Allowed Tracing Origins should be an array')
})
})

describe('configureTracingUrls', () => {
it('defaults to an empty array', () => {
expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.allowedTracingOrigins).toEqual([])
expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.configureTracingUrls).toEqual([])
})

it('is set to provided value', () => {
expect(
validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedTracingOrigins: ['foo'],
configureTracingUrls: ['foo'],
service: 'bar',
})!.allowedTracingOrigins
).toEqual(['foo'])
})!.configureTracingUrls
).toEqual([{ match: 'foo', headerTypes: ['dd'] }])
})

it('accepts functions', () => {
const customOriginFunction = (origin: string): boolean => origin === 'foo'
const customOriginFunction = (url: string): boolean => url === 'https://my.origin.com'

expect(
validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
configureTracingUrls: [customOriginFunction],
service: 'bar',
})!.configureTracingUrls
).toEqual([{ match: customOriginFunction, headerTypes: ['dd'] }])
})

it('accepts RegExp', () => {
expect(
validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedTracingOrigins: [customOriginFunction],
configureTracingUrls: [/az/i],
service: 'bar',
})!.allowedTracingOrigins
).toEqual([customOriginFunction])
})!.configureTracingUrls
).toEqual([{ match: /az/i, headerTypes: ['dd'] }])
})

it('keeps headers', () => {
expect(
validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
configureTracingUrls: [{ match: 'simple', headerTypes: ['b3m', 'w3c'] }],
service: 'bar',
})!.configureTracingUrls
).toEqual([{ match: 'simple', headerTypes: ['b3m', 'w3c'] }])
})

it('does not validate the configuration if a value is provided and service is undefined', () => {
expect(
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, allowedTracingOrigins: ['foo'] })
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, configureTracingUrls: ['foo'] })
).toBeUndefined()
expect(displayErrorSpy).toHaveBeenCalledOnceWith('Service need to be configured when tracing is enabled')
expect(displayErrorSpy).toHaveBeenCalledOnceWith('Service needs to be configured when tracing is enabled')
})

it('does not validate the configuration if an incorrect value is provided', () => {
expect(
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, allowedTracingOrigins: 'foo' as any })
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, configureTracingUrls: 'foo' as any })
).toBeUndefined()
expect(displayErrorSpy).toHaveBeenCalledOnceWith('Allowed Tracing Origins should be an array')
expect(displayErrorSpy).toHaveBeenCalledOnceWith('Configure Tracing URLs should be an array')
})
})

Expand Down
133 changes: 120 additions & 13 deletions packages/rum-core/src/domain/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Configuration, InitConfiguration, MatchOption, RawTelemetryConfiguration } from '@datadog/browser-core'
import {
getOrigin,
isMatchOption,
serializeConfiguration,
assign,
DefaultPrivacyLevel,
Expand All @@ -10,6 +12,7 @@ import {
} from '@datadog/browser-core'
import type { RumEventDomainContext } from '../domainContext.types'
import type { RumEvent } from '../rumEvent.types'
import type { ConfigureTracingOption } from './tracing/tracer.types'

export interface RumInitConfiguration extends InitConfiguration {
// global options
Expand All @@ -22,7 +25,11 @@ export interface RumInitConfiguration extends InitConfiguration {
excludedActivityUrls?: MatchOption[] | undefined

// tracing options
/**
* @deprecated use configureTracingUrls instead
*/
allowedTracingOrigins?: MatchOption[] | undefined
configureTracingUrls?: Array<MatchOption | ConfigureTracingOption> | undefined
tracingSampleRate?: number | undefined

// replay options
Expand Down Expand Up @@ -50,8 +57,8 @@ export type HybridInitConfiguration = Omit<RumInitConfiguration, 'applicationId'
export interface RumConfiguration extends Configuration {
// Built from init configuration
actionNameAttribute: string | undefined
allowedTracingOrigins: MatchOption[]
tracingSampleRate: number | undefined
configureTracingUrls: ConfigureTracingOption[]
excludedActivityUrls: MatchOption[]
applicationId: string
defaultPrivacyLevel: DefaultPrivacyLevel
Expand Down Expand Up @@ -98,22 +105,16 @@ export function validateAndBuildRumConfiguration(
return
}

if (initConfiguration.allowedTracingOrigins !== undefined) {
if (!Array.isArray(initConfiguration.allowedTracingOrigins)) {
display.error('Allowed Tracing Origins should be an array')
return
}
if (initConfiguration.allowedTracingOrigins.length !== 0 && initConfiguration.service === undefined) {
display.error('Service need to be configured when tracing is enabled')
return
}
}

if (initConfiguration.excludedActivityUrls !== undefined && !Array.isArray(initConfiguration.excludedActivityUrls)) {
display.error('Excluded Activity Urls should be an array')
return
}

const configureTracingUrls = handleTracingParameters(initConfiguration)
if (configureTracingUrls === undefined) {
return
}

const baseConfiguration = validateAndBuildConfiguration(initConfiguration)
if (!baseConfiguration) {
return
Expand All @@ -128,8 +129,8 @@ export function validateAndBuildRumConfiguration(
actionNameAttribute: initConfiguration.actionNameAttribute,
sessionReplaySampleRate: initConfiguration.sessionReplaySampleRate ?? premiumSampleRate ?? 100,
oldPlansBehavior: initConfiguration.sessionReplaySampleRate === undefined,
allowedTracingOrigins: initConfiguration.allowedTracingOrigins ?? [],
tracingSampleRate: initConfiguration.tracingSampleRate,
configureTracingUrls,
excludedActivityUrls: initConfiguration.excludedActivityUrls ?? [],
trackInteractions: !!initConfiguration.trackInteractions || trackFrustrations,
trackFrustrations,
Expand All @@ -144,6 +145,109 @@ export function validateAndBuildRumConfiguration(
)
}

/**
* Handles configureTracingUrls and processes legacy allowedTracingOrigins
*/
function handleTracingParameters(initConfiguration: RumInitConfiguration): ConfigureTracingOption[] | undefined {
// Advise about parameters precedence.
if (initConfiguration.configureTracingUrls !== undefined && initConfiguration.allowedTracingOrigins !== undefined) {
display.warn(
'Both configureTracingUrls and allowedTracingOrigins (deprecated) have been defined. The parameter configureTracingUrls will override allowedTracingOrigins.'
)
}

// Handle configureTracingUrls first
if (initConfiguration.configureTracingUrls !== undefined) {
if (!Array.isArray(initConfiguration.configureTracingUrls)) {
display.error('Configure Tracing URLs should be an array')
return
}
if (initConfiguration.configureTracingUrls.length !== 0 && initConfiguration.service === undefined) {
display.error('Service needs to be configured when tracing is enabled')
return
}

// Convert from MatchOption | TracingOption to TracingOption
const configureTracingUrls: ConfigureTracingOption[] = []
for (const option of initConfiguration.configureTracingUrls) {
configureTracingUrls.push(
isMatchOption(option) ? ({ match: option, headerTypes: ['dd'] } as ConfigureTracingOption) : option
)
}
return configureTracingUrls
}

// Handle conversion of allowedTracingOrigins to configureTracingUrls
if (initConfiguration.allowedTracingOrigins !== undefined) {
if (!Array.isArray(initConfiguration.allowedTracingOrigins)) {
display.error('Allowed Tracing Origins should be an array')
return
}
if (initConfiguration.allowedTracingOrigins.length !== 0 && initConfiguration.service === undefined) {
display.error('Service needs to be configured when tracing is enabled')
return
}

return initConfiguration.allowedTracingOrigins.reduce((configArray, item) => {
const option = convertLegacyMatchOptionToTracingOption(item)
if (option) {
configArray.push(option)
}
return configArray
}, [] as ConfigureTracingOption[])
}

return []
}

/**
* Converts parameters from the deprecated allowedTracingOrigins
* to configureTracingUrls. Handles the change from origin to full URLs.
*/
function convertLegacyMatchOptionToTracingOption(item: MatchOption) {
let match: MatchOption | undefined
if (typeof item === 'string') {
match = item
}
if (item instanceof RegExp) {
match = (url) => item.test(getOrigin(url))
}
if (typeof item === 'function') {
match = (url) => item(getOrigin(url))
}

if (match === undefined) {
display.warn('Allowed Tracing Origins parameters should be a string, RegExp or function. Ignoring parameter', item)
return undefined
}

return { match, headerTypes: ['dd'] } as ConfigureTracingOption
}

/**
* Combines the selected tracing headers from the different options in configureTracingUrls,
* and assumes 'dd' has been selected when using allowedTracingOrigins
*/
function getSelectedTracingHeaders(configuration: RumInitConfiguration) {
const usedTracingHeaders = new Set()

if (Array.isArray(configuration.configureTracingUrls) && configuration.configureTracingUrls.length > 0) {
configuration.configureTracingUrls.forEach((config) => {
if (isMatchOption(config)) {
usedTracingHeaders.add('dd')
} else if (Array.isArray(config.headerTypes)) {
config.headerTypes.forEach((headerType) => usedTracingHeaders.add(headerType))
}
})
}

if (Array.isArray(configuration.allowedTracingOrigins) && configuration.allowedTracingOrigins.length > 0) {
usedTracingHeaders.add('dd')
}

return Array.from(usedTracingHeaders)
}

export function serializeRumConfiguration(configuration: RumInitConfiguration): RawTelemetryConfiguration {
const baseSerializedConfiguration = serializeConfiguration(configuration)

Expand All @@ -156,6 +260,9 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration):
action_name_attribute: configuration.actionNameAttribute,
use_allowed_tracing_origins:
Array.isArray(configuration.allowedTracingOrigins) && configuration.allowedTracingOrigins.length > 0,
use_configure_tracing_urls:
Array.isArray(configuration.configureTracingUrls) && configuration.configureTracingUrls.length > 0,
selected_tracing_headers: getSelectedTracingHeaders(configuration),
default_privacy_level: configuration.defaultPrivacyLevel,
use_excluded_activity_urls:
Array.isArray(configuration.allowedTracingOrigins) && configuration.allowedTracingOrigins.length > 0,
Expand Down
Loading

0 comments on commit 427e90a

Please sign in to comment.