Skip to content

Commit

Permalink
⚗️ ✨ [RUM-6868] implement consistent probabilistic trace sampling (#…
Browse files Browse the repository at this point in the history
…3186)

* 🚚 [RUM-6868] move crypto in a separate module

* 🚚 [RUM-6868] move identifier logic in a separate module

* 🚚 [RUM-6868] simplify identifier implementation

This commit reduces the scope of identifiers to make alternative
identifier implementations easier.

* ✨ [RUM-6868] make trace identifiers 64 bits

To comply with other tracers identifiers and be able to properly
implement probabilistic sampling, let's make trace identifiers 64 bits.

This PR differentiate Span and Trace identifiers to make sure we use
each at the right place.

* ✅ [RUM-6868] consider identifiers byte ordering little endian

This is not strictly needed from a functionality point of view, but
helps implementing tests that work with both the current
UintArray-based identifier and the future BigInt-based implementation.
The implementation is also simpler.

Rational:

In our tests, we are mocking `getRandomValues` to edit the
random bytes of the identifier, then we make sure the identifier gets
formated as a string correctly.

Currently, we set the last byte to 0xff, and since we consider this byte
to be the least significant byte (big endian), naturally the output is ff.

But in the next commit we'll introduce an alternative implementation of
identifiers based on BigInt generated using a BigUInt64Array. In this
setup, bigints are usually encoded as little endian. So if we edit the
last byte to 0xff, we actually edit the most significant byte, and the
output would be ff00000000000.

To keep tests simple, let's use the same endianness for both
implementation, so editing the same byte results in the same trace id.

* ✨ [RUM-6868] implement trace ids via bigints

* ✨ [RUM-6868] implement consistent trace sampling

* 🚩 [RUM-6868] put consistent sampling behind an experimental flag

* 👌 add comment about ie11 support

---------

Co-authored-by: Thomas Lebeau <[email protected]>
  • Loading branch information
BenoitZugmeyer and thomas-lebeau authored Dec 11, 2024
1 parent f87c2fe commit 518c07a
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 123 deletions.
1 change: 1 addition & 0 deletions packages/core/src/tools/experimentalFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum ExperimentalFeature {
LONG_ANIMATION_FRAME = 'long_animation_frame',
ANONYMOUS_USER_TRACKING = 'anonymous_user_tracking',
ACTION_NAME_MASKING = 'action_name_masking',
CONSISTENT_TRACE_SAMPLING = 'consistent_trace_sampling',
}

const enabledExperimentalFeatures: Set<ExperimentalFeature> = new Set()
Expand Down
4 changes: 4 additions & 0 deletions packages/rum-core/src/browser/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function getCrypto() {
// TODO: remove msCrypto when IE11 support is dropped
return window.crypto || (window as any).msCrypto
}
7 changes: 4 additions & 3 deletions packages/rum-core/src/domain/requestCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { LifeCycle, LifeCycleEventType } from './lifeCycle'
import type { RequestCompleteEvent, RequestStartEvent } from './requestCollection'
import { trackFetch, trackXhr } from './requestCollection'
import type { Tracer } from './tracing/tracer'
import { clearTracingIfNeeded, createTraceIdentifier } from './tracing/tracer'
import { clearTracingIfNeeded } from './tracing/tracer'
import { createSpanIdentifier, createTraceIdentifier } from './tracing/identifier'

const DEFAULT_PAYLOAD = {} as Payload

