Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
49eb625
First implementation for OTel sampling support
vpellan Sep 2, 2026
5785a9b
CODEOWNERS
vpellan Sep 2, 2026
9a7dfdd
fix(otel): generate sampling state for OTel propagation
vpellan Sep 3, 2026
af0fa51
fix(sampling): preserve committed decision metadata during checks
vpellan Sep 3, 2026
1186c76
fix(standalone): mark extracted force-keeps non-probability
vpellan Sep 3, 2026
30ab81c
fix(propagation): merge matching W3C tracestate into B3 context
vpellan Sep 3, 2026
42005a6
perf(sampling): defer OTel decision metadata until propagation
vpellan Sep 3, 2026
660cb44
Merge branch 'master' into vpellan/otel-threshold-random-value
vpellan Sep 3, 2026
46548fb
fix(otel): preserve standard parent tracestate
vpellan Sep 3, 2026
e8d4fd6
fix(sampling): refresh manual decision maker
vpellan Sep 3, 2026
9c18613
fix(propagation): clear conflicting OTel thresholds
vpellan Sep 3, 2026
f9ff0ad
refactor(sampling): use kebab-case filenames
vpellan Sep 3, 2026
8fa5940
fix(otel): ignore uncommitted sampling probe rates
vpellan Sep 3, 2026
9dc3f5d
fix(otel): preserve inherited tracestate member order
vpellan Sep 3, 2026
cf02963
Merge branch 'master' into vpellan/otel-threshold-random-value
vpellan Sep 4, 2026
6c0513a
fix(propagation): inherit W3C priority when selected style omits it
vpellan Sep 7, 2026
3e895ea
fix(sampling): ignore invalid priority tag values
vpellan Sep 7, 2026
59c8dd7
fix(sampling): avoid rescanning stale legacy tags
vpellan Sep 7, 2026
c0614ce
Merge branch 'master' into vpellan/otel-threshold-random-value
vpellan Sep 7, 2026
3447697
fix(sampling): preserve product force-keep decisions
vpellan Sep 8, 2026
abf2703
fix(propagation): clear threshold for B3 debug decisions
vpellan Sep 8, 2026
fa1dad8
perf(sampling): restore decision hot path
vpellan Sep 8, 2026
d7deb7d
refactor(sampling): expose automatic decision hook
vpellan Sep 8, 2026
d5c0f55
perf(sampling): prefer value checks for manual tags
vpellan Sep 8, 2026
1f726e5
Merge branch 'master' into vpellan/otel-threshold-random-value
vpellan Sep 8, 2026
57d5657
fix(propagation): clear stale decision maker on conflicts
vpellan Sep 8, 2026
5af8a45
fix(sampling): preserve product mechanism during probes
vpellan Sep 8, 2026
df787d4
fix(propagation): unify matching tracecontext state
vpellan Sep 8, 2026
c13b5f1
fix(standalone): preserve mechanism during probes
vpellan Sep 8, 2026
0af916c
fix(sampling): ignore inherited bulk sampling tags
vpellan Sep 8, 2026
6e6f7f0
test(propagation): preserve agreeing rule sampling state
vpellan Sep 8, 2026
a8937f5
test(sampling): codify allowed rate-limit semantics
vpellan Sep 8, 2026
3237de8
fix(propagation): clear stale state for B3 debug
vpellan Sep 8, 2026
ddf0605
fix(propagation): inherit W3C decision metadata
vpellan Sep 9, 2026
875ae2e
Merge branch 'master' into vpellan/otel-threshold-random-value
vpellan Sep 9, 2026
eef8dcd
perf(tracing): optimize bulk sampling tag checks
vpellan Sep 9, 2026
dd8c192
fix(propagation): merge implicit W3C state into winner
vpellan Sep 9, 2026
77ef7a5
fix(otel): preserve inherited sampling mechanism
vpellan Sep 9, 2026
e74e7cc
fix(otel): preserve inherited decision maker tag
vpellan Sep 9, 2026
b99215d
fix(propagation): inherit agreeing W3C decision metadata
vpellan Sep 9, 2026
8444a9a
fix(otel): map inherited Datadog tracestate fields
vpellan Sep 9, 2026
fade28a
fix(propagation): hydrate Datadog sampling mechanism
vpellan Sep 9, 2026
ca108b1
fix(propagation): preserve state after oversized member
vpellan Sep 9, 2026
754e47f
refactor(otel): keep unrelated tracestate fields opaque
vpellan Sep 9, 2026
2ea52be
refactor(otel): keep decision maker in tracestate
vpellan Sep 9, 2026
79fa291
refactor(propagation): keep agreeing metadata opaque
vpellan Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@
/packages/dd-trace/test/baggage.spec.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/src/carrier.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/test/carrier.spec.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/src/knuth-hash.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/src/otel-sampling.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/test/opentracing/propagation/otel-sampling.spec.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/src/*sampler.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/test/*sampler.spec.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/src/sampling_rule.js @DataDog/dd-trace-js @DataDog/apm-sdk-capabilities-js
Expand Down
18 changes: 18 additions & 0 deletions packages/dd-trace/src/knuth-hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'
Comment thread
vpellan marked this conversation as resolved.

const UINT64_MODULO = 2n ** 64n

// Knuth's factor for the sampling algorithm shared across Datadog tracers.
const SAMPLING_KNUTH_FACTOR = 1_111_111_111_111_111_111n

/**
* Hashes the lower 64 bits of a trace ID for deterministic trace sampling.
*
* @param {bigint} traceId
* @returns {bigint}
*/
function knuthHash (traceId) {
return (traceId * SAMPLING_KNUTH_FACTOR) % UINT64_MODULO
}

