From 85e844032e85fd1f808c0bc0e812c08cb397c4df Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 21 Jun 2024 14:38:32 +0200 Subject: [PATCH] 128 bit trace IDs --- .gitignore | 3 + package.json | 4 +- packages/core/src/DdSdkReactNative.tsx | 1 + .../distributedTracing/TracingIdentifier.tsx | 225 +++++++++++ .../__utils__/TracingIdentifierUtils.ts | 64 ++++ .../__tests__/distributedTracing.test.ts | 356 +++++++++++++++--- .../distributedTracing/distributedTracing.tsx | 63 +--- .../distributedTracingHeaders.ts | 52 ++- .../DatadogRumResource/ResourceReporter.ts | 9 +- .../XHRProxy/__tests__/XHRProxy.test.ts | 145 ++++++- yarn.lock | 8 +- 11 files changed, 791 insertions(+), 139 deletions(-) create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/TracingIdentifier.tsx create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils.ts diff --git a/.gitignore b/.gitignore index 3e159e30e..7c8f52bf9 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ packages/**/*.tgz !.yarn/releases !.yarn/sdks !.yarn/versions + +# Jest +coverage diff --git a/package.json b/package.json index 2047136de..ba36785a7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@react-native/metro-config": "0.73.5", "@react-native/typescript-config": "0.73.1", "@testing-library/react-native": "7.0.2", - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.0", "@types/react": "^18.2.6", "@types/react-native": "0.71.0", "@types/react-test-renderer": "18.0.0", @@ -52,7 +52,7 @@ "eslint-plugin-react-hooks": "4.3.0", "eslint-plugin-react-native": "3.10.0", "genversion": "3.0.2", - "jest": "^29.6.3", + "jest": "^29.7.0", "lerna": "7.1.0", "pod-install": "0.1.14", "prettier": "2.2.0", diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 110a6b1f8..20fd1eeca 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -47,6 +47,7 @@ export class DdSdkReactNative { private static readonly DD_SDK_VERSION = '_dd.sdk_version'; private static readonly DD_VERSION = '_dd.version'; private static readonly DD_VERSION_SUFFIX = '_dd.version_suffix'; + private static wasAutoInstrumented = false; private static features?: AutoInstrumentationConfiguration; private static _isInitialized = false; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/TracingIdentifier.tsx b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/TracingIdentifier.tsx new file mode 100644 index 000000000..7d179a6b5 --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/TracingIdentifier.tsx @@ -0,0 +1,225 @@ +/* + * 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. + */ + +/** + * Available formats for representing the {@link TracingIdentifier} as a string. + */ +export enum TracingIdFormat { + /** + * Decimal string representation of the full tracing id. + */ + decimal, + + /** + * The low bits of the tracing id as a decimal. + */ + lowDecimal, + + /** + * The high bits of the tracing id as a decimal. + */ + highDecimal, + + /** + * Hexadecimal string representation of the full tracing id. + */ + hex, + + /** + * Hexadecimal string representation of the low bits of the tracing id. + */ + lowHex, + + /** + * Hexadecimal string representation of the high bits of the tracing id. + */ + highHex, + + /** + * Padded hexadecimal string representation of the full tracing id. + */ + paddedHex, + + /** + * Padded hexadecimal string representation of the low bits of the tracing id. + */ + paddedLowHex, + + /** + * Padded hexadecimal string representation of the high bits of the tracing id. + */ + paddedHighHex +} + +/** + * A {@link TracingIdentifier} used for Traces (128 bit). + */ +export type TraceId = TracingIdentifier & { + _brand: 'traceId'; +}; + +/** + * A {@link TracingIdentifier} used for Spans (64 bit). + */ +export type SpanId = TracingIdentifier & { + _brand: 'spanId'; +}; + +/** + * The tracing identifier type. + */ +export enum TracingIdType { + /** + * 128-bit UUID. + */ + trace, + /** + * 64-bit UUID. + */ + span +} + +/** + * Value used to mask the low 64 bits of the trace identifier. + */ +const LOW_64BIT_MASK = (BigInt('0xffffffff') << 32n) + BigInt('0xffffffff'); + +/** + * Value used to mask the low 32 bits of the trace identifier. + */ +const LOW_32BIT_MASK = (BigInt('0xffff') << 16n) + BigInt('0xffff'); + +/** + * A {@link TracingIdentifier} is a unique UUID that can be 64bit or 128bit, and provides + * convenient methods to represent it as a HEX or DECIMAL string, and it allows the masking + * of its low or high bits. + * + * Create a new identifier by calling {@link TracingIdentifier.createTraceId()} or + * {@link TracingIdentifier.createSpanId()}. + */ +export class TracingIdentifier { + /** + * Read-only generated ID as a {@link bigint}. + */ + readonly id: bigint; + + /** + * Read-only type to determine whether the identifier is a {@link TraceId} or a {@link SpanId}. + */ + readonly type: TracingIdType; + + /** + * Creates a new unique Trace ID. + * @returns the generated {@link TraceId}. + */ + public static createTraceId(): TraceId { + return new TracingIdentifier(TracingIdType.trace) as TraceId; + } + + /** + * Creates a new unique Span ID. + * @returns the generated {@link SpanId}. + */ + public static createSpanId(): SpanId { + return new TracingIdentifier(TracingIdType.span) as SpanId; + } + + /** + * Private constructor to initialize the {@link TracingIdentifier} based on the given + * {@link TracingIdType}. + */ + private constructor(type: TracingIdType) { + this.id = this.generateUUID(type); + this.type = type; + } + + /** + * Generates a unique ID with the given format. + * @param format - the desired format (64bit or 128bit). + * @returns the generated UUID as a {@link bigint}. + */ + private generateUUID(type: TracingIdType): bigint { + // Get the current Unix timestamp in seconds + const unixSeconds = Math.floor(Date.now() / 1000); + + // Ensure the Unix timestamp is 32 bits + const unixSeconds32 = unixSeconds & 0xffffffff; + + // 32 bits of zero + const zeros32 = 0; + + // Generate 64 random bits using Math.random() + const random32Bit1 = Math.floor(Math.random() * 0xffffffff); + const random32Bit2 = Math.floor(Math.random() * 0xffffffff); + const random64Hex = + random32Bit1.toString(16).padStart(8, '0') + + random32Bit2.toString(16).padStart(8, '0'); + + // If type is 'span' we return the generated 64 bit ID + if (type === TracingIdType.span) { + return BigInt(`0x${random64Hex}`); + } + + // Convert parts to hexadecimal strings + const unixSecondsHex = unixSeconds32.toString(16).padStart(8, '0'); + const zerosHex = zeros32.toString(16).padStart(8, '0'); + + // Combine parts to form the 128-bit ID + const hex128BitID = unixSecondsHex + zerosHex + random64Hex; + + return BigInt(`0x${hex128BitID}`); + } + + /** + * 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. + */ + public toString(format: TracingIdFormat): string { + const lowTraceMask = + this.type === TracingIdType.trace ? LOW_64BIT_MASK : LOW_32BIT_MASK; + const highTraceMask = this.type === TracingIdType.trace ? 64n : 32n; + const padding = this.type === TracingIdType.trace ? 32 : 16; + + switch (format) { + case TracingIdFormat.decimal: + return this.id.toString(10); + + case TracingIdFormat.lowDecimal: + return (this.id & lowTraceMask).toString(10); + + case TracingIdFormat.highDecimal: + return (this.id >> highTraceMask).toString(10); + + case TracingIdFormat.hex: + return this.id.toString(16); + + case TracingIdFormat.lowHex: + return (this.id & lowTraceMask).toString(16); + + case TracingIdFormat.highHex: + return (this.id >> highTraceMask).toString(16); + + case TracingIdFormat.paddedHex: + return this.toString(TracingIdFormat.hex).padStart( + padding, + '0' + ); + + case TracingIdFormat.paddedLowHex: + return this.toString(TracingIdFormat.lowHex).padStart( + padding / 2, + '0' + ); + + case TracingIdFormat.paddedHighHex: + return this.toString(TracingIdFormat.highHex).padStart( + padding / 2, + '0' + ); + } + } +} diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils.ts b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils.ts new file mode 100644 index 000000000..fba328e23 --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils.ts @@ -0,0 +1,64 @@ +/* + * 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. + */ + +export default class TracingIdentifierUtils { + /** + * Extracts the Unix timestamp from the 128-bit hex string representation. + * @param idHex - The 128-bit ID as a hexadecimal string. + * @returns The Unix timestamp as a number. + */ + public static extractTimestamp(idHex: string): number { + // Extract the first 8 characters which represent the 32-bit Unix timestamp + const timestampHex = idHex.substring(0, 8); + + // Convert the hexadecimal string to a number + const timestamp = parseInt(timestampHex, 16); + + return timestamp; + } + + /** + * Checks if a string representation of an ID in a given radix is within 32 bits. + * @param idString - The string representation of the ID. + * @param radix - Optional base to use for the conversion (default is 10). + * @returns True if the ID is within 32 bits, otherwise false. + */ + public static isWithin32Bits( + idString: string, + radix: number = 10 + ): boolean { + const bigIntValue = BigInt(parseInt(idString, radix)); + return bigIntValue < BigInt(1) << BigInt(32); + } + + /** + * Checks if a string representation of an ID in a given radix is within 64 bits. + * @param idString - The string representation of the ID. + * @param radix - Optional base to use for the conversion (default is 10). + * @returns True if the ID is within 64 bits, otherwise false. + */ + public static isWithin64Bits( + idString: string, + radix: number = 10 + ): boolean { + const bigIntValue = BigInt(parseInt(idString, radix)); + return bigIntValue < BigInt(1) << BigInt(64); + } + + /** + * Checks if a string representation of an ID in a given radix is within 128 bits. + * @param idString - The string representation of the ID. + * @param radix - Optional base to use for the conversion (default is 10). + * @returns True if the ID is within 128 bits, otherwise false. + */ + public static isWithin128Bits( + idString: string, + radix: number = 10 + ): boolean { + const bigIntValue = BigInt(parseInt(idString, radix)); + return bigIntValue < BigInt(1) << BigInt(128); + } +} diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/__tests__/distributedTracing.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/__tests__/distributedTracing.test.ts index 3868d34e1..7234566ea 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/__tests__/distributedTracing.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/__tests__/distributedTracing.test.ts @@ -1,6 +1,14 @@ -import { TraceIdentifier } from '../distributedTracing'; +/* + * 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. + */ -describe('TraceIdentifier', () => { +import { TracingIdentifier, TracingIdFormat } from '../TracingIdentifier'; + +import TracingIdentifierUtils from './__utils__/tracingIdentifierUtils'; + +describe('TracingIdentifier', () => { it('M return an unique identifier W toString', async () => { // GIVEN const generatedIds = new Set(); @@ -9,63 +17,327 @@ describe('TraceIdentifier', () => { // WHEN while (counter-- > 0) { - generatedIds.add(new TraceIdentifier().toString(10)); + generatedIds.add( + TracingIdentifier.createTraceId().toString( + TracingIdFormat.decimal + ) + ); + generatedIds.add( + TracingIdentifier.createSpanId().toString( + TracingIdFormat.decimal + ) + ); } // THEN - expect(generatedIds.size).toBe(iterations); + expect(generatedIds.size).toBe(iterations * 2); }); - it('M return an 64 bits positive integer W toString', async () => { - let iterations = 100; - while (iterations-- > 0) { - // GIVEN - const id = new TraceIdentifier().toString(10); + describe('Trace IDs', () => { + it('M return a valid 128 bits HEX string W toString(.hex)', async () => { + let iterations = 100; + while (iterations-- > 0) { + // GIVEN + const id = TracingIdentifier.createTraceId(); + const idStr128 = id.toString(TracingIdFormat.hex); - // THEN - expect(id).toMatch(/[1-9]{1,19}/); - // should be less than the max 64 bits integer - if (id.length === 19) { - expect(id < '9223372036854775807').toBeTruthy(); + // THEN + expect(idStr128).toMatch(/^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/); + expect(idStr128.length).toBeLessThanOrEqual(32); + + expect( + TracingIdentifierUtils.isWithin128Bits(idStr128, 16) + ).toBe(true); } - } - }); + }); - it('M return an 64 bits positive hex W toString(16)', async () => { - let iterations = 100; - while (iterations-- > 0) { - // GIVEN - const trace = new TraceIdentifier(); - const id = trace.toString(16); - const paddedId = trace.toPaddedString(16, 16); + it('M return a valid 64 bits HEX string W toString(.lowHex)', () => { + let iterations = 100; + while (iterations-- > 0) { + const tracingId = TracingIdentifier.createTraceId(); + const idHex = tracingId.toString(TracingIdFormat.lowHex); - // THEN - expect(id).toMatch(/[1-9a-f]{1,16}/); - expect(paddedId).toMatch(/[0-9a-f]{16}/); - } - }); + expect(idHex).toMatch(/^[0-9a-f]{1,}$/); + expect(idHex.length).toBeLessThanOrEqual(16); + + expect(TracingIdentifierUtils.isWithin64Bits(idHex, 16)).toBe( + true + ); + } + }); + + it('M return a valid 64 bits HEX string W toString(.highHex)', () => { + let iterations = 100; + while (iterations-- > 0) { + const tracingId = TracingIdentifier.createTraceId(); + const idHex = tracingId.toString(TracingIdFormat.highHex); + + expect(idHex).toMatch(/^[0-9a-f]{1,}$/); + expect(idHex.length).toBeLessThanOrEqual(16); + + expect(TracingIdentifierUtils.isWithin64Bits(idHex, 16)).toBe( + true + ); + } + }); + + it('M return a valid 128 bits HEX 32 string W toString(.paddedHex)', async () => { + let iterations = 100; + while (iterations-- > 0) { + // GIVEN + const id = TracingIdentifier.createTraceId(); + const idStr128 = id.toString(TracingIdFormat.paddedHex); + + // THEN + expect(idStr128).toMatch(/^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/); + expect( + TracingIdentifierUtils.isWithin128Bits(idStr128, 16) + ).toBe(true); + } + }); + + it('M return a valid 64 bits HEX 16 string W paddedLowHex', () => { + let iterations = 100; + while (iterations-- > 0) { + const tracingId = TracingIdentifier.createTraceId(); + const idHex = tracingId.toString(TracingIdFormat.paddedLowHex); + + expect(idHex).toMatch(/^[0-9a-f]{16}$/); + expect(TracingIdentifierUtils.isWithin64Bits(idHex, 16)).toBe( + true + ); + } + }); + + it('M return a valid 64 bits HEX 16 string W paddedHighHex', () => { + const tracingId = TracingIdentifier.createTraceId(); + const idHex = tracingId.toString(TracingIdFormat.paddedHighHex); + + expect(idHex).toMatch(/^[0-9a-f]{8}[0]{8}$/); + expect(TracingIdentifierUtils.isWithin64Bits(idHex, 16)).toBe(true); + }); + + it('M return a valid 128 bits integer W toString(.decimal)', async () => { + let iterations = 100; + while (iterations-- > 0) { + // GIVEN + const id = TracingIdentifier.createTraceId(); + const idDecimal = id.toString(TracingIdFormat.decimal); + + // THEN + expect(TracingIdentifierUtils.isWithin128Bits(idDecimal)).toBe( + true + ); + } + }); + + it('M return a valid 64 bits low and high part integer W toString(.lowDecimal) & toString(.highDecimal)', async () => { + let iterations = 100; + while (iterations-- > 0) { + // GIVEN + const id = TracingIdentifier.createTraceId(); + const idStrLow64 = id.toString(TracingIdFormat.lowDecimal); + const idStrHigh64 = id.toString(TracingIdFormat.highDecimal); + + // THEN + expect(TracingIdentifierUtils.isWithin64Bits(idStrLow64)).toBe( + true + ); + expect(TracingIdentifierUtils.isWithin64Bits(idStrHigh64)).toBe( + true + ); + } + }); + + it('M return a valid timestamp in the high part of the 128 bits ID w toString(.paddedHex)', () => { + const tracingId = TracingIdentifier.createTraceId(); + const idHex = tracingId.toString(TracingIdFormat.paddedHex); + const timestamp = TracingIdentifierUtils.extractTimestamp(idHex); + + const currentUnixTime = Math.floor(Date.now() / 1000); + const fiveMinutesInSeconds = 5 * 60; + + expect(timestamp).toBeGreaterThan( + currentUnixTime - fiveMinutesInSeconds + ); + expect(timestamp).toBeLessThan( + currentUnixTime + fiveMinutesInSeconds + ); + }); - it('M return an 64 bits positive padded hex W toPaddedString(16, 32)', async () => { - let iterations = 100; - while (iterations-- > 0) { + it('M return valid string representations for zero ID w toString', () => { // GIVEN - const id = new TraceIdentifier().toPaddedString(16, 32); + const tracingId = TracingIdentifier.createTraceId(); + (tracingId as any)['id'] = 0n; // THEN - expect(id).not.toMatch(/[0]{32}/); - expect(id).toMatch(/[0]{16}[0-9a-f]{16}/); - } + + // Decimal + expect(tracingId.toString(TracingIdFormat.decimal)).toBe('0'); + expect(tracingId.toString(TracingIdFormat.lowDecimal)).toBe('0'); + expect(tracingId.toString(TracingIdFormat.highDecimal)).toBe('0'); + + // Hex + expect(tracingId.toString(TracingIdFormat.hex)).toBe('0'); + expect(tracingId.toString(TracingIdFormat.lowHex)).toBe('0'); + expect(tracingId.toString(TracingIdFormat.highHex)).toBe('0'); + + // Padded Hex + expect(tracingId.toString(TracingIdFormat.paddedHex)).toBe( + '00000000000000000000000000000000' + ); + expect(tracingId.toString(TracingIdFormat.paddedLowHex)).toBe( + '0000000000000000' + ); + expect(tracingId.toString(TracingIdFormat.paddedHighHex)).toBe( + '0000000000000000' + ); + }); }); - it('M return original string hex W toPaddedString(16, 10)', async () => { - let iterations = 100; - while (iterations-- > 0) { + describe('Span IDs', () => { + it('M return a valid 64 bits HEX string W toString(.hex)', async () => { + let iterations = 100; + while (iterations-- > 0) { + // GIVEN + const id = TracingIdentifier.createSpanId(); + const idStr64 = id.toString(TracingIdFormat.hex); + + // THEN + expect(idStr64).toMatch(/^[0-9a-f]{1,}$/); + expect(idStr64.length).toBeLessThanOrEqual(16); + + expect(TracingIdentifierUtils.isWithin64Bits(idStr64, 16)).toBe( + true + ); + } + }); + + it('M return a valid 32 bits HEX string W toString(.lowHex)', () => { + let iterations = 100; + while (iterations-- > 0) { + const tracingId = TracingIdentifier.createSpanId(); + const idHex = tracingId.toString(TracingIdFormat.lowHex); + + expect(idHex).toMatch(/^[0-9a-f]{1,}$/); + expect(idHex.length).toBeLessThanOrEqual(8); + + expect(TracingIdentifierUtils.isWithin32Bits(idHex, 16)).toBe( + true + ); + } + }); + + it('M return a valid 32 bits HEX string W toString(.highHex)', () => { + let iterations = 100; + while (iterations-- > 0) { + const tracingId = TracingIdentifier.createSpanId(); + const idHex = tracingId.toString(TracingIdFormat.highHex); + + expect(idHex).toMatch(/^[0-9a-f]{1,}$/); + expect(idHex.length).toBeLessThanOrEqual(8); + + expect(TracingIdentifierUtils.isWithin32Bits(idHex, 16)).toBe( + true + ); + } + }); + + it('M return a valid 64 bits HEX 16 string W toString(.paddedHex)', async () => { + let iterations = 100; + while (iterations-- > 0) { + // GIVEN + const id = TracingIdentifier.createSpanId(); + const idStr128 = id.toString(TracingIdFormat.paddedHex); + + // THEN + expect(idStr128).toMatch(/^[0-9a-f]{16}$/); + expect( + TracingIdentifierUtils.isWithin64Bits(idStr128, 16) + ).toBe(true); + } + }); + + it('M return a valid 64 bits HEX 8 string W paddedLowHex', () => { + let iterations = 100; + while (iterations-- > 0) { + const tracingId = TracingIdentifier.createSpanId(); + const idHex = tracingId.toString(TracingIdFormat.paddedLowHex); + + expect(idHex).toMatch(/^[0-9a-f]{8}$/); + expect(TracingIdentifierUtils.isWithin64Bits(idHex, 16)).toBe( + true + ); + } + }); + + it('M return a valid 64 bits HEX 8 string W paddedHighHex', () => { + const tracingId = TracingIdentifier.createSpanId(); + const idHex = tracingId.toString(TracingIdFormat.paddedHighHex); + + expect(idHex).toMatch(/^[0-9a-f]{8}$/); + expect(TracingIdentifierUtils.isWithin64Bits(idHex, 16)).toBe(true); + }); + + it('M return a valid 64 bits integer W toString(.decimal)', async () => { + let iterations = 100; + while (iterations-- > 0) { + // GIVEN + const id = TracingIdentifier.createSpanId(); + const idDecimal = id.toString(TracingIdFormat.decimal); + + // THEN + expect(TracingIdentifierUtils.isWithin64Bits(idDecimal)).toBe( + true + ); + } + }); + + it('M return a valid 32 bits low and high part integer W toString(.lowDecimal) & toString(.highDecimal)', async () => { + let iterations = 100; + while (iterations-- > 0) { + // GIVEN + const id = TracingIdentifier.createSpanId(); + const idStrLow32 = id.toString(TracingIdFormat.lowDecimal); + const idStrHigh32 = id.toString(TracingIdFormat.highDecimal); + + // THEN + expect(TracingIdentifierUtils.isWithin32Bits(idStrLow32)).toBe( + true + ); + expect(TracingIdentifierUtils.isWithin32Bits(idStrHigh32)).toBe( + true + ); + } + }); + + it('M return valid string representations for zero ID w toString', () => { // GIVEN - const id = new TraceIdentifier().toPaddedString(16, 10); + const tracingId = TracingIdentifier.createSpanId(); + (tracingId as any)['id'] = 0n; // THEN - expect(id).not.toMatch(/[0]{10}/); - expect(id).toMatch(/[0-9a-f]{10}/); - } + + // Decimal + expect(tracingId.toString(TracingIdFormat.decimal)).toBe('0'); + expect(tracingId.toString(TracingIdFormat.lowDecimal)).toBe('0'); + expect(tracingId.toString(TracingIdFormat.highDecimal)).toBe('0'); + + // Hex + expect(tracingId.toString(TracingIdFormat.hex)).toBe('0'); + expect(tracingId.toString(TracingIdFormat.lowHex)).toBe('0'); + expect(tracingId.toString(TracingIdFormat.highHex)).toBe('0'); + + // Padded Hex + expect(tracingId.toString(TracingIdFormat.paddedHex)).toBe( + '0000000000000000' + ); + expect(tracingId.toString(TracingIdFormat.paddedLowHex)).toBe( + '00000000' + ); + expect(tracingId.toString(TracingIdFormat.paddedHighHex)).toBe( + '00000000' + ); + }); }); }); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx index 579eb0c3e..7f5332560 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx @@ -7,6 +7,8 @@ import type { PropagatorType } from '../../../types'; import type { RegexMap } from '../requestProxy/interfaces/RequestProxy'; +import { TracingIdentifier } from './TracingIdentifier'; +import type { SpanId, TraceId } from './TracingIdentifier'; import type { Hostname } from './firstPartyHosts'; import { getPropagatorsForHost } from './firstPartyHosts'; @@ -62,8 +64,8 @@ const generateTracingAttributesWithSampling = ( ): DdRumResourceTracingAttributes => { const isSampled = Math.random() * 100 <= tracingSamplingRate; const tracingAttributes: DdRumResourceTracingAttributes = { - traceId: new TraceIdentifier() as TraceId, - spanId: new TraceIdentifier() as SpanId, + traceId: TracingIdentifier.createTraceId(), + spanId: TracingIdentifier.createSpanId(), samplingPriorityHeader: isSampled ? '1' : '0', tracingStrategy: 'KEEP', rulePsr: tracingSamplingRate / 100, @@ -72,60 +74,3 @@ const generateTracingAttributesWithSampling = ( return tracingAttributes; }; - -/** - * Using branded types will ensure we don't accidentally use - * traceId for spanId when generating headers. - */ -export type TraceId = TraceIdentifier & { - _brand: 'traceId'; -}; - -export type SpanId = TraceIdentifier & { - _brand: 'spanId'; -}; - -/* - * This code was inspired from browser-sdk at (https://github.com/DataDog/browser-sdk/blob/0e9722d5b06f6d49264bc82cd254a207d647d66c/packages/rum-core/src/domain/tracing/tracer.ts#L190) - */ -const MAX_32_BITS_NUMBER = 4294967295; // 2^32-1 -const MAX_31_BITS_NUMBER = 2147483647; // 2^31-1 -export class TraceIdentifier { - private low: number; - private high: number; - - constructor() { - // We need to have a 63 bits number max - this.high = Math.floor(Math.random() * MAX_31_BITS_NUMBER); - this.low = Math.floor(Math.random() * MAX_32_BITS_NUMBER); - } - - toString = (radix: number) => { - let low = this.low; - let high = this.high; - let str = ''; - - while (high > 0 || low > 0) { - const mod = (high % radix) * (MAX_32_BITS_NUMBER + 1) + low; - high = Math.floor(high / radix); - low = Math.floor(mod / radix); - str = (mod % radix).toString(radix) + str; - } - return str; - }; - - /** - * This function pads the trace with `0`. - * It should not be used with a `length` lower than the trace, as we return the full trace in this case. - * @param radix radix for the trace - * @param length minimum length - * @returns padded string - */ - toPaddedString = (radix: number, length: number) => { - const traceId = this.toString(radix); - if (traceId.length > length) { - return traceId; - } - return Array(length - traceId.length + 1).join('0') + traceId; - }; -} diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts index 32b409d2f..0b0dea3dc 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts @@ -6,11 +6,9 @@ import { PropagatorType } from '../../../types'; -import type { - DdRumResourceTracingAttributes, - SpanId, - TraceId -} from './distributedTracing'; +import { TracingIdFormat } from './TracingIdentifier'; +import type { TraceId, SpanId } from './TracingIdentifier'; +import type { DdRumResourceTracingAttributes } from './distributedTracing'; export const SAMPLING_PRIORITY_HEADER_KEY = 'x-datadog-sampling-priority'; /** @@ -20,6 +18,9 @@ export const ORIGIN_HEADER_KEY = 'x-datadog-origin'; export const ORIGIN_RUM = 'rum'; export const TRACE_ID_HEADER_KEY = 'x-datadog-trace-id'; export const PARENT_ID_HEADER_KEY = 'x-datadog-parent-id'; +export const TAGS_HEADER_KEY = 'x-datadog-tags'; +export const DD_TRACE_ID_TAG = '_dd.p.tid'; + /** * OTel headers */ @@ -51,11 +52,21 @@ export const getTracingHeaders = ( }, { header: TRACE_ID_HEADER_KEY, - value: tracingAttributes.traceId.toString(10) + value: tracingAttributes.traceId.toString( + TracingIdFormat.lowDecimal + ) }, { header: PARENT_ID_HEADER_KEY, - value: tracingAttributes.spanId.toString(10) + value: tracingAttributes.spanId.toString( + TracingIdFormat.decimal + ) + }, + { + header: TAGS_HEADER_KEY, + value: `${DD_TRACE_ID_TAG}=${tracingAttributes.traceId.toString( + TracingIdFormat.paddedHighHex + )}` } ); break; @@ -99,11 +110,15 @@ export const getTracingHeaders = ( headers.push( { header: B3_MULTI_TRACE_ID_HEADER_KEY, - value: tracingAttributes.traceId.toPaddedString(16, 32) + value: tracingAttributes.traceId.toString( + TracingIdFormat.paddedHex + ) }, { header: B3_MULTI_SPAN_ID_HEADER_KEY, - value: tracingAttributes.spanId.toPaddedString(16, 16) + value: tracingAttributes.spanId.toString( + TracingIdFormat.paddedHex + ) }, { header: B3_MULTI_SAMPLED_HEADER_KEY, @@ -129,10 +144,9 @@ const generateTraceContextHeader = ({ isSampled: boolean; }) => { const flags = isSampled ? '01' : '00'; - return `${version}-${traceId.toPaddedString( - 16, - 32 - )}-${parentId.toPaddedString(16, 16)}-${flags}`; + return `${version}-${traceId.toString( + TracingIdFormat.paddedHex + )}-${parentId.toString(TracingIdFormat.paddedHex)}-${flags}`; }; const generateTraceStateHeader = ({ @@ -144,7 +158,7 @@ const generateTraceStateHeader = ({ }) => { const sampled = `s:${isSampled ? '1' : '0'}`; const origin = 'o:rum'; - const parent = `p:${parentId.toPaddedString(16, 16)}`; + const parent = `p:${parentId.toString(TracingIdFormat.paddedHex)}`; return `dd=${sampled};${origin};${parent}`; }; @@ -158,9 +172,9 @@ const generateB3Header = ({ spanId: SpanId; isSampled: boolean; }) => { - const flags = isSampled ? '1' : '0'; - return `${traceId.toPaddedString(16, 32)}-${spanId.toPaddedString( - 16, - 16 - )}-${flags}`; + return [ + traceId.toString(TracingIdFormat.paddedHex), + spanId.toString(TracingIdFormat.paddedHex), + isSampled ? '1' : '0' + ].join('-'); }; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts index 84c9986de..f06a6258f 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts @@ -5,6 +5,7 @@ */ import { DdRum } from '../../../../../DdRum'; +import { TracingIdFormat } from '../../../distributedTracing/TracingIdentifier'; import type { RUMResource } from '../../interfaces/RumResource'; import { createTimings } from './resourceTiming'; @@ -37,8 +38,12 @@ const formatResourceStartContext = ( ): Record => { const attributes: Record = {}; if (tracingAttributes.samplingPriorityHeader !== '0') { - attributes['_dd.span_id'] = tracingAttributes.spanId.toString(10); - attributes['_dd.trace_id'] = tracingAttributes.traceId.toString(10); + attributes['_dd.span_id'] = tracingAttributes.spanId.toString( + TracingIdFormat.decimal + ); + attributes['_dd.trace_id'] = tracingAttributes.traceId.toString( + TracingIdFormat.paddedHex + ); attributes['_dd.rule_psr'] = tracingAttributes.rulePsr; } diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 4bbfa0de7..4bebdd217 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -12,6 +12,7 @@ import { BufferSingleton } from '../../../../../../sdk/DatadogProvider/Buffer/Bu import { DdRum } from '../../../../../DdRum'; import { PropagatorType } from '../../../../../types'; import { XMLHttpRequestMock } from '../../../__tests__/__utils__/XMLHttpRequestMock'; +import TracingIdentifierUtils from '../../../distributedTracing/__tests__/__utils__/tracingIdentifierUtils'; import { PARENT_ID_HEADER_KEY, TRACE_ID_HEADER_KEY, @@ -23,7 +24,8 @@ import { B3_MULTI_SAMPLED_HEADER_KEY, ORIGIN_RUM, ORIGIN_HEADER_KEY, - TRACESTATE_HEADER_KEY + TRACESTATE_HEADER_KEY, + TAGS_HEADER_KEY } from '../../../distributedTracing/distributedTracingHeaders'; import { firstPartyHostsRegexMapBuilder } from '../../../distributedTracing/firstPartyHosts'; import { @@ -450,7 +452,9 @@ describe('XHRProxy', () => { // THEN const contextHeader = xhr.requestHeaders[TRACECONTEXT_HEADER_KEY]; - expect(contextHeader).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/); + expect(contextHeader).toMatch( + /^00-[0-9a-f]{8}[0]{8}[0-9a-f]{16}-[0-9a-f]{16}-01$/ + ); // Parent value of the context header is the 3rd part of it const parentValue = contextHeader.split('-')[2]; @@ -458,6 +462,99 @@ describe('XHRProxy', () => { expect(stateHeader).toBe(`dd=s:1;o:rum;p:${parentValue}`); }); + it('adds correct trace IDs headers for all propagatorTypes', async () => { + // GIVEN + const method = 'GET'; + const url = 'https://api.example.com:443/v2/user'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([ + { + match: 'example.com', + propagatorTypes: [PropagatorType.DATADOG] + }, + { + match: 'example.com', + propagatorTypes: [PropagatorType.TRACECONTEXT] + }, + { + match: 'example.com', + propagatorTypes: [PropagatorType.B3] + }, + { + match: 'example.com', + propagatorTypes: [PropagatorType.B3MULTI] + } + ]) + }); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.send(); + xhr.notifyResponseArrived(); + xhr.complete(200, 'ok'); + await flushPromises(); + + // THEN + + /* ================================================================================= + * Verify that the trace id in the traceparent header is a 128 bit trace ID (hex). + * ================================================================================= */ + const traceparentHeader = + xhr.requestHeaders[TRACECONTEXT_HEADER_KEY]; + const traceparentTraceId = traceparentHeader.split('-')[1]; + + expect(traceparentTraceId).toMatch( + /^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/ + ); + expect( + TracingIdentifierUtils.isWithin128Bits(traceparentTraceId, 16) + ); + + /* ========================================================================= + * Verify that the trace id in the x-datadog-trace-id is a 64 bit decimal. + * ========================================================================= */ + + // x-datadog-trace-id is a decimal representing the low 64 bits of the 128 bits Trace ID + const xDatadogTraceId = xhr.requestHeaders[TRACE_ID_HEADER_KEY]; + + expect(TracingIdentifierUtils.isWithin64Bits(xDatadogTraceId)); + + /* =============================================================== + * Verify that the trace id in x-datadog-tags headers is HEX 16. + * =============================================================== */ + + // x-datadog-tags is a HEX 16 contains the high 64 bits of the 128 bits Trace ID + const xDatadogTagsTraceId = xhr.requestHeaders[ + TAGS_HEADER_KEY + ].split('=')[1]; + + expect(xDatadogTagsTraceId).toMatch(/^[a-f0-9]{16}$/); + expect( + TracingIdentifierUtils.isWithin64Bits(xDatadogTagsTraceId, 16) + ); + + /* ========================================================================= + * Verify that the trace id in the b3 header is a 128 bit trace ID (hex). + * ========================================================================= */ + + const b3Header = xhr.requestHeaders[B3_HEADER_KEY]; + const b3TraceId = b3Header.split('-')[0]; + + expect(b3TraceId).toMatch(/^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/); + expect(TracingIdentifierUtils.isWithin128Bits(b3TraceId, 16)); + + /* ================================================================================= + * Verify that the trace id in the X-B3-TraceId header is a 128 bit trace ID (hex). + * ================================================================================= */ + + const xB3TraceId = xhr.requestHeaders[B3_MULTI_TRACE_ID_HEADER_KEY]; + + expect(xB3TraceId).toMatch(/^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/); + expect(TracingIdentifierUtils.isWithin128Bits(xB3TraceId, 16)); + }); + it('adds tracing headers with matching value when all headers are added', async () => { // GIVEN const method = 'GET'; @@ -493,13 +590,33 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - const datadogTraceValue = xhr.requestHeaders[TRACE_ID_HEADER_KEY]; - const datadogParentValue = xhr.requestHeaders[PARENT_ID_HEADER_KEY]; + // x-datadog-trace-id is just the low 64 bits (DECIMAL) + const datadogLowTraceValue = + xhr.requestHeaders[TRACE_ID_HEADER_KEY]; + + // We convert the low 64 bits to HEX + const datadogLowTraceValueHex = `${BigInt(datadogLowTraceValue) + .toString(16) + .padStart(16, '0')}`; + + // The high 64 bits are expressed in x-datadog-tags (HEX) + const datadogHighTraceValueHex = xhr.requestHeaders[ + TAGS_HEADER_KEY + ].split('=')[1]; // High HEX 64 bits + + // We re-compose the full 128 bit trace-id by joining the strings + const datadogTraceValue128BitHex = `${datadogHighTraceValueHex}${datadogLowTraceValueHex}`; + + // We then get the decimal value of the trace-id + const datadogTraceValue128BitDec = hexToDecimal( + datadogTraceValue128BitHex + ); + + const datadogParentValue = xhr.requestHeaders[PARENT_ID_HEADER_KEY]; const contextHeader = xhr.requestHeaders[TRACECONTEXT_HEADER_KEY]; const traceContextValue = contextHeader.split('-')[1]; const parentContextValue = contextHeader.split('-')[2]; - const b3MultiTraceHeader = xhr.requestHeaders[B3_MULTI_TRACE_ID_HEADER_KEY]; const b3MultiParentHeader = @@ -509,13 +626,17 @@ describe('XHRProxy', () => { const traceB3Value = b3Header.split('-')[0]; const parentB3Value = b3Header.split('-')[1]; - expect(hexToDecimal(traceContextValue)).toBe(datadogTraceValue); + expect(hexToDecimal(traceContextValue)).toBe( + datadogTraceValue128BitDec + ); expect(hexToDecimal(parentContextValue)).toBe(datadogParentValue); - - expect(hexToDecimal(b3MultiTraceHeader)).toBe(datadogTraceValue); + // + expect(hexToDecimal(b3MultiTraceHeader)).toBe( + datadogTraceValue128BitDec + ); expect(hexToDecimal(b3MultiParentHeader)).toBe(datadogParentValue); - expect(hexToDecimal(traceB3Value)).toBe(datadogTraceValue); + expect(hexToDecimal(traceB3Value)).toBe(datadogTraceValue128BitDec); expect(hexToDecimal(parentB3Value)).toBe(datadogParentValue); }); @@ -549,7 +670,7 @@ describe('XHRProxy', () => { const traceId = xhr.requestHeaders[B3_MULTI_TRACE_ID_HEADER_KEY]; const spanId = xhr.requestHeaders[B3_MULTI_SPAN_ID_HEADER_KEY]; const sampled = xhr.requestHeaders[B3_MULTI_SAMPLED_HEADER_KEY]; - expect(traceId).toMatch(/^[0-9a-f]{32}$/); + expect(traceId).toMatch(/^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/); expect(spanId).toMatch(/^[0-9a-f]{16}$/); expect(sampled).toBe('1'); }); @@ -582,7 +703,9 @@ describe('XHRProxy', () => { // THEN const headerValue = xhr.requestHeaders[B3_HEADER_KEY]; - expect(headerValue).toMatch(/^[0-9a-f]{32}-[0-9a-f]{16}-1$/); + expect(headerValue).toMatch( + /^[0-9a-f]{8}[0]{8}[0-9a-f]{16}-[0-9a-f]{16}-1$/ + ); }); it('adds all headers when the host is matched for different propagators', async () => { diff --git a/yarn.lock b/yarn.lock index b6b70aa09..64159afab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5998,7 +5998,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.12": +"@types/jest@npm:^29.5.0": version: 29.5.12 resolution: "@types/jest@npm:29.5.12" dependencies: @@ -8549,7 +8549,7 @@ __metadata: "@react-native/metro-config": 0.73.5 "@react-native/typescript-config": 0.73.1 "@testing-library/react-native": 7.0.2 - "@types/jest": ^29.5.12 + "@types/jest": ^29.5.0 "@types/react": ^18.2.6 "@types/react-native": 0.71.0 "@types/react-test-renderer": 18.0.0 @@ -8565,7 +8565,7 @@ __metadata: eslint-plugin-react-hooks: 4.3.0 eslint-plugin-react-native: 3.10.0 genversion: 3.0.2 - jest: ^29.6.3 + jest: ^29.7.0 lerna: 7.1.0 pod-install: 0.1.14 prettier: 2.2.0 @@ -12634,7 +12634,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.6.3": +"jest@npm:^29.6.3, jest@npm:^29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: