From 574425626cdee8bd2ff9ac0d09fa6d07b7c0b7dd Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 5 Feb 2025 16:36:49 +0100 Subject: [PATCH] New APIs to retrieve tracing headers and generate IDs * Removed generateUUID * Added generateTraceId() * Added generateSpanId() * Added getTracingHeaders() * Added injectTracingHeaders() * Added buildTracingHeadersInjector() --- example/src/screens/MainScreen.tsx | 24 +- packages/core/jest/mock.js | 10 +- packages/core/src/index.tsx | 13 +- packages/core/src/rum/DdRum.ts | 88 +++-- packages/core/src/rum/__tests__/DdRum.test.ts | 364 ++++++++++++++++-- .../__utils__/TracingHeadersUtils.ts | 146 +++++++ .../DatadogTracingIdentifier.ts | 49 +++ .../distributedTracingHeaders.ts | 23 +- .../requestProxy/XHRProxy/XHRProxy.ts | 4 +- packages/core/src/rum/types.ts | 76 +++- 10 files changed, 729 insertions(+), 68 deletions(-) create mode 100644 packages/core/src/rum/__tests__/__utils__/TracingHeadersUtils.ts create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier.ts diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index a2ae07ad2..1662d34d4 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -11,7 +11,7 @@ import { } from 'react-native'; import styles from './styles'; import { APPLICATION_KEY, API_KEY } from '../../src/ddCredentials'; -import { DdSdkReactNative, TrackingConsent } from '@datadog/mobile-react-native'; +import { DdRum, DdSdkReactNative, FormattedUUID, PropagatorType, TracingIdFormat, TracingIdType, TrackingConsent } from '@datadog/mobile-react-native'; import { getTrackingConsent, saveTrackingConsent } from '../utils'; import { ConsentModal } from '../components/consent'; @@ -109,6 +109,18 @@ export default class MainScreen extends Component { } this.setState({ trackingConsentModalVisible: visible }) } + + generateUUIDs() { + const spanUUID: FormattedUUID = DdRum.generateUUID(TracingIdType.span); + console.log("Span UUID for context propagation (DATADOG): " + spanUUID.forContextPropagation(PropagatorType.DATADOG)); + console.log("Span UUID for resource: " + spanUUID.forResource()); + console.log("Span UUID with custom representation (high decimal): " + spanUUID.toString(TracingIdFormat.highDecimal)); + + const traceUUID: FormattedUUID = DdRum.generateUUID(TracingIdType.trace); + console.log("Trace UUID for context propagation (DATADOG): " + traceUUID.forContextPropagation(PropagatorType.DATADOG)); + console.log("Trace UUID for resource: " + traceUUID.forResource()); + console.log("Trace UUID with custom representation (high decimal): " + traceUUID.toString(TracingIdFormat.highDecimal)); + } render() { return @@ -203,6 +215,16 @@ export default class MainScreen extends Component { Click me (error) + { + this.generateUUIDs(); // called on purpose to trigger an error + }} + > + + Click me (Generate UUIDs) + + } diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index fb0d7c462..42f7c475c 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -98,7 +98,6 @@ module.exports = { stopResource: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), - generateUUID: jest.fn().mockImplementation(() => 'fakeUUID'), addError: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), @@ -117,7 +116,14 @@ module.exports = { () => new Promise(resolve => resolve('test-session-id')) ), setTimeProvider: jest.fn().mockImplementation(() => {}), - timeProvider: jest.fn().mockReturnValue(undefined) + timeProvider: jest.fn().mockReturnValue(undefined), + getTracingHeaders: jest.fn().mockReturnValue([]), + injectTracingHeaders: jest.fn().mockImplementation(() => {}), + buildTracingHeadersInjector: jest.fn().mockReturnValue({ + inject: (url, injectHeaders) => {} + }), + generateTraceId: jest.fn().mockReturnValue('mock-trace-id'), + generateSpanId: jest.fn().mockReturnValue('mock-span-id') }, DatadogProvider: DatadogProviderMock diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 3fd3bd0f0..365d7255c 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -27,6 +27,7 @@ import { DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, DATADOG_GRAPH_QL_VARIABLES_HEADER } from './rum/instrumentation/resourceTracking/graphql/graphqlHeaders'; +import type { TracingHeadersInjector } from './rum/types'; import { RumActionType, ErrorSource, PropagatorType } from './rum/types'; import { DatadogProvider } from './sdk/DatadogProvider/DatadogProvider'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; @@ -34,7 +35,11 @@ import { DdTrace } from './trace/DdTrace'; import { DefaultTimeProvider } from './utils/time-provider/DefaultTimeProvider'; import { TimeProvider } from './utils/time-provider/TimeProvider'; import type { Timestamp } from './utils/time-provider/TimeProvider'; -import { TracingIdType } from './rum/instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; +import { + TracingIdType, + TracingIdFormat +} from './rum/instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; +import { DatadogTracingIdentifier } from './rum/instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; /* eslint-enable arca/import-ordering */ @@ -66,7 +71,9 @@ export { DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, DATADOG_GRAPH_QL_VARIABLES_HEADER, - TracingIdType + TracingIdType, + TracingIdFormat, + DatadogTracingIdentifier }; -export type { Timestamp }; +export type { Timestamp, TracingHeadersInjector }; diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 2eb62665f..9fc63dae8 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -17,22 +17,22 @@ import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider' import type { TimeProvider } from '../utils/time-provider/TimeProvider'; import { DdAttributes } from './DdAttributes'; -import type { ActionEventMapper } from './eventMappers/actionEventMapper'; import { generateActionEventMapper } from './eventMappers/actionEventMapper'; -import type { ErrorEventMapper } from './eventMappers/errorEventMapper'; +import type { ActionEventMapper } from './eventMappers/actionEventMapper'; import { generateErrorEventMapper } from './eventMappers/errorEventMapper'; -import type { ResourceEventMapper } from './eventMappers/resourceEventMapper'; +import type { ErrorEventMapper } from './eventMappers/errorEventMapper'; import { generateResourceEventMapper } from './eventMappers/resourceEventMapper'; -import { - TracingIdFormat, - TracingIdType, - TracingIdentifier -} from './instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; +import type { ResourceEventMapper } from './eventMappers/resourceEventMapper'; +import { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; +import { TracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; +import { getTracingHeaders } from './instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders'; import type { ErrorSource, DdRumType, RumActionType, - ResourceKind + ResourceKind, + FirstPartyHost, + TracingHeadersInjector } from './types'; const generateEmptyPromise = () => new Promise(resolve => resolve()); @@ -230,26 +230,6 @@ class DdRumWrapper implements DdRumType { ); }; - generateUUID = (type: TracingIdType): string => { - switch (type) { - case TracingIdType.trace: - return TracingIdentifier.createTraceId().toString( - TracingIdFormat.paddedHex - ); - case TracingIdType.span: - return TracingIdentifier.createSpanId().toString( - TracingIdFormat.decimal - ); - default: - console.warn( - `Unsupported tracing ID type '${type}' for generateUUID. Falling back to 64 bit Span ID.` - ); - return TracingIdentifier.createSpanId().toString( - TracingIdFormat.decimal - ); - } - }; - addError = ( message: string, source: ErrorSource, @@ -320,6 +300,56 @@ class DdRumWrapper implements DdRumType { return this.nativeRum.getCurrentSessionId(); } + getTracingHeaders = ( + url: string, + tracingSamplingRate: number, + firstPartyHosts: FirstPartyHost[] + ): { header: string; value: string }[] => { + return getTracingHeaders(url, tracingSamplingRate, firstPartyHosts); + }; + + injectTracingHeaders( + url: string, + tracingSamplingRate: number, + firstPartyHosts: FirstPartyHost[], + injectHeaders: (header: string, value: string) => void + ) { + getTracingHeaders(url, tracingSamplingRate, firstPartyHosts).forEach( + ({ header, value }) => { + injectHeaders(header, value); + } + ); + } + + buildTracingHeadersInjector( + tracingSamplingRate: number, + firstPartyHosts: FirstPartyHost[] + ): TracingHeadersInjector { + const _firstPartyHosts = [...firstPartyHosts]; + return { + inject: ( + url: string, + injectHeaders: (header: string, value: string) => void + ) => { + getTracingHeaders( + url, + tracingSamplingRate, + _firstPartyHosts + ).forEach(({ header, value }) => { + injectHeaders(header, value); + }); + } + }; + } + + generateTraceId(): DatadogTracingIdentifier { + return new DatadogTracingIdentifier(TracingIdentifier.createTraceId()); + } + + generateSpanId(): DatadogTracingIdentifier { + return new DatadogTracingIdentifier(TracingIdentifier.createSpanId()); + } + registerErrorEventMapper(errorEventMapper: ErrorEventMapper) { this.errorEventMapper = generateErrorEventMapper(errorEventMapper); } diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 3f824b5a7..a532c7061 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -5,7 +5,6 @@ * Copyright 2016-Present Datadog, Inc. */ -import BigInt from 'big-integer'; import { NativeModules } from 'react-native'; import { InternalLog } from '../../InternalLog'; @@ -13,14 +12,22 @@ import { SdkVerbosity } from '../../SdkVerbosity'; import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdSdk } from '../../sdk/DdSdk'; import { GlobalState } from '../../sdk/GlobalState/GlobalState'; +import { TracingHeadersInjector } from '../..'; import { DdRum } from '../DdRum'; import type { ActionEventMapper } from '../eventMappers/actionEventMapper'; import type { ErrorEventMapper } from '../eventMappers/errorEventMapper'; import type { ResourceEventMapper } from '../eventMappers/resourceEventMapper'; -import { TracingIdType } from '../instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; +import { DatadogTracingIdentifier } from '../instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; +import { + TracingIdFormat, + TracingIdType +} from '../instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; import { TracingIdentifierUtils } from '../instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; +import type { FirstPartyHost } from '../types'; import { ErrorSource, PropagatorType, RumActionType } from '../types'; +import { TracingHeadersUtils } from './__utils__/TracingHeadersUtils'; + jest.mock('../../utils/time-provider/DefaultTimeProvider', () => { return { DefaultTimeProvider: jest.fn().mockImplementation(() => { @@ -451,34 +458,345 @@ describe('DdRum', () => { }); }); - describe('DdRum.generateUUID', () => { - it('generates a valid trace id in paddedHex format', () => { - const uuid = DdRum.generateUUID(TracingIdType.trace); + describe('Tracing Headers APIs', () => { + describe('Types and Enums', () => { + it('exposes TracingIdFormat enum', () => { + expect(TracingIdFormat).toBeDefined(); + }); - expect(uuid).toBeDefined(); // Ensure the value is defined - expect(BigInt(uuid, 16).greater(BigInt(0))).toBe(true); // Ensure it's a valid positive number - expect(TracingIdentifierUtils.isWithin128Bits(uuid)).toBe(true); // Ensure the value is within 128 bits - expect(uuid).toMatch(/^[0-9a-f]{32}$/); // Ensure the value is in paddedHex format + it('exposes DatadogTracingIdentifier enum', () => { + expect(DatadogTracingIdentifier).toBeDefined(); + }); }); - it('generates a valid span id in decimal format', () => { - const uuid = DdRum.generateUUID(TracingIdType.span); + describe('DdRum.generateTraceId', () => { + it('generates 128-bit trace ID (100 iterations)', () => { + for (let i = 0; i < 100; i++) { + const traceId = DdRum.generateTraceId(); + expect(traceId).toBeDefined(); + expect( + TracingIdentifierUtils.isWithin128Bits( + traceId.toString(TracingIdFormat.decimal) + ) + ).toBe(true); + } + }); + }); - expect(uuid).toBeDefined(); // Ensure the value is defined - expect(BigInt(uuid).greater(BigInt(0))).toBe(true); // Ensure it's a valid positive number - expect(TracingIdentifierUtils.isWithin64Bits(uuid)).toBe(true); // Ensure the value is within 64 bits - expect(uuid).toMatch(/^[0-9]+$/); // Ensure the value contains only decimal digits + describe('DdRum.generateSpanId', () => { + it('generates 64-bit span ID (100 iterations)', () => { + for (let i = 0; i < 100; i++) { + const spanId = DdRum.generateSpanId(); + expect(spanId).toBeDefined(); + expect( + TracingIdentifierUtils.isWithin64Bits( + spanId.toString(TracingIdFormat.decimal) + ) + ).toBe(true); + } + }); }); - it('falls back to 64 bit span id when wrong tracingIdType is passed', () => { - const uuid = DdRum.generateUUID( - ('wrong' as unknown) as TracingIdType - ); + describe('DdRum.getTracingHeaders', () => { + it('returns tracing headers with DATADOG propagator and sampling rate (50% 0, 50% 100)', () => { + for (let i = 0; i < 100; i++) { + const url = 'https://www.example.com'; + const tracingSamplingRate = + Math.random() < 0.5 ? 0 : 100; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [PropagatorType.DATADOG] + } + ]; + + const headers = DdRum.getTracingHeaders( + url, + tracingSamplingRate, + firstPartyHosts + ); + + expect(headers).toHaveLength(5); + TracingHeadersUtils.verifyDatadogHeaders( + headers, + tracingSamplingRate === 100 + ); + } + }); - expect(uuid).toBeDefined(); // Ensure the value is defined - expect(BigInt(uuid).greater(BigInt(0))).toBe(true); // Ensure it's a valid positive number - expect(TracingIdentifierUtils.isWithin64Bits(uuid)).toBe(true); // Ensure the value is within 64 bits - expect(uuid).toMatch(/^[0-9]+$/); // Ensure the value contains only decimal digits + it('returns tracing headers with TRACECONTEXT propagator and sampling rate (50% 0, 50% 100)', () => { + for (let i = 0; i < 100; i++) { + const url = 'https://www.example.com'; + const tracingSamplingRate = + Math.random() < 0.5 ? 0 : 100; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [PropagatorType.TRACECONTEXT] + } + ]; + + const headers = DdRum.getTracingHeaders( + url, + tracingSamplingRate, + firstPartyHosts + ); + + expect(headers).toHaveLength(2); + TracingHeadersUtils.verifyTraceContextHeaders( + headers, + tracingSamplingRate === 100 + ); + } + }); + + it('returns tracing headers with B3 propagator and sampling rate (50% 0, 50% 100)', () => { + for (let i = 0; i < 100; i++) { + const url = 'https://www.example.com'; + const tracingSamplingRate = + Math.random() < 0.5 ? 0 : 100; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [PropagatorType.B3] + } + ]; + + const headers = DdRum.getTracingHeaders( + url, + tracingSamplingRate, + firstPartyHosts + ); + + expect(headers).toHaveLength(1); + TracingHeadersUtils.verifyB3Headers( + headers, + tracingSamplingRate === 100 + ); + } + }); + + it('returns tracing headers with B3MULTI propagator and sampling rate (50% 0, 50% 100)', () => { + for (let i = 0; i < 100; i++) { + const url = 'https://www.example.com'; + const tracingSamplingRate = + Math.random() < 0.5 ? 0 : 100; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [PropagatorType.B3MULTI] + } + ]; + + const headers = DdRum.getTracingHeaders( + url, + tracingSamplingRate, + firstPartyHosts + ); + + expect(headers).toHaveLength(3); + TracingHeadersUtils.verifyB3MultiHeaders( + headers, + tracingSamplingRate === 100 + ); + } + }); + + it('returns tracing headers with all propagators and sampling rate (50% 0, 50% 100)', () => { + for (let i = 0; i < 100; i++) { + const url = 'https://www.example.com'; + const tracingSamplingRate = + Math.random() < 0.5 ? 0 : 100; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [ + PropagatorType.DATADOG, + PropagatorType.TRACECONTEXT, + PropagatorType.B3, + PropagatorType.B3MULTI + ] + } + ]; + + const headers = DdRum.getTracingHeaders( + url, + tracingSamplingRate, + firstPartyHosts + ); + + expect(headers).toHaveLength(11); + + TracingHeadersUtils.verifyDatadogHeaders( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyTraceContextHeaders( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyB3Headers( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyB3MultiHeaders( + headers, + tracingSamplingRate === 100 + ); + } + }); + + it('returns empty headers array for non-matching host with all propagators and sampling rate 100', () => { + const url = 'https://not-the-right-host.com'; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [ + PropagatorType.DATADOG, + PropagatorType.TRACECONTEXT, + PropagatorType.B3, + PropagatorType.B3MULTI + ] + } + ]; + + const headers = DdRum.getTracingHeaders( + url, + 100, + firstPartyHosts + ); + + expect(headers).toHaveLength(0); + }); + + it('returns empty headers with no propagators and sampling rate 100', () => { + const url = 'https://www.example.com'; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [] + } + ]; + + const headers = DdRum.getTracingHeaders( + url, + 100, + firstPartyHosts + ); + + expect(headers).toHaveLength(0); + }); + }); + + describe('DdRum.injectTracingHeaders', () => { + it('injects all headers with all propagators and sampling rate (50% 0, 50% 100)', () => { + for (let i = 0; i < 100; i++) { + const url = 'https://www.example.com'; + const tracingSamplingRate = + Math.random() < 0.5 ? 0 : 100; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [ + PropagatorType.DATADOG, + PropagatorType.TRACECONTEXT, + PropagatorType.B3, + PropagatorType.B3MULTI + ] + } + ]; + + const headers: { header: string; value: string }[] = []; + + DdRum.injectTracingHeaders( + url, + tracingSamplingRate, + firstPartyHosts, + (header, value) => { + headers.push({ header, value }); + } + ); + + expect(headers).toHaveLength(11); + + TracingHeadersUtils.verifyDatadogHeaders( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyTraceContextHeaders( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyB3Headers( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyB3MultiHeaders( + headers, + tracingSamplingRate === 100 + ); + } + }); + }); + + describe('DdRum.buildTracingHeadersInjector', () => { + it('built tracingHeadersInjector injects all headers with all propagators and sampling rate (50% 0, 50% 100)', () => { + for (let i = 0; i < 100; i++) { + const url = 'https://www.example.com'; + const tracingSamplingRate = + Math.random() < 0.5 ? 0 : 100; + const firstPartyHosts: FirstPartyHost[] = [ + { + match: 'example.com', + propagatorTypes: [ + PropagatorType.DATADOG, + PropagatorType.TRACECONTEXT, + PropagatorType.B3, + PropagatorType.B3MULTI + ] + } + ]; + + const headers: { header: string; value: string }[] = []; + + const injector = DdRum.buildTracingHeadersInjector( + tracingSamplingRate, + firstPartyHosts + ); + + injector.inject(url, (header, value) => { + headers.push({ header, value }); + }); + + expect(headers).toHaveLength(11); + + TracingHeadersUtils.verifyDatadogHeaders( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyTraceContextHeaders( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyB3Headers( + headers, + tracingSamplingRate === 100 + ); + + TracingHeadersUtils.verifyB3MultiHeaders( + headers, + tracingSamplingRate === 100 + ); + } + }); }); }); diff --git a/packages/core/src/rum/__tests__/__utils__/TracingHeadersUtils.ts b/packages/core/src/rum/__tests__/__utils__/TracingHeadersUtils.ts new file mode 100644 index 000000000..6dd421e07 --- /dev/null +++ b/packages/core/src/rum/__tests__/__utils__/TracingHeadersUtils.ts @@ -0,0 +1,146 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { TracingIdentifierUtils } from '../../instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; + +type Header = { header: string; value: string }; + +export class TracingHeadersUtils { + static verifyDatadogHeaders(headersArray: Header[], isSampled: boolean) { + const headers = this.getHeadersMapFromArray(headersArray); + + // x-datadog-origin + const originHeader = headers.get('x-datadog-origin') as string; + expect(originHeader).toBeDefined(); + expect(originHeader).toBe('rum'); + + // x-datadog-sampling-priority + const samplingPriorityHeader = headers.get( + 'x-datadog-sampling-priority' + ) as string; + expect(samplingPriorityHeader).toBeDefined(); + expect(samplingPriorityHeader).toBe(isSampled ? '1' : '0'); + + // x-datadog-trace-id + const traceIdHeader = headers.get('x-datadog-trace-id') as string; + expect(traceIdHeader).toBeDefined(); + expect(traceIdHeader).toMatch(/^(?:\d+|\d*\.\d+)$/); + expect(TracingIdentifierUtils.isWithin64Bits(traceIdHeader)).toBe(true); + + // x-datadog-parent-id + const parentIdHeader = headers.get('x-datadog-parent-id') as string; + expect(parentIdHeader).toBeDefined(); + expect(parentIdHeader).toMatch(/^(?:\d+|\d*\.\d+)$/); + expect(TracingIdentifierUtils.isWithin64Bits(parentIdHeader)).toBe( + true + ); + + // x-datadog-tags + const tagsHeader = headers.get('x-datadog-tags') as string; + expect(tagsHeader).toBeDefined(); + expect(tagsHeader).toMatch(/^_dd\.p\.tid=[0-9a-fA-F]{16}$/); + expect( + TracingIdentifierUtils.isWithin64Bits(tagsHeader.split('=')[1]) + ).toBe(true); + } + + static verifyTraceContextHeaders( + headersArray: Header[], + isSampled: boolean + ) { + const headers = this.getHeadersMapFromArray(headersArray); + + // traceparent (example: 00-67a9ce4b00000000566c8ec4106d55b4-b69743e07c0f3fb5-01) + const traceParentHeader = headers.get('traceparent') as string; + expect(traceParentHeader).toBeDefined(); + + const traceParentParts = traceParentHeader.split('-'); + expect(traceParentParts).toHaveLength(4); + + const version = traceParentParts[0]; + expect(version).toBe('00'); + + const traceId = traceParentParts[1]; + expect(traceId).toMatch(/^[0-9a-fA-F]{32}$/); + expect(TracingIdentifierUtils.isWithin128Bits(traceId, 16)).toBe(true); + + const parentId = traceParentParts[2]; + expect(parentId).toMatch(/^[0-9a-fA-F]{16}$/); + expect(TracingIdentifierUtils.isWithin64Bits(parentId, 16)).toBe(true); + + const flags = traceParentParts[3]; + expect(flags).toBe(isSampled ? '01' : '00'); + + // tracestate (example: dd=s:1;o:rum;p:b69743e07c0f3fb5) + const traceStateHeader = headers.get('tracestate') as string; + expect(traceStateHeader).toBeDefined(); + + const traceStateParts = traceStateHeader.split(';'); + expect(traceStateParts).toHaveLength(3); + + const datadogFlags = traceStateParts[0]; + expect(datadogFlags.split('=')[1]).toBe(`s:${isSampled ? 1 : 0}`); + + const origin = traceStateParts[1]; + expect(origin.split(':')[1]).toBe('rum'); + + const parent = traceStateParts[2]; + expect(parent.split(':')[1]).toMatch(/^[0-9a-fA-F]{16}$/); + expect( + TracingIdentifierUtils.isWithin64Bits(parent.split(':')[1], 16) + ).toBe(true); + } + + static verifyB3Headers(headersArray: Header[], isSampled: boolean) { + const headers = this.getHeadersMapFromArray(headersArray); + + // b3 (example: 67a9d1b800000000b83b034110cf109f-934634763188a363-1) + const b3Header = headers.get('b3') as string; + expect(b3Header).toBeDefined(); + + const parts = b3Header.split('-'); + expect(parts).toHaveLength(3); + + const traceId = parts[0]; + expect(traceId).toMatch(/^[0-9a-fA-F]{32}$/); + expect(TracingIdentifierUtils.isWithin128Bits(traceId, 16)).toBe(true); + + const parentId = parts[1]; + expect(parentId).toMatch(/^[0-9a-fA-F]{16}$/); + expect(TracingIdentifierUtils.isWithin64Bits(parentId, 16)).toBe(true); + + const samplingFlag = parts[2]; + expect(samplingFlag).toBe(isSampled ? '1' : '0'); + } + + static verifyB3MultiHeaders(headersArray: Header[], isSampled: boolean) { + const headers = this.getHeadersMapFromArray(headersArray); + + // X-B3-TraceId (example: 67a9d2cc00000000f1118fc009a8ea2f) + const traceIdHeader = headers.get('X-B3-TraceId') as string; + expect(traceIdHeader).toBeDefined(); + expect(traceIdHeader).toMatch(/^[0-9a-fA-F]{32}$/); + expect(TracingIdentifierUtils.isWithin128Bits(traceIdHeader, 16)); + + // X-B3-SpanId (example: 64765d396cb90802) + const spanIdHeader = headers.get('X-B3-SpanId') as string; + expect(spanIdHeader).toBeDefined(); + expect(spanIdHeader).toMatch(/^[0-9a-fA-F]{16}$/); + expect(TracingIdentifierUtils.isWithin64Bits(traceIdHeader, 16)); + + // X-B3-Sampled + const samplingHeader = headers.get('X-B3-Sampled') as string; + expect(samplingHeader).toBeDefined(); + expect(samplingHeader).toBe(isSampled ? '1' : '0'); + } + + private static getHeadersMapFromArray( + headers: Header[] + ): Map { + return new Map(headers.map(({ header, value }) => [header, value])); + } +} diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier.ts b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier.ts new file mode 100644 index 000000000..4bb0c83bd --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier.ts @@ -0,0 +1,49 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import type { BigInteger } from 'big-integer'; + +import type { + TraceId, + SpanId, + TracingIdentifier, + TracingIdType, + TracingIdFormat +} from './TracingIdentifier'; + +/** + * A read-only wrapper of {@link TracingIdentifier} for public API usage. + */ +export class DatadogTracingIdentifier { + /** + * Read-only generated ID as a {@link BigInteger}. + */ + get id(): BigInteger { + return this.uuid.id; + } + + /** + * Read-only type to determine whether the identifier is a {@link TraceId} or a {@link SpanId}. + */ + get type(): TracingIdType { + return this.uuid.type; + } + + private uuid: TracingIdentifier; + + public constructor(uuid: TraceId | SpanId) { + this.uuid = uuid; + } + + /** + * Returns a string representation of the Tracing ID. + * @param format - The type of representation to use. + * @returns The ID as a string in the specified representation type. + */ + toString(format: TracingIdFormat): string { + return this.uuid.toString(format); + } +} diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts index 0b0dea3dc..20244396c 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts @@ -4,11 +4,15 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { FirstPartyHost } from '../../../types'; import { PropagatorType } from '../../../types'; +import { URLHostParser } from '../requestProxy/XHRProxy/URLHostParser'; import { TracingIdFormat } from './TracingIdentifier'; import type { TraceId, SpanId } from './TracingIdentifier'; +import { getTracingAttributes } from './distributedTracing'; import type { DdRumResourceTracingAttributes } from './distributedTracing'; +import { firstPartyHostsRegexMapBuilder } from './firstPartyHosts'; export const SAMPLING_PRIORITY_HEADER_KEY = 'x-datadog-sampling-priority'; /** @@ -31,7 +35,7 @@ export const B3_MULTI_TRACE_ID_HEADER_KEY = 'X-B3-TraceId'; export const B3_MULTI_SPAN_ID_HEADER_KEY = 'X-B3-SpanId'; export const B3_MULTI_SAMPLED_HEADER_KEY = 'X-B3-Sampled'; -export const getTracingHeaders = ( +export const getTracingHeadersFromAttributes = ( tracingAttributes: DdRumResourceTracingAttributes ): { header: string; value: string }[] => { const headers: { header: string; value: string }[] = []; @@ -132,6 +136,23 @@ export const getTracingHeaders = ( return headers; }; +export const getTracingHeaders = ( + url: string, + tracingSamplingRate: number, + firstPartyHosts: FirstPartyHost[] +): { header: string; value: string }[] => { + const hostname = URLHostParser(url); + const firstPartyHostsRegexMap = firstPartyHostsRegexMapBuilder( + firstPartyHosts + ); + const tracingAttributes = getTracingAttributes({ + hostname, + firstPartyHostsRegexMap, + tracingSamplingRate + }); + return getTracingHeadersFromAttributes(tracingAttributes); +}; + const generateTraceContextHeader = ({ version, traceId, diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts index 2ee671920..9f826335e 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts @@ -5,7 +5,7 @@ */ import Timer from '../../../../../utils/Timer'; -import { getTracingHeaders } from '../../distributedTracing/distributedTracingHeaders'; +import { getTracingHeadersFromAttributes } from '../../distributedTracing/distributedTracingHeaders'; import type { DdRumResourceTracingAttributes } from '../../distributedTracing/distributedTracing'; import { getTracingAttributes } from '../../distributedTracing/distributedTracing'; import { @@ -125,7 +125,7 @@ const proxySend = (providers: XHRProxyProviders): void => { // keep track of start time this._datadog_xhr.timer.start(); - const tracingHeaders = getTracingHeaders( + const tracingHeaders = getTracingHeadersFromAttributes( this._datadog_xhr.tracingAttributes ); tracingHeaders.forEach(({ header, value }) => { diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index 275bf2641..bc160172d 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import type { TracingIdType } from './instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; +import type { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; /** * The entry point to use Datadog's RUM feature. @@ -123,12 +123,6 @@ export type DdRumType = { timestampMs?: number ): Promise; - /** - * Generate a new unique tracing ID. - * @param type - The type of the tracing ID to generate. Trace (128-bit) or Span (64-bit). - */ - generateUUID(type: TracingIdType): string; - /** * Add a RUM Error. * @param message: The error message. @@ -160,6 +154,67 @@ export type DdRumType = { * Returns current session ID, or undefined if unavailable. */ getCurrentSessionId(): Promise; + + /** + * Returns the Datadog tracing headers for manual instrumentation of your requests. + * @param url the request URL. + * @param tracingSamplingRate Percentage of tracing integrations for network calls between your app and your backend. Range `0`-`100`. + * @param firstPartyHosts List of your backends hosts to enable tracing with. + * @returns The generated tracing headers as a list of { header: string; value: string}. + */ + getTracingHeaders( + url: string, + tracingSamplingRate: number, + firstPartyHosts: FirstPartyHost[] + ): { header: string; value: string }[]; + + /** + * A function that can be used to manually retrieve the tracing headers for manual instrumentation of your requests. + * @param url the request URL. + * @param tracingSamplingRate Percentage of tracing integrations for network calls between your app and your backend. Range `0`-`100`. + * @param firstPartyHosts List of your backends hosts to enable tracing with. + * @param injectHeaders A callback function which returns the generated tracing headers for the given parameters. + */ + injectTracingHeaders( + url: string, + tracingSamplingRate: number, + firstPartyHosts: FirstPartyHost[], + injectHeaders: (key: string, value: string) => void + ): void; + + /** + * An alternative to {@link injectTracingHeaders}. + * It returns an object for the given `tracingSamplingRate` and `firstPartyHosts`, which can be used to call `.inject` with the + * request URL, and a callback to get the tracing headers. + * + * It's particularly useful to avoid specifying the `tracingSamplingRate` and `firstPartyHosts` for each call. + * + * Usage: + * + * const injector = this.getTracingHeadersInjector(tracingSamplingRate, firstPartyHosts); + * const headers = new Map(); + * injector.inject('myurl', (header: string, value: string) => { + * headers.set(header, value); + * }); + * + * @param tracingSamplingRate Percentage of tracing integrations for network calls between your app and your backend. Range `0`-`100`. + * @param firstPartyHosts List of your backends hosts to enable tracing with. + * @returns an object that can be used to manually inject Datadog tracing headers + */ + buildTracingHeadersInjector( + tracingSamplingRate: number, + firstPartyHosts: FirstPartyHost[] + ): TracingHeadersInjector; + + /** + * Generates a unique 128bit Trace ID. + */ + generateTraceId(): DatadogTracingIdentifier; + + /** + * Generates a unique 128bit Span ID. + */ + generateSpanId(): DatadogTracingIdentifier; }; /** @@ -217,3 +272,10 @@ export type FirstPartyHost = { match: string; propagatorTypes: PropagatorType[]; }; + +export type TracingHeadersInjector = { + inject: ( + url: string, + injectHeaders: (header: string, value: string) => void + ) => void; +};