diff --git a/packages/core/src/tools/utils.ts b/packages/core/src/tools/utils.ts index b8c467c25d..dd0e349a17 100644 --- a/packages/core/src/tools/utils.ts +++ b/packages/core/src/tools/utils.ts @@ -662,6 +662,10 @@ export function removeDuplicates(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') { diff --git a/packages/rum-core/src/domain/configuration.spec.ts b/packages/rum-core/src/domain/configuration.spec.ts index 65500cffbf..3b86d979ef 100644 --- a/packages/rum-core/src/domain/configuration.spec.ts +++ b/packages/rum-core/src/domain/configuration.spec.ts @@ -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') }) }) diff --git a/packages/rum-core/src/domain/configuration.ts b/packages/rum-core/src/domain/configuration.ts index 8f3603d385..663f8403a6 100644 --- a/packages/rum-core/src/domain/configuration.ts +++ b/packages/rum-core/src/domain/configuration.ts @@ -1,5 +1,7 @@ import type { Configuration, InitConfiguration, MatchOption, RawTelemetryConfiguration } from '@datadog/browser-core' import { + getOrigin, + isMatchOption, serializeConfiguration, assign, DefaultPrivacyLevel, @@ -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 @@ -22,7 +25,11 @@ export interface RumInitConfiguration extends InitConfiguration { excludedActivityUrls?: MatchOption[] | undefined // tracing options + /** + * @deprecated use configureTracingUrls instead + */ allowedTracingOrigins?: MatchOption[] | undefined + configureTracingUrls?: Array | undefined tracingSampleRate?: number | undefined // replay options @@ -50,8 +57,8 @@ export type HybridInitConfiguration = Omit { + 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) @@ -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, diff --git a/packages/rum-core/src/domain/tracing/tracer.spec.ts b/packages/rum-core/src/domain/tracing/tracer.spec.ts index d42f305fa6..291533cf43 100644 --- a/packages/rum-core/src/domain/tracing/tracer.spec.ts +++ b/packages/rum-core/src/domain/tracing/tracer.spec.ts @@ -1,10 +1,10 @@ -import { isIE, objectEntries } from '@datadog/browser-core' +import { display, isIE, objectEntries } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../test/specHelper' import { setup } from '../../../test/specHelper' import type { RumSessionManagerMock } from '../../../test/mockRumSessionManager' import { createRumSessionManagerMock } from '../../../test/mockRumSessionManager' import type { RumFetchResolveContext, RumFetchStartContext, RumXhrStartContext } from '../requestCollection' -import type { RumConfiguration } from '../configuration' +import type { RumConfiguration, RumInitConfiguration } from '../configuration' import { validateAndBuildRumConfiguration } from '../configuration' import { startTracer, TraceIdentifier } from './tracer' @@ -19,13 +19,15 @@ describe('tracer', () => { let setupBuilder: TestSetupBuilder let sessionManager: RumSessionManagerMock + const baseConfiguration: RumInitConfiguration = { + clientToken: 'xxx', + applicationId: 'xxx', + service: 'service', + configureTracingUrls: [{ match: window.location.origin, headerTypes: ['dd'] }], + } + beforeEach(() => { - configuration = validateAndBuildRumConfiguration({ - clientToken: 'xxx', - applicationId: 'xxx', - allowedTracingOrigins: [window.location.origin], - service: 'service', - })! + configuration = validateAndBuildRumConfiguration(baseConfiguration)! setupBuilder = setup() sessionManager = createRumSessionManagerMock() }) @@ -105,14 +107,14 @@ describe('tracer', () => { }) it('should trace requests on configured origins', () => { - const configurationWithTracingUrls = { - ...configuration, - allowedTracingOrigins: [ + const configurationWithTracingUrls = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [ /^https?:\/\/qux\.com/, 'http://bar.com', (origin: string) => origin === 'http://dynamic.com', ], - } + })! const stub = xhrStub as unknown as XMLHttpRequest const tracer = startTracer(configurationWithTracingUrls, sessionManager) @@ -132,6 +134,90 @@ describe('tracer', () => { expect(context.traceId).toBeDefined() expect(context.spanId).toBeDefined() }) + + it('should add only B3 Multiple headers', () => { + const configurationWithB3Multi = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [{ match: window.location.origin, headerTypes: ['b3m'] }], + })! + + const tracer = startTracer(configurationWithB3Multi, sessionManager) + const context = { ...ALLOWED_DOMAIN_CONTEXT } + tracer.traceXhr(context, xhrStub as unknown as XMLHttpRequest) + + expect(xhrStub.headers['X-B3-TraceId']).toBeDefined() + expect(xhrStub.headers['X-B3-SpanId']).toBeDefined() + expect(xhrStub.headers['X-B3-Sampled']).toBeDefined() + + expect(xhrStub.headers['x-datadog-origin']).toBeUndefined() + expect(xhrStub.headers['x-datadog-parent-id']).toBeUndefined() + expect(xhrStub.headers['x-datadog-trace-id']).toBeUndefined() + expect(xhrStub.headers['x-datadog-sampling-priority']).toBeUndefined() + }) + + it('should add B3 (single) and W3C headers', () => { + const configurationWithB3andW3C = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [{ match: window.location.origin, headerTypes: ['b3', 'w3c'] }], + })! + + const tracer = startTracer(configurationWithB3andW3C, sessionManager) + const context = { ...ALLOWED_DOMAIN_CONTEXT } + tracer.traceXhr(context, xhrStub as unknown as XMLHttpRequest) + + expect(xhrStub.headers['b3']).toBeDefined() + expect(xhrStub.headers['traceparent']).toBeDefined() + }) + + it('should not add any headers', () => { + const configurationWithoutHeaders = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [{ match: window.location.origin, headerTypes: [] }], + })! + + const tracer = startTracer(configurationWithoutHeaders, sessionManager) + const context = { ...ALLOWED_DOMAIN_CONTEXT } + tracer.traceXhr(context, xhrStub as unknown as XMLHttpRequest) + + expect(xhrStub.headers['b3']).toBeUndefined() + expect(xhrStub.headers['traceparent']).toBeUndefined() + expect(xhrStub.headers['x-datadog-trace-id']).toBeUndefined() + expect(xhrStub.headers['X-B3-TraceId']).toBeUndefined() + }) + + it('should ignore wrong header types', () => { + const configurationWithBadParams = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [{ match: window.location.origin, headerTypes: ['foo', 32, () => true] as any }], + })! + + const tracer = startTracer(configurationWithBadParams, sessionManager) + const context = { ...ALLOWED_DOMAIN_CONTEXT } + tracer.traceXhr(context, xhrStub as unknown as XMLHttpRequest) + + expect(xhrStub.headers['b3']).toBeUndefined() + expect(xhrStub.headers['traceparent']).toBeUndefined() + expect(xhrStub.headers['x-datadog-trace-id']).toBeUndefined() + expect(xhrStub.headers['X-B3-TraceId']).toBeUndefined() + }) + + it('should survive a wrong match type', () => { + const displaySpy = spyOn(display, 'error') + const configurationWithBadParams = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [42 as any, undefined, { match: 42 as any, headerTypes: ['dd'] }], + })! + + const tracer = startTracer(configurationWithBadParams, sessionManager) + const context = { ...ALLOWED_DOMAIN_CONTEXT } + tracer.traceXhr(context, xhrStub as unknown as XMLHttpRequest) + + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(xhrStub.headers['b3']).toBeUndefined() + expect(xhrStub.headers['traceparent']).toBeUndefined() + expect(xhrStub.headers['x-datadog-trace-id']).toBeUndefined() + expect(xhrStub.headers['X-B3-TraceId']).toBeUndefined() + }) }) describe('traceFetch', () => { @@ -328,14 +414,14 @@ describe('tracer', () => { }) it('should trace requests on configured urls', () => { - const configurationWithTracingUrls = { - ...configuration, - allowedTracingOrigins: [ + const configurationWithTracingUrls = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [ /^https?:\/\/qux\.com.*/, 'http://bar.com', (origin: string) => origin === 'http://dynamic.com', ], - } + })! const quxDomainContext: Partial = { url: 'http://qux.com' } const barDomainContext: Partial = { url: 'http://bar.com' } const dynamicDomainContext: Partial = { url: 'http://dynamic.com' } @@ -352,6 +438,56 @@ describe('tracer', () => { expect(dynamicDomainContext.traceId).toBeDefined() expect(dynamicDomainContext.spanId).toBeDefined() }) + + it('should add only B3 Multiple headers', () => { + const configurationWithB3Multi = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [{ match: window.location.origin, headerTypes: ['b3m'] }], + })! + + const tracer = startTracer(configurationWithB3Multi, sessionManager) + const context: Partial = { ...ALLOWED_DOMAIN_CONTEXT } + tracer.traceFetch(context) + + expect(context.init!.headers).toContain(jasmine.arrayContaining(['X-B3-TraceId'])) + expect(context.init!.headers).toContain(jasmine.arrayContaining(['X-B3-SpanId'])) + expect(context.init!.headers).toContain(jasmine.arrayContaining(['X-B3-Sampled'])) + + expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-origin'])) + expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-parent-id'])) + expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-trace-id'])) + expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-sampling-priority'])) + }) + + it('should add B3 (single) and W3C headers', () => { + const configurationWithB3andW3C = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [{ match: window.location.origin, headerTypes: ['b3', 'w3c'] }], + })! + + const tracer = startTracer(configurationWithB3andW3C, sessionManager) + const context: Partial = { ...ALLOWED_DOMAIN_CONTEXT } + tracer.traceFetch(context) + + expect(context.init!.headers).toContain(jasmine.arrayContaining(['b3'])) + expect(context.init!.headers).toContain(jasmine.arrayContaining(['traceparent'])) + }) + + it('should not add any headers', () => { + const configurationWithoutHeaders = validateAndBuildRumConfiguration({ + ...baseConfiguration, + configureTracingUrls: [{ match: window.location.origin, headerTypes: [] }], + })! + + const tracer = startTracer(configurationWithoutHeaders, sessionManager) + const context: Partial = { ...ALLOWED_DOMAIN_CONTEXT } + tracer.traceFetch(context) + + expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['b3'])) + expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['traceparent'])) + expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-trace-id'])) + expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['X-B3-TraceId'])) + }) }) describe('clearTracingIfCancelled', () => { @@ -391,6 +527,13 @@ describe('TraceIdentifier', () => { expect(traceIdentifier.toDecimalString()).toMatch(/^\d+$/) }) + + it('should pad the string to 16 characters', () => { + const traceIdentifier: any = new TraceIdentifier() // Forcing as any to access private member: buffer + traceIdentifier.buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]) + + expect((traceIdentifier as TraceIdentifier).toPaddedHexadecimalString()).toEqual('0001020304050607') + }) }) function toPlainObject(headers: Headers) { diff --git a/packages/rum-core/src/domain/tracing/tracer.ts b/packages/rum-core/src/domain/tracing/tracer.ts index 5889b79f43..79a84fa6de 100644 --- a/packages/rum-core/src/domain/tracing/tracer.ts +++ b/packages/rum-core/src/domain/tracing/tracer.ts @@ -1,4 +1,4 @@ -import { getOrigin, matchList, objectEntries, shallowClone, performDraw, isNumber } from '@datadog/browser-core' +import { objectEntries, shallowClone, performDraw, isNumber, assign, display } from '@datadog/browser-core' import type { RumConfiguration } from '../configuration' import type { RumFetchResolveContext, @@ -7,6 +7,7 @@ import type { RumXhrStartContext, } from '../requestCollection' import type { RumSessionManager } from '../rumSessionManager' +import type { ConfigureTracingOption, TracingHeadersType } from './tracer.types' export interface Tracer { traceFetch: (context: Partial) => void @@ -87,18 +88,46 @@ function injectHeadersIfTracingAllowed( sessionManager: RumSessionManager, inject: (tracingHeaders: TracingHeaders) => void ) { - if (!isTracingSupported() || !isAllowedUrl(configuration, context.url!) || !sessionManager.findTrackedSession()) { + if (!isTracingSupported() || !sessionManager.findTrackedSession()) { + return + } + + const config = findConfigurationForUrl(configuration.configureTracingUrls, context.url!) + if (!config) { return } context.traceId = new TraceIdentifier() context.spanId = new TraceIdentifier() context.traceSampled = !isNumber(configuration.tracingSampleRate) || performDraw(configuration.tracingSampleRate) - inject(makeTracingHeaders(context.traceId, context.spanId, context.traceSampled)) + inject(makeTracingHeaders(context.traceId, context.spanId, context.traceSampled, config.headerTypes)) } -function isAllowedUrl(configuration: RumConfiguration, requestUrl: string) { - return matchList(configuration.allowedTracingOrigins, getOrigin(requestUrl)) +function findConfigurationForUrl(configurationTracingUrls: ConfigureTracingOption[], url: string) { + const checkMatch = (config: ConfigureTracingOption) => { + try { + const item = config.match + if (typeof item === 'function') { + return item(url) + } + if (item instanceof RegExp) { + return item.test(url) + } + // String.startsWith is not available on IE11 + return url.lastIndexOf(item, 0) === 0 + } catch (e) { + display.error(e) + return false + } + } + + // Array.find is not available on IE11 + for (const config of configurationTracingUrls) { + if (checkMatch(config)) { + return config + } + } + return undefined } export function isTracingSupported() { @@ -113,7 +142,27 @@ function getCrypto() { * When trace is not sampled, set priority to '0' instead of not adding the tracing headers * to prepare the implementation for sampling delegation. */ -function makeTracingHeaders(traceId: TraceIdentifier, spanId: TraceIdentifier, traceSampled: boolean): TracingHeaders { +function makeTracingHeaders( + traceId: TraceIdentifier, + spanId: TraceIdentifier, + traceSampled: boolean, + requestedHeaders: TracingHeadersType[] +): TracingHeaders { + return requestedHeaders.reduce( + (headers, request) => + assign( + headers, + makeTracingHeadersFor[request] ? makeTracingHeadersFor[request](traceId, spanId, traceSampled) : {} + ), + {} as TracingHeaders + ) +} + +function makeTracingHeadersForDD( + traceId: TraceIdentifier, + spanId: TraceIdentifier, + traceSampled: boolean +): TracingHeaders { return { 'x-datadog-origin': 'rum', 'x-datadog-parent-id': spanId.toDecimalString(), @@ -122,6 +171,58 @@ function makeTracingHeaders(traceId: TraceIdentifier, spanId: TraceIdentifier, t } } +/** + * https://www.w3.org/TR/trace-context/ + */ +function makeTracingHeadersForW3C( + traceId: TraceIdentifier, + spanId: TraceIdentifier, + traceSampled: boolean +): TracingHeaders { + return { + traceparent: `00-0000000000000000${traceId.toPaddedHexadecimalString()}-${spanId.toPaddedHexadecimalString()}-0${ + traceSampled ? '1' : '0' + }`, + } +} + +/** + * https://github.com/openzipkin/b3-propagation + */ +function makeTracingHeadersForB3( + traceId: TraceIdentifier, + spanId: TraceIdentifier, + traceSampled: boolean +): TracingHeaders { + return { + b3: `${traceId.toPaddedHexadecimalString()}-${spanId.toPaddedHexadecimalString()}-${traceSampled ? '1' : '0'}`, + } +} +function makeTracingHeadersForB3M( + traceId: TraceIdentifier, + spanId: TraceIdentifier, + traceSampled: boolean +): TracingHeaders { + return { + 'X-B3-TraceId': traceId.toPaddedHexadecimalString(), + 'X-B3-SpanId': spanId.toPaddedHexadecimalString(), + 'X-B3-Sampled': traceSampled ? '1' : '0', + } +} + +const makeTracingHeadersFor: { + [key in TracingHeadersType]: ( + traceId: TraceIdentifier, + spanId: TraceIdentifier, + traceSampled: boolean + ) => TracingHeaders +} = { + dd: makeTracingHeadersForDD, + w3c: makeTracingHeadersForW3C, + b3: makeTracingHeadersForB3, + b3m: makeTracingHeadersForB3M, +} as const + /* eslint-disable no-bitwise */ export class TraceIdentifier { private buffer: Uint8Array = new Uint8Array(8) @@ -153,6 +254,14 @@ export class TraceIdentifier { return this.toString(10) } + /** + * Format used by OTel headers + */ + toPaddedHexadecimalString() { + const traceId = this.toString(16) + return Array(17 - traceId.length).join('0') + traceId + } + private readInt32(offset: number) { return ( this.buffer[offset] * 16777216 + diff --git a/packages/rum-core/src/domain/tracing/tracer.types.ts b/packages/rum-core/src/domain/tracing/tracer.types.ts new file mode 100644 index 0000000000..447d26179a --- /dev/null +++ b/packages/rum-core/src/domain/tracing/tracer.types.ts @@ -0,0 +1,11 @@ +import type { MatchOption } from '@datadog/browser-core' + +/** + * dd: Datadog (x-datadog-*) + * w3c: Trace Context (traceparent) + * b3: B3 Single Header (b3) + * b3m: B3 Multi Headers (X-B3-*) + */ +export const availableTracingHeaders = ['dd', 'b3', 'b3m', 'w3c'] as const +export type TracingHeadersType = typeof availableTracingHeaders[number] +export type ConfigureTracingOption = { match: MatchOption; headerTypes: TracingHeadersType[] }