module.exports = knuthHash
8 changes: 6 additions & 2 deletions packages/dd-trace/src/opentelemetry/span_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

const api = require('@opentelemetry/api')
const { AUTO_KEEP } = require('../../../../ext/priority')
const { updateOtelTraceState } = require('../otel-sampling')
const DatadogSpanContext = require('../opentracing/span_context')
const TraceState = require('../opentracing/propagation/tracestate')
const id = require('../id')

function newContext () {
Expand Down Expand Up @@ -37,8 +39,10 @@ class SpanContext {
}

get traceState () {
const ts = this._ddContext._tracestate
return api.createTraceState(ts ? ts.toString() : '')
this._ddContext._ensureSamplingPriority()
const traceState = TraceState.fromString(this._ddContext._tracestate?.toString())
Comment thread
vpellan marked this conversation as resolved.
updateOtelTraceState(this._ddContext, traceState)
return api.createTraceState(traceState.toString())
}
}

Expand Down
32 changes: 14 additions & 18 deletions packages/dd-trace/src/opentelemetry/tracer.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,11 @@ class Tracer {
_convertOtelContextToDatadog (traceId, spanId, traceFlag, ts, meta = {}) {
let origin = null
let samplingPriority = traceFlag
let samplingMechanism
const traceStateValue = typeof ts?.serialize === 'function' ? ts.serialize() : ts?.traceparent
Comment thread
vpellan marked this conversation as resolved.
const traceState = TraceState.fromString(traceStateValue)

ts = ts?.traceparent

if (ts) {
// Use TraceState.fromString to parse the tracestate header
const traceState = TraceState.fromString(ts)
if (traceStateValue) {
let ddTraceStateData = null

// Extract Datadog specific trace state data
Expand All @@ -89,29 +88,26 @@ class Tracer {
})

if (ddTraceStateData) {
// Assuming ddTraceStateData is now a Map or similar structure containing Datadog trace state data
// Extract values as needed, similar to the original logic
const samplingPriorityTs = ddTraceStateData.get('s')
origin = ddTraceStateData.get('o') ?? null
// Convert Map to object for meta
const otherPropagatedTags = Object.fromEntries(ddTraceStateData.entries())

// Update meta and samplingPriority based on extracted values
Object.assign(meta, otherPropagatedTags)
// Guard against an undefined/empty `s:` field that would result in NaN.
const tracestateSamplingPriority = samplingPriorityTs ? Math.trunc(samplingPriorityTs) : undefined
const priority = Number.parseInt(ddTraceStateData.get('s'), 10)
const tracestateSamplingPriority = Number.isInteger(priority) ? priority : undefined
origin = ddTraceStateData.get('o')?.replaceAll('~', '=') ?? null

const mechanism = Math.abs(Number.parseInt(ddTraceStateData.get('t.dm'), 10))
if (Number.isInteger(mechanism)) samplingMechanism = mechanism

samplingPriority = getSamplingPriority(traceFlag, tracestateSamplingPriority, origin)
} else {
log.debug('No dd list member in tracestate from incoming request:', ts)
log.debug('No dd list member in tracestate from incoming request:', traceStateValue)
}
}

const spanContext = new SpanContext({
traceId: id(traceId, 16), spanId: id(), tags: meta, parentId: id(spanId, 16),
traceId: id(traceId, 16), spanId: id(), tags: meta, parentId: id(spanId, 16), tracestate: traceState,
})

spanContext._ddContext._sampling = { priority: samplingPriority }
spanContext._ddContext._trace = { ...spanContext._ddContext._trace, origin }
if (samplingMechanism !== undefined) spanContext._ddContext._sampling.mechanism = samplingMechanism
return spanContext
}

Expand Down
66 changes: 56 additions & 10 deletions packages/dd-trace/src/opentracing/propagation/text_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ const zeroTraceId = '0000000000000000'
const hex16 = /^[0-9A-Fa-f]{16}$/
const percentByte = /%([0-9A-Fa-f]{2})/g

let updateOtelTraceState

/**
* @typedef {object} B3Context
* @property {string} [flags]
Expand Down Expand Up @@ -151,20 +153,23 @@ function getB3Priority (sampled, debug) {
* @returns {DatadogSpanContext | undefined}
*/
function extractB3Context (b3) {
const priority = getB3Priority(b3.sampled, b3.flags === '1')
const spanContext = extractGenericContext(b3.traceId, b3.spanId, 16)
const debug = b3.flags === '1'
const priority = getB3Priority(b3.sampled, debug)
let spanContext = extractGenericContext(b3.traceId, b3.spanId, 16)

if (priority !== undefined) {
if (!spanContext) {
return new DatadogSpanContext({
if (spanContext) {
spanContext._sampling.priority = priority
} else {
spanContext = new DatadogSpanContext({
traceId: id(),
spanId: null,
sampling: { priority },
isRemote: true,
})
}

spanContext._sampling.priority = priority
if (debug) spanContext._sampling.isProbabilityDecision = false
}

if (spanContext && b3.traceId) extract128BitTraceId(b3.traceId, spanContext)
Expand Down Expand Up @@ -619,6 +624,9 @@ class TextMapPropagator {

writeTraceparent(carrier, spanContext.toTraceparent())

updateOtelTraceState ??= require('../../otel-sampling').updateOtelTraceState
updateOtelTraceState(spanContext, ts)

ts.forVendor('dd', state => {
if (!spanContext._isRemote) {
// SpanContext was created by a ddtrace span.
Expand Down Expand Up @@ -694,6 +702,33 @@ class TextMapPropagator {
return this.#config.tracePropagationStyle[mode].includes(name)
}

/**
* Merges W3C sampling state into the context selected by propagation-style precedence.
*
* @param {DatadogSpanContext} w3cSpanContext
* @param {DatadogSpanContext} selectedSpanContext
* @returns {void}
*/
#mergeTraceContextState (w3cSpanContext, selectedSpanContext) {
const selectedPriority = selectedSpanContext._sampling.priority
if (selectedPriority === undefined) {
selectedSpanContext._sampling.priority = w3cSpanContext._sampling.priority
selectedSpanContext._sampling.mechanism = w3cSpanContext._sampling.mechanism
selectedSpanContext._trace.tags['_dd.p.dm'] = w3cSpanContext._trace.tags['_dd.p.dm']
Comment on lines +714 to +717

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Copy W3C decision metadata when B3 already has a priority

When B3 wins extraction and supplies sampled=1 or sampled=0, selectedPriority is already defined, so an agreeing same-trace W3C context's mechanism and _dd.p.dm are not copied. The W3C tracestate is still forwarded, but the local trace chunk and any Datadog-only downstream injection lose the inherited decision maker. Fresh evidence after the missing-priority fix is that this branch copies the metadata only for undefined priorities; agreeing defined priorities need the same metadata merge.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this follow-up because agreeing B3/W3C contexts already retain the raw ot tracestate without copying Datadog-only decision metadata.

} else if (
selectedSpanContext._sampling.isProbabilityDecision === false ||
(selectedPriority >= AUTO_KEEP) !== (w3cSpanContext._sampling.priority >= AUTO_KEEP)
) {
// Copied sampling metadata must not describe a conflicting or non-probabilistic selected decision.
selectedSpanContext._sampling.isProbabilityDecision = false
w3cSpanContext._tracestate.forVendor('dd', state => {
if (state.get('t.dm') !== undefined) state.delete('t.dm')
})
}

selectedSpanContext._tracestate = w3cSpanContext._tracestate
}

/**
* @param {DatadogSpanContext | undefined} w3cSpanContext
* @param {DatadogSpanContext} firstSpanContext
Expand All @@ -703,10 +738,13 @@ class TextMapPropagator {
*/
#resolveTraceContextConflicts (w3cSpanContext, firstSpanContext, carrier, datadogContext) {
if (w3cSpanContext === undefined ||
firstSpanContext.toTraceId(true) !== w3cSpanContext.toTraceId(true) ||
firstSpanContext.toSpanId() === w3cSpanContext.toSpanId()) {
firstSpanContext.toTraceId(true) !== w3cSpanContext.toTraceId(true)) {
return firstSpanContext
}

this.#mergeTraceContextState(w3cSpanContext, firstSpanContext)
if (firstSpanContext.toSpanId() === w3cSpanContext.toSpanId()) return firstSpanContext

if (tags.DD_PARENT_ID in w3cSpanContext._trace.tags) {
// tracecontext headers contain a p value, ensure this value is sent to backend
firstSpanContext._trace.tags[tags.DD_PARENT_ID] = w3cSpanContext._trace.tags[tags.DD_PARENT_ID]
Expand Down Expand Up @@ -744,7 +782,8 @@ class TextMapPropagator {
traceContext = this.#extractTraceparentContext(carrier)
traceContextExtracted = true
}
this.#addTraceContextState(extractedContext, traceContext)
const selectedContext = context && context !== traceContext ? context : extractedContext
this.#addTraceContextState(selectedContext, traceContext)
}
break
case 'tracecontext':
Expand Down Expand Up @@ -846,7 +885,14 @@ class TextMapPropagator {
}

const traceTags = this.#extractTags(carrier)
if (traceTags) spanContext._trace.tags = traceTags
if (traceTags) {
spanContext._trace.tags = traceTags
const decisionMaker = traceTags['_dd.p.dm']
if (decisionMaker !== undefined) {
const mechanism = Math.abs(Number.parseInt(decisionMaker, 10))
if (Number.isInteger(mechanism)) spanContext._sampling.mechanism = mechanism
}
}

return spanContext
}
Expand Down Expand Up @@ -883,7 +929,7 @@ class TextMapPropagator {
#addTraceContextState (datadogContext, traceContext) {
if (traceContext && datadogContext._traceId.equals(traceContext._traceId)) {
datadogContext._traceparent = traceContext._traceparent
datadogContext._tracestate = traceContext._tracestate
this.#mergeTraceContextState(traceContext, datadogContext)
Comment thread
vpellan marked this conversation as resolved.
}
}

Expand Down
39 changes: 38 additions & 1 deletion packages/dd-trace/src/opentracing/propagation/tracestate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// W3C Trace Context §3.3.1.2: max 32 list-members.
// https://www.w3.org/TR/trace-context/#tracestate-header-field-values
const MAX_LIST_MEMBERS = 32
const MAX_TRACESTATE_BYTES = 512
const WHITESPACE = /[ \t]/

/**
Expand Down Expand Up @@ -56,6 +57,37 @@ function toString (map, pairSeparator, fieldSeparator) {
return result
}

/**
* Keeps complete leftmost members within the W3C count and byte limits.
*
* @param {string} value
* @returns {string}
*/
function limitTraceState (value) {
let result = ''
let byteLength = 0
let members = 0
let start = 0

while (start < value.length && members < MAX_LIST_MEMBERS) {
let next = value.indexOf(',', start)
if (next === -1) next = value.length
const member = value.slice(start, next)
const memberLength = Buffer.byteLength(member)
if (memberLength <= MAX_TRACESTATE_BYTES) {
const separatorLength = members === 0 ? 0 : 1
if (byteLength + separatorLength + memberLength > MAX_TRACESTATE_BYTES) break
if (separatorLength) result += ','
result += member
byteLength += separatorLength + memberLength
members++
}
start = next + 1
}

return result
}

class TraceStateData {
#map
changed = false
Expand Down Expand Up @@ -162,7 +194,12 @@ class TraceState {
}

toString () {
return toString(this, '=', ',')
const value = toString(this, '=', ',')
if (this.size <= MAX_LIST_MEMBERS &&
(value.length <= MAX_TRACESTATE_BYTES / 4 || Buffer.byteLength(value) <= MAX_TRACESTATE_BYTES)) {
return value
}
return limitTraceState(value)
}
}

Expand Down
26 changes: 17 additions & 9 deletions packages/dd-trace/src/opentracing/span.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,8 @@ class DatadogSpan {
setTag (key, value) {
this._spanContext.setTag(key, value)

if (isSamplingPriorityTag(key) && this._spanContext._sampling.priority === undefined) {
this._prioritySampler.sample(this, false)
if (isSamplingPriorityTag(key)) {
this._prioritySampler.setPriorityFromTag(this, key, value)
Comment thread
vpellan marked this conversation as resolved.
}

if (tagsUpdateCh.hasSubscribers) {
Expand All @@ -232,25 +232,33 @@ class DatadogSpan {
// `options.tags` callers that pass `'key:val,key:val'` strings.
const tags = this._spanContext.getTags()
let mayChangeSamplingPriority
let samplingTags

if (keyValueMap !== null && typeof keyValueMap === 'object' && !Array.isArray(keyValueMap)) {
Object.assign(tags, keyValueMap)
samplingTags = keyValueMap
// Keep the common path to cheap property probes and verify ownership only when a sampling tag is present.
mayChangeSamplingPriority =
MANUAL_KEEP in keyValueMap ||
MANUAL_DROP in keyValueMap ||
SAMPLING_PRIORITY in keyValueMap
(MANUAL_KEEP in keyValueMap && Object.hasOwn(keyValueMap, MANUAL_KEEP)) ||
(MANUAL_DROP in keyValueMap && Object.hasOwn(keyValueMap, MANUAL_DROP)) ||
(SAMPLING_PRIORITY in keyValueMap && Object.hasOwn(keyValueMap, SAMPLING_PRIORITY))
} else {
/* istanbul ignore if: v5 fallback, master ships 6.0.0-pre */
if (DD_MAJOR < 6 && (typeof keyValueMap === 'string' || Array.isArray(keyValueMap))) {
tagger.add(tags, keyValueMap)
mayChangeSamplingPriority = true
samplingTags = {}
tagger.add(samplingTags, keyValueMap)
Object.assign(tags, samplingTags)
mayChangeSamplingPriority =
(MANUAL_KEEP in samplingTags && Object.hasOwn(samplingTags, MANUAL_KEEP)) ||
(MANUAL_DROP in samplingTags && Object.hasOwn(samplingTags, MANUAL_DROP)) ||
(SAMPLING_PRIORITY in samplingTags && Object.hasOwn(samplingTags, SAMPLING_PRIORITY))
} else {
return this
}
}

if (mayChangeSamplingPriority && this._spanContext._sampling.priority === undefined) {
this._prioritySampler.sample(this, false)
if (mayChangeSamplingPriority) {
this._prioritySampler.setPriorityFromTags(this, samplingTags)
}

if (tagsUpdateCh.hasSubscribers) {
Expand Down
Loading
Loading