Expand All @@ -34,7 +35,7 @@ describe('collect fetch', () => {
clearTracingIfNeeded,
traceFetch: (context) => {
context.traceId = createTraceIdentifier()
context.spanId = createTraceIdentifier()
context.spanId = createSpanIdentifier()
},
}
;({ stop: stopFetchTracking } = trackFetch(lifeCycle, tracerStub as Tracer))
Expand Down Expand Up @@ -197,7 +198,7 @@ describe('collect xhr', () => {
clearTracingIfNeeded,
traceXhr: (context) => {
context.traceId = createTraceIdentifier()
context.spanId = createTraceIdentifier()
context.spanId = createSpanIdentifier()
},
}
;({ stop: stopXhrTracking } = trackXhr(lifeCycle, configuration, tracerStub as Tracer))
Expand Down
7 changes: 4 additions & 3 deletions packages/rum-core/src/domain/requestCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import type { RumConfiguration } from './configuration'
import type { LifeCycle } from './lifeCycle'
import { LifeCycleEventType } from './lifeCycle'
import { isAllowedRequestUrl } from './resource/resourceUtils'
import type { TraceIdentifier, Tracer } from './tracing/tracer'
import type { Tracer } from './tracing/tracer'
import { startTracer } from './tracing/tracer'
import type { SpanIdentifier, TraceIdentifier } from './tracing/identifier'

export interface CustomContext {
requestIndex: number
spanId?: TraceIdentifier
spanId?: SpanIdentifier
traceId?: TraceIdentifier
traceSampled?: boolean
}
Expand All @@ -47,7 +48,7 @@ export interface RequestCompleteEvent {
responseType?: string
startClocks: ClocksState
duration: Duration
spanId?: TraceIdentifier
spanId?: SpanIdentifier
traceId?: TraceIdentifier
traceSampled?: boolean
xhr?: XMLHttpRequest
Expand Down
14 changes: 7 additions & 7 deletions packages/rum-core/src/domain/resource/resourceCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import { RumEventType } from '../../rawRumEvent.types'
import type { RawRumEventCollectedData } from '../lifeCycle'
import { LifeCycle, LifeCycleEventType } from '../lifeCycle'
import type { RequestCompleteEvent } from '../requestCollection'
import { createTraceIdentifier } from '../tracing/tracer'
import type { RumConfiguration } from '../configuration'
import { validateAndBuildRumConfiguration } from '../configuration'
import type { RumPerformanceEntry } from '../../browser/performanceObservable'
import { RumPerformanceEntryType } from '../../browser/performanceObservable'
import { createSpanIdentifier, createTraceIdentifier } from '../tracing/identifier'
import { startResourceCollection } from './resourceCollection'

const HANDLING_STACK_REGEX = /^Error: \n\s+at <anonymous> @/
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('resourceCollection', () => {
createCompletedRequest({
type: RequestType.XHR,
traceId: createTraceIdentifier(),
spanId: createTraceIdentifier(),
spanId: createSpanIdentifier(),
traceSampled: true,
})
)
Expand Down Expand Up @@ -316,7 +316,7 @@ describe('resourceCollection', () => {
LifeCycleEventType.REQUEST_COMPLETED,
createCompletedRequest({
traceSampled: true,
spanId: createTraceIdentifier(),
spanId: createSpanIdentifier(),
traceId: createTraceIdentifier(),
})
)
Expand All @@ -331,7 +331,7 @@ describe('resourceCollection', () => {
LifeCycleEventType.REQUEST_COMPLETED,
createCompletedRequest({
traceSampled: false,
spanId: createTraceIdentifier(),
spanId: createSpanIdentifier(),
traceId: createTraceIdentifier(),
})
)
Expand All @@ -352,7 +352,7 @@ describe('resourceCollection', () => {
LifeCycleEventType.REQUEST_COMPLETED,
createCompletedRequest({
traceSampled: true,
spanId: createTraceIdentifier(),
spanId: createSpanIdentifier(),
traceId: createTraceIdentifier(),
})
)
Expand All @@ -371,7 +371,7 @@ describe('resourceCollection', () => {
LifeCycleEventType.REQUEST_COMPLETED,
createCompletedRequest({
traceSampled: true,
spanId: createTraceIdentifier(),
spanId: createSpanIdentifier(),
traceId: createTraceIdentifier(),
})
)
Expand All @@ -391,7 +391,7 @@ describe('resourceCollection', () => {
LifeCycleEventType.REQUEST_COMPLETED,
createCompletedRequest({
traceSampled: true,
spanId: createTraceIdentifier(),
spanId: createSpanIdentifier(),
traceId: createTraceIdentifier(),
})
)
Expand Down
8 changes: 4 additions & 4 deletions packages/rum-core/src/domain/resource/resourceCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type { RawRumEventCollectedData, LifeCycle } from '../lifeCycle'
import type { RequestCompleteEvent } from '../requestCollection'
import type { PageStateHistory } from '../contexts/pageStateHistory'
import { PageState } from '../contexts/pageStateHistory'
import { createTraceIdentifier } from '../tracing/tracer'
import { createSpanIdentifier } from '../tracing/identifier'
import { matchRequestResourceEntry } from './matchRequestResourceEntry'
import {
computeResourceEntryDetails,
Expand Down Expand Up @@ -199,8 +199,8 @@ function computeRequestTracingInfo(request: RequestCompleteEvent, configuration:
}
return {
_dd: {
span_id: request.spanId!.toDecimalString(),
trace_id: request.traceId!.toDecimalString(),
span_id: request.spanId!.toString(),
trace_id: request.traceId!.toString(),
rule_psr: getRulePsr(configuration),
},
}
Expand All @@ -214,7 +214,7 @@ function computeResourceEntryTracingInfo(entry: RumPerformanceResourceTiming, co
return {
_dd: {
trace_id: entry.traceId,
span_id: createTraceIdentifier().toDecimalString(),
span_id: createSpanIdentifier().toString(),
rule_psr: getRulePsr(configuration),
},
}
Expand Down
90 changes: 90 additions & 0 deletions packages/rum-core/src/domain/tracing/identifier.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ExperimentalFeature } from '@datadog/browser-core'
import { mockExperimentalFeatures } from '../../../../core/test'
import { getCrypto } from '../../browser/crypto'
import { createSpanIdentifier, createTraceIdentifier, toPaddedHexadecimalString } from './identifier'

describe('identifier', () => {
describe('TraceIdentifier', () => {
it('generates a random id', () => {
const identifier = createTraceIdentifier()
expect(identifier.toString()).toMatch(/^\d+$/)
})

it('formats using base 16', () => {
mockRandomValues((buffer) => (buffer[0] = 0xff))
const identifier = createTraceIdentifier()
expect(identifier.toString(16)).toEqual('ff')
})

it('should generate a max value of 64 bits', () => {
mockRandomValues((buffer) => fill(buffer, 0xff))
const identifier = createTraceIdentifier()
expect(identifier.toString(16)).toEqual('ffffffffffffffff')
})
})

describe('SpanIdentifier', () => {
it('generates a max value of 63 bits', () => {
mockRandomValues((buffer) => fill(buffer, 0xff))
const identifier = createSpanIdentifier()
expect(identifier.toString(16)).toEqual('7fffffffffffffff')
})
})

// Run the same tests again with consistent trace sampling enabled, which uses the BigInt
// implementation
describe('with CONSISTENT_TRACE_SAMPLING enabled', () => {
beforeEach(() => {
mockExperimentalFeatures([ExperimentalFeature.CONSISTENT_TRACE_SAMPLING])
})

describe('TraceIdentifier', () => {
it('generates a random id', () => {
const identifier = createTraceIdentifier()
expect(identifier.toString()).toMatch(/^\d+$/)
})

it('formats using base 16', () => {
mockRandomValues((buffer) => (buffer[0] = 0xff))
const identifier = createTraceIdentifier()
expect(identifier.toString(16)).toEqual('ff')
})

it('should generate a max value of 64 bits', () => {
mockRandomValues((buffer) => fill(buffer, 0xff))
const identifier = createTraceIdentifier()
expect(identifier.toString(16)).toEqual('ffffffffffffffff')
})
})

describe('SpanIdentifier', () => {
it('generates a max value of 63 bits', () => {
mockRandomValues((buffer) => fill(buffer, 0xff))
const identifier = createSpanIdentifier()
expect(identifier.toString(16)).toEqual('7fffffffffffffff')
})
})
})
})

describe('toPaddedHexadecimalString', () => {
it('should pad the string to 16 characters', () => {
mockRandomValues((buffer) => (buffer[0] = 0x01))
const identifier = createTraceIdentifier()
expect(toPaddedHexadecimalString(identifier)).toEqual('0000000000000001')
})
})

function mockRandomValues(cb: (buffer: Uint8Array) => void) {
spyOn(getCrypto(), 'getRandomValues').and.callFake((bufferView) => {
cb(new Uint8Array(bufferView!.buffer))
return bufferView
})
}

// TODO: replace with `buffer.fill(value)` when we drop support for IE11
function fill(buffer: Uint8Array, value: number) {
for (let i = 0; i < buffer.length; i++) {
buffer[i] = value
}
}
86 changes: 86 additions & 0 deletions packages/rum-core/src/domain/tracing/identifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ExperimentalFeature, isExperimentalFeatureEnabled } from '@datadog/browser-core'
import { getCrypto } from '../../browser/crypto'

interface BaseIdentifier {
toString(radix?: number): string
}

export interface TraceIdentifier extends BaseIdentifier {
// We use a brand to distinguish between TraceIdentifier and SpanIdentifier, else TypeScript
// considers them as the same type
__brand: 'traceIdentifier'
}

export interface SpanIdentifier extends BaseIdentifier {
__brand: 'spanIdentifier'
}

export function createTraceIdentifier() {
return createIdentifier(64) as TraceIdentifier
}

export function createSpanIdentifier() {
return createIdentifier(63) as SpanIdentifier
}

let createIdentifierImplementationCache: ((bits: 63 | 64) => BaseIdentifier) | undefined

function createIdentifier(bits: 63 | 64): BaseIdentifier {
if (!createIdentifierImplementationCache) {
createIdentifierImplementationCache =
isExperimentalFeatureEnabled(ExperimentalFeature.CONSISTENT_TRACE_SAMPLING) && areBigIntIdentifiersSupported()
? createIdentifierUsingBigInt
: createIdentifierUsingUint32Array
}
return createIdentifierImplementationCache(bits)
}

export function areBigIntIdentifiersSupported() {
try {
crypto.getRandomValues(new BigUint64Array(1))
return true
} catch {
return false
}
}

function createIdentifierUsingBigInt(bits: 63 | 64): BaseIdentifier {
let id = crypto.getRandomValues(new BigUint64Array(1))[0]
if (bits === 63) {
// eslint-disable-next-line no-bitwise
id >>= BigInt('1')
}
return id
}

// TODO: remove this when all browser we support have BigInt support
function createIdentifierUsingUint32Array(bits: 63 | 64): BaseIdentifier {
const buffer = getCrypto().getRandomValues(new Uint32Array(2))
if (bits === 63) {
// eslint-disable-next-line no-bitwise
buffer[buffer.length - 1] >>>= 1 // force 63-bit
}

return {
toString(radix = 10) {
let high = buffer[1]
let low = buffer[0]
let str = ''

do {
const mod = (high % radix) * 4294967296 + low
high = Math.floor(high / radix)
low = Math.floor(mod / radix)
str = (mod % radix).toString(radix) + str
} while (high || low)

return str
},
}
}

export function toPaddedHexadecimalString(id: BaseIdentifier) {
const traceId = id.toString(16)
// TODO: replace with String.prototype.padStart when we drop IE11 support
return Array(17 - traceId.length).join('0') + traceId
}
Loading

0 comments on commit 518c07a

Please sign in to comment.