dd-trace 6.19.0 → 6.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/index.d.ts +163 -22
  2. package/package.json +2 -2
  3. package/packages/datadog-esbuild/index.js +9 -6
  4. package/packages/datadog-instrumentations/src/ai.js +26 -0
  5. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  9. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  10. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  11. package/packages/dd-trace/src/config/remote_config.js +12 -0
  12. package/packages/dd-trace/src/config/supported-configurations.json +21 -0
  13. package/packages/dd-trace/src/debugger/constants.js +13 -4
  14. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  15. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  16. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  17. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  18. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  19. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  20. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  21. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  22. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  23. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  24. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  25. package/packages/dd-trace/src/debugger/index.js +60 -28
  26. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  27. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  28. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  29. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  30. package/packages/dd-trace/src/knuth-hash.js +17 -0
  31. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  32. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  33. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  34. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  35. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  36. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  37. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  38. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  39. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +2 -2
  40. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  41. package/packages/dd-trace/src/llmobs/span_processor.js +42 -26
  42. package/packages/dd-trace/src/llmobs/tagger.js +35 -0
  43. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  44. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  45. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  46. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  47. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  48. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  49. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  50. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  51. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  52. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  53. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  54. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  55. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  56. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  57. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  58. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  59. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  60. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  61. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  62. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  63. package/packages/dd-trace/src/opentracing/span.js +14 -9
  64. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  65. package/packages/dd-trace/src/otel-sampling.js +171 -0
  66. package/packages/dd-trace/src/plugins/index.js +0 -1
  67. package/packages/dd-trace/src/priority_sampler.js +155 -33
  68. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  69. package/packages/dd-trace/src/sampler.js +2 -5
  70. package/packages/dd-trace/src/sampling_rule.js +4 -8
  71. package/packages/dd-trace/src/span_format.js +6 -0
  72. package/packages/dd-trace/src/span_processor.js +26 -2
  73. package/packages/dd-trace/src/standalone/index.js +5 -4
  74. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  75. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  76. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  77. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -0,0 +1,171 @@
1
+ 'use strict'
2
+
3
+ const { AUTO_KEEP } = require('../../../ext/priority')
4
+ const knuthHash = require('./knuth-hash')
5
+ const { SAMPLING_AGENT_DECISION, SAMPLING_RULE_DECISION } = require('./constants')
6
+
7
+ const MAX_THRESHOLD = 2n ** 56n
8
+ const MAX_ENCODABLE_THRESHOLD = MAX_THRESHOLD - 1n
9
+ const UINT64_MASK = 2n ** 64n - 1n
10
+
11
+ /**
12
+ * @param {unknown} value
13
+ * @param {number} minLength
14
+ * @param {number} maxLength
15
+ * @returns {value is string}
16
+ */
17
+ function isLowerHex (value, minLength, maxLength) {
18
+ if (typeof value !== 'string' || value.length < minLength || value.length > maxLength) return false
19
+
20
+ for (let index = 0; index < value.length; index++) {
21
+ const code = value.charCodeAt(index)
22
+ if ((code < 48 || code > 57) && (code < 97 || code > 102)) return false
23
+ }
24
+
25
+ return true
26
+ }
27
+
28
+ /**
29
+ * Derives the OTel 56-bit random value from Datadog's sampling hash.
30
+ *
31
+ * @param {bigint} traceId
32
+ */
33
+ function randomValueFor (traceId) {
34
+ return ((~knuthHash(traceId)) & UINT64_MASK) >> 8n
35
+ }
36
+
37
+ /**
38
+ * Converts a sample rate to an OTel 56-bit rejection threshold.
39
+ *
40
+ * @param {number} sampleRate
41
+ */
42
+ function thresholdFor (sampleRate) {
43
+ if (sampleRate === 1) return 0n
44
+ if (sampleRate === 0) return MAX_ENCODABLE_THRESHOLD
45
+
46
+ const threshold = BigInt(Math.round((1 - sampleRate) * Number(MAX_THRESHOLD)))
47
+ if (threshold < 0n) return 0n
48
+ if (threshold > MAX_ENCODABLE_THRESHOLD) return MAX_ENCODABLE_THRESHOLD
49
+ return threshold
50
+ }
51
+
52
+ /**
53
+ * Formats an OTel threshold with trailing zero nibbles removed.
54
+ *
55
+ * @param {bigint} threshold
56
+ */
57
+ function formatThreshold (threshold) {
58
+ const value = threshold.toString(16).padStart(14, '0')
59
+ let end = value.length
60
+ while (end > 0 && value.charCodeAt(end - 1) === 48) end--
61
+ return end === 0 ? '0' : value.slice(0, end)
62
+ }
63
+
64
+ /**
65
+ * Generates OTel sampling fields for a local probability decision.
66
+ *
67
+ * @param {import('./opentracing/span_context')} context
68
+ * @param {number} probabilityRate
69
+ * @returns {{ randomValue: string, threshold: string } | undefined}
70
+ */
71
+ function generateFields (context, probabilityRate) {
72
+ const { priority } = context._sampling
73
+ if (priority === undefined || probabilityRate === undefined) return
74
+
75
+ const thresholdValue = thresholdFor(probabilityRate)
76
+ let randomValue = randomValueFor(context._traceId.toBigInt())
77
+ const kept = priority >= AUTO_KEEP
78
+
79
+ if (kept) {
80
+ if (randomValue < thresholdValue) {
81
+ randomValue = thresholdValue
82
+ }
83
+ } else if (randomValue >= thresholdValue) {
84
+ randomValue = thresholdValue > 0n ? thresholdValue - 1n : 0n
85
+ }
86
+
87
+ return {
88
+ randomValue: randomValue.toString(16).padStart(14, '0'),
89
+ threshold: formatThreshold(thresholdValue),
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Returns the probability rate recorded by a committed Datadog sampling decision.
95
+ * Sampling probes deliberately leave these rate fields unset.
96
+ *
97
+ * @param {import('./opentracing/span_context')} context
98
+ * @returns {number | undefined}
99
+ */
100
+ function getProbabilityRate (context) {
101
+ if (context._sampling.isProbabilityDecision === false) return
102
+ return (context._trace[SAMPLING_RULE_DECISION] ?? context._trace[SAMPLING_AGENT_DECISION]) || undefined
103
+ }
104
+
105
+ /**
106
+ * Removes malformed OTel sampling fields and returns the valid values.
107
+ *
108
+ * @param {import('./opentracing/propagation/tracestate')} state
109
+ * @returns {{ randomValue: string | undefined, threshold: string | undefined }}
110
+ */
111
+ function sanitizeFields (state) {
112
+ let randomValue = state.get('rv')
113
+ let threshold = state.get('th')
114
+
115
+ if (!isLowerHex(randomValue, 14, 14)) {
116
+ state.delete('rv')
117
+ randomValue = undefined
118
+ }
119
+ if (!isLowerHex(threshold, 1, 14)) {
120
+ state.delete('th')
121
+ threshold = undefined
122
+ }
123
+
124
+ return { randomValue, threshold }
125
+ }
126
+
127
+ /**
128
+ * Sanitizes inherited OTel sampling fields without changing valid state.
129
+ *
130
+ * @param {import('./opentracing/propagation/tracestate')} traceState
131
+ */
132
+ function normalizeOtelTraceState (traceState) {
133
+ if (traceState.get('ot') === undefined) return
134
+ traceState.forVendor('ot', sanitizeFields)
135
+ }
136
+
137
+ /**
138
+ * Updates the OTel tracestate member to represent the context's sampling decision.
139
+ *
140
+ * @param {import('./opentracing/span_context')} context
141
+ * @param {import('./opentracing/propagation/tracestate')} traceState
142
+ */
143
+ function updateOtelTraceState (context, traceState) {
144
+ const otelMember = traceState.get('ot')
145
+ const probabilityRate = getProbabilityRate(context)
146
+ if (context._sampling.isProbabilityDecision === false) {
147
+ if (otelMember === undefined) return
148
+ } else if (probabilityRate === undefined) {
149
+ return
150
+ }
151
+
152
+ traceState.forVendor('ot', state => {
153
+ const { randomValue, threshold } = sanitizeFields(state)
154
+
155
+ if (context._sampling.isProbabilityDecision === false) {
156
+ state.delete('th')
157
+ } else if (randomValue === undefined && threshold === undefined) {
158
+ const generated = generateFields(context, probabilityRate)
159
+ if (generated) {
160
+ // Entries serialize in reverse insertion order, so add th before rv.
161
+ state.set('th', generated.threshold)
162
+ state.set('rv', generated.randomValue)
163
+ }
164
+ }
165
+ })
166
+ }
167
+
168
+ module.exports = {
169
+ normalizeOtelTraceState,
170
+ updateOtelTraceState,
171
+ }
@@ -15,7 +15,6 @@ const plugins = {
15
15
  get '@supabase/postgrest-js' () { return require('../../../datadog-plugin-supabase/src') },
16
16
  get '@supabase/realtime-js' () { return require('../../../datadog-plugin-supabase/src') },
17
17
  get '@supabase/storage-js' () { return require('../../../datadog-plugin-supabase/src') },
18
- get '@supabase/supabase-js' () { return require('../../../datadog-plugin-supabase/src') },
19
18
  get 'durable-functions' () { return require('../../../datadog-plugin-azure-durable-functions/src') },
20
19
  get '@azure/service-bus' () { return require('../../../datadog-plugin-azure-service-bus/src') },
21
20
  get '@cucumber/cucumber' () { return require('../../../datadog-plugin-cucumber/src') },
@@ -26,8 +26,10 @@ const {
26
26
  SAMPLING_MECHANISM_AGENT,
27
27
  SAMPLING_MECHANISM_RULE,
28
28
  SAMPLING_MECHANISM_MANUAL,
29
+ SAMPLING_MECHANISM_APPSEC,
29
30
  SAMPLING_MECHANISM_REMOTE_USER,
30
31
  SAMPLING_MECHANISM_REMOTE_DYNAMIC,
32
+ SAMPLING_MECHANISM_AI_GUARD,
31
33
  SAMPLING_RULE_DECISION,
32
34
  SAMPLING_LIMIT_DECISION,
33
35
  SAMPLING_AGENT_DECISION,
@@ -39,6 +41,17 @@ const DEFAULT_KEY = 'service:,env:'
39
41
 
40
42
  const defaultSampler = new Sampler(AUTO_KEEP)
41
43
 
44
+ /**
45
+ * Returns whether a product has already force-kept the trace.
46
+ *
47
+ * @param {import('./opentracing/span_context')} context
48
+ */
49
+ function isProductForceKeep (context) {
50
+ const { priority, mechanism } = context._sampling
51
+ return priority === USER_KEEP &&
52
+ (mechanism === SAMPLING_MECHANISM_APPSEC || mechanism === SAMPLING_MECHANISM_AI_GUARD)
53
+ }
54
+
42
55
  /**
43
56
  * PrioritySampler is responsible for determining whether a span should be sampled
44
57
  * based on various rules, rate limits, and priorities. It supports manual and
@@ -114,10 +127,10 @@ class PrioritySampler {
114
127
  const tag = this._getPriorityFromTags(context.getTags(), context)
115
128
 
116
129
  if (this.validate(tag)) {
117
- context._sampling.priority = tag
118
130
  context._sampling.mechanism = SAMPLING_MECHANISM_MANUAL
131
+ this._recordDecision(context, tag)
119
132
  } else if (auto) {
120
- context._sampling.priority = this._getPriorityFromAuto(root)
133
+ this.decideFromAuto(root)
121
134
  } else {
122
135
  return
123
136
  }
@@ -125,6 +138,22 @@ class PrioritySampler {
125
138
  this.#addDecisionMaker(root)
126
139
  }
127
140
 
141
+ /**
142
+ * Applies manual sampling tags from a supplied tag collection using their defined precedence.
143
+ *
144
+ * @param {DatadogSpan} span
145
+ * @param {Record<string, unknown>} tags
146
+ */
147
+ setPriorityFromTags (span, tags) {
148
+ if (!span) return
149
+
150
+ const context = this._getContext(span)
151
+ if (isProductForceKeep(context)) return
152
+
153
+ const priority = this._getPriorityFromTags(tags, context)
154
+ if (this.validate(priority)) this.setPriority(span, priority)
155
+ }
156
+
128
157
  /**
129
158
  * Updates agent-provided sampling rates keyed by `service:,env:`.
130
159
  *
@@ -179,14 +208,13 @@ class PrioritySampler {
179
208
  return // noop span
180
209
  }
181
210
 
182
- context._sampling.priority = samplingPriority
183
-
184
211
  const mechanism = product?.mechanism ?? SAMPLING_MECHANISM_MANUAL
185
212
  context._sampling.mechanism = mechanism
213
+ this._recordDecision(context, samplingPriority)
186
214
 
187
215
  log.trace(span, samplingPriority, mechanism)
188
216
 
189
- this.#addDecisionMaker(root)
217
+ this.#addDecisionMaker(root, product === undefined)
190
218
  }
191
219
 
192
220
  /**
@@ -201,6 +229,23 @@ class PrioritySampler {
201
229
  : /** @type {DatadogSpanContext} */ (span)
202
230
  }
203
231
 
232
+ /**
233
+ * Evaluates and records an automatic sampling decision.
234
+ *
235
+ * @param {DatadogSpan} span
236
+ * @returns {SamplingPriority}
237
+ */
238
+ decideFromAuto (span) {
239
+ const context = this._getContext(span)
240
+ const rule = this.#findRule(span)
241
+ const priority = rule
242
+ ? this.#getPriorityByRule(context, rule, true)
243
+ : this.#getPriorityByAgent(context, true)
244
+
245
+ context._sampling.priority = priority
246
+ return priority
247
+ }
248
+
204
249
  /**
205
250
  * Computes priority using rules and agent rates when no manual tag is present.
206
251
  *
@@ -212,27 +257,25 @@ class PrioritySampler {
212
257
  const rule = this.#findRule(span)
213
258
 
214
259
  return rule
215
- ? this.#getPriorityByRule(context, rule)
216
- : this.#getPriorityByAgent(context)
260
+ ? this.#getPriorityByRule(context, rule, false)
261
+ : this.#getPriorityByAgent(context, false)
217
262
  }
218
263
 
219
264
  /**
220
- * Computes priority from manual sampling tags if present.
221
- * Included for compatibility with {@link import('./standalone/tracesource_priority_sampler')._getPriorityFromTags}
265
+ * Converts one sampling tag into a user priority.
222
266
  *
223
- * @param {Record<string, unknown>} tags
267
+ * @param {string} key
268
+ * @param {unknown} value
224
269
  * @param {DatadogSpanContext} _context
225
270
  * @returns {SamplingPriority|undefined}
226
271
  */
227
- _getPriorityFromTags (tags, _context) {
228
- if (Object.hasOwn(tags, MANUAL_KEEP) && tags[MANUAL_KEEP] !== false) {
272
+ _getPriorityFromTag (key, value, _context) {
273
+ if (key === MANUAL_KEEP && value !== false) {
229
274
  return USER_KEEP
230
- } else if (Object.hasOwn(tags, MANUAL_DROP) && tags[MANUAL_DROP] !== false) {
275
+ } else if (key === MANUAL_DROP && value !== false) {
231
276
  return USER_REJECT
232
- }
233
- const rawPriority = tags[SAMPLING_PRIORITY]
234
- if (rawPriority !== undefined) {
235
- const priority = Math.trunc(rawPriority)
277
+ } else if (key === SAMPLING_PRIORITY && (typeof value === 'number' || typeof value === 'string')) {
278
+ const priority = Math.trunc(/** @type {number} */ (value))
236
279
 
237
280
  if (priority === 1 || priority === 2) {
238
281
  return USER_KEEP
@@ -242,30 +285,85 @@ class PrioritySampler {
242
285
  }
243
286
  }
244
287
 
288
+ /**
289
+ * Computes priority from manual sampling tags if present.
290
+ * Calls the overridable single-tag conversion so specialized samplers retain their filtering rules.
291
+ *
292
+ * @param {Record<string, unknown>} tags
293
+ * @param {DatadogSpanContext} _context
294
+ * @returns {SamplingPriority|undefined}
295
+ */
296
+ _getPriorityFromTags (tags, _context) {
297
+ if (!Object.hasOwn(tags, MANUAL_KEEP) && !Object.hasOwn(tags, MANUAL_DROP)) {
298
+ const priority = tags[SAMPLING_PRIORITY]
299
+ if (priority !== undefined) return this._getPriorityFromTag(SAMPLING_PRIORITY, priority, _context)
300
+ return
301
+ }
302
+
303
+ return this.#getPriorityFromManualTags(tags, _context)
304
+ }
305
+
306
+ /**
307
+ * Computes priority when at least one manual keep/drop tag is present.
308
+ * Value checks cover the common case; own-property checks preserve undefined-valued manual tags.
309
+ *
310
+ * @param {Record<string, unknown>} tags
311
+ * @param {DatadogSpanContext} context
312
+ * @returns {SamplingPriority|undefined}
313
+ */
314
+ #getPriorityFromManualTags (tags, context) {
315
+ const manualKeep = tags[MANUAL_KEEP]
316
+ if (manualKeep !== undefined || Object.hasOwn(tags, MANUAL_KEEP)) {
317
+ const priority = this._getPriorityFromTag(MANUAL_KEEP, manualKeep, context)
318
+ if (priority !== undefined) return priority
319
+ }
320
+
321
+ const manualDrop = tags[MANUAL_DROP]
322
+ if (manualDrop !== undefined || Object.hasOwn(tags, MANUAL_DROP)) {
323
+ const priority = this._getPriorityFromTag(MANUAL_DROP, manualDrop, context)
324
+ if (priority !== undefined) return priority
325
+ }
326
+ const priority = tags[SAMPLING_PRIORITY]
327
+ if (priority !== undefined) return this._getPriorityFromTag(SAMPLING_PRIORITY, priority, context)
328
+ }
329
+
245
330
  /**
246
331
  * Applies a matching rule and rate limit to compute the sampling priority.
247
332
  *
248
333
  * @param {DatadogSpanContext} context
249
334
  * @param {import('./sampling_rule')} rule
335
+ * @param {boolean} recordDecision
250
336
  * @returns {SamplingPriority}
251
337
  */
252
- #getPriorityByRule (context, rule) {
253
- context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate
338
+ #getPriorityByRule (context, rule, recordDecision) {
339
+ if (recordDecision) context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate
254
340
  context._trace.tags[SAMPLING_KNUTH_RATE] = formatKnuthRate(rule.sampleRate)
255
- context._sampling.mechanism = SAMPLING_MECHANISM_RULE
256
- if (rule.provenance === 'customer') {
257
- context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_USER
258
- } else if (rule.provenance === 'dynamic') {
259
- context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_DYNAMIC
341
+ if (recordDecision) {
342
+ context._sampling.mechanism = SAMPLING_MECHANISM_RULE
343
+ if (rule.provenance === 'customer') {
344
+ context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_USER
345
+ } else if (rule.provenance === 'dynamic') {
346
+ context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_DYNAMIC
347
+ }
260
348
  }
261
349
 
262
350
  if (rule.discard) {
263
351
  context._sampling.discard = true
264
352
  }
265
353
 
266
- return rule.sample(context) && this._isSampledByRateLimit(context)
267
- ? USER_KEEP
268
- : USER_REJECT
354
+ const sampled = rule.sample(context, true)
355
+ if (sampled !== true) {
356
+ if (recordDecision && sampled === undefined) this._recordDecisionMetadata(context)
357
+ return USER_REJECT
358
+ }
359
+
360
+ if (!this._isSampledByRateLimit(context)) {
361
+ // The limiter makes only rejected traces non-probabilistic; allowed traces retain the rule probability.
362
+ if (recordDecision) this._recordDecisionMetadata(context)
363
+ return USER_REJECT
364
+ }
365
+
366
+ return USER_KEEP
269
367
  }
270
368
 
271
369
  /**
@@ -287,39 +385,63 @@ class PrioritySampler {
287
385
  * Computes priority using agent-provided sampling rates.
288
386
  *
289
387
  * @param {DatadogSpanContext} context
388
+ * @param {boolean} recordDecision
290
389
  * @returns {SamplingPriority}
291
390
  */
292
- #getPriorityByAgent (context) {
391
+ #getPriorityByAgent (context, recordDecision) {
293
392
  const key = `service:${context.getTag(SERVICE_NAME)},env:${this._env}`
294
393
  // TODO: Change underscored properties to private ones.
295
394
  const sampler = this._samplers[key] || this._samplers[DEFAULT_KEY]
296
395
 
297
396
  const rate = sampler.rate()
298
- context._trace[SAMPLING_AGENT_DECISION] = rate
397
+ if (recordDecision) context._trace[SAMPLING_AGENT_DECISION] = rate
299
398
 
300
399
  if (sampler === defaultSampler) {
301
- context._sampling.mechanism = SAMPLING_MECHANISM_DEFAULT
400
+ if (recordDecision) context._sampling.mechanism = SAMPLING_MECHANISM_DEFAULT
302
401
  } else {
303
402
  context._trace.tags[SAMPLING_KNUTH_RATE] = formatKnuthRate(rate)
304
- context._sampling.mechanism = SAMPLING_MECHANISM_AGENT
403
+ if (recordDecision) context._sampling.mechanism = SAMPLING_MECHANISM_AGENT
305
404
  }
306
405
 
307
406
  return sampler.isSampled(context) ? AUTO_KEEP : AUTO_REJECT
308
407
  }
309
408
 
409
+ /**
410
+ * Records that a sampling decision does not represent a probability.
411
+ *
412
+ * @param {DatadogSpanContext} context
413
+ */
414
+ _recordDecisionMetadata (context) {
415
+ context._sampling.isProbabilityDecision = false
416
+ }
417
+
418
+ /**
419
+ * Records a local non-probability sampling decision.
420
+ *
421
+ * @param {DatadogSpanContext} context
422
+ * @param {SamplingPriority} priority
423
+ * @returns {SamplingPriority}
424
+ */
425
+ _recordDecision (context, priority) {
426
+ context._sampling.priority = priority
427
+ this._recordDecisionMetadata(context)
428
+ return priority
429
+ }
430
+
310
431
  /**
311
432
  * Tags the trace with a decision maker when priority is keep, or removes it otherwise.
312
433
  *
313
434
  * @param {DatadogSpan} span
435
+ * @param {boolean} [overwrite]
314
436
  */
315
- #addDecisionMaker (span) {
437
+ #addDecisionMaker (span, overwrite = false) {
316
438
  const context = span.context()
317
439
  const trace = context._trace
318
440
  const priority = context._sampling.priority
319
441
  const mechanism = context._sampling.mechanism
320
442
 
321
443
  if (priority >= AUTO_KEEP) {
322
- if (!trace.tags[DECISION_MAKER_KEY]) {
444
+ if (overwrite || !trace.tags[DECISION_MAKER_KEY]) {
323
445
  trace.tags[DECISION_MAKER_KEY] = `-${mechanism}`
324
446
  }
325
447
  } else if (trace.tags[DECISION_MAKER_KEY] !== undefined) {
@@ -12,19 +12,28 @@ module.exports = {
12
12
  ASM_CUSTOM_BLOCKING_RESPONSE: 1n << 9n,
13
13
  ASM_TRUSTED_IPS: 1n << 10n,
14
14
  ASM_API_SECURITY_SAMPLE_RATE: 1n << 11n, // deprecated
15
+ APM_TRACING_SAMPLE_RATE: 1n << 12n,
16
+ APM_TRACING_LOGS_INJECTION: 1n << 13n,
17
+ APM_TRACING_HTTP_HEADER_TAGS: 1n << 14n,
18
+ APM_TRACING_CUSTOM_TAGS: 1n << 15n,
15
19
  ASM_PROCESSOR_OVERRIDES: 1n << 16n,
16
20
  ASM_CUSTOM_DATA_SCANNERS: 1n << 17n,
17
21
  ASM_EXCLUSION_DATA: 1n << 18n,
22
+ APM_TRACING_ENABLED: 1n << 19n,
18
23
  ASM_RASP_SQLI: 1n << 21n,
19
24
  ASM_RASP_LFI: 1n << 22n,
20
25
  ASM_RASP_SSRF: 1n << 23n,
21
26
  ASM_RASP_SHI: 1n << 24n,
27
+ APM_TRACING_SAMPLE_RULES: 1n << 29n,
22
28
  ASM_AUTO_USER_INSTRUM_MODE: 1n << 31n,
23
29
  ASM_ENDPOINT_FINGERPRINT: 1n << 32n,
24
30
  ASM_SESSION_FINGERPRINT: 1n << 33n,
25
31
  ASM_NETWORK_FINGERPRINT: 1n << 34n,
26
32
  ASM_HEADER_FINGERPRINT: 1n << 35n,
27
33
  ASM_RASP_CMDI: 1n << 37n,
34
+ APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION: 1n << 38n,
35
+ APM_TRACING_ENABLE_CODE_ORIGIN: 1n << 40n,
36
+ APM_TRACING_ENABLE_LIVE_DEBUGGING: 1n << 41n,
28
37
  ASM_DD_MULTICONFIG: 1n << 42n,
29
38
  ASM_TRACE_TAGGING_RULES: 1n << 43n,
30
39
  ASM_EXTENDED_DATA_COLLECTION: 1n << 44n,
@@ -5,10 +5,7 @@
5
5
  // as it is cast into a float64 when computing the threshold
6
6
  const MAX_TRACE_ID = 2 ** 64 - 1
7
7
 
8
- const UINT64_MODULO = 2n ** 64n
9
-
10
- // Knuth's factor for the sampling algorithm
11
- const SAMPLING_KNUTH_FACTOR = 1_111_111_111_111_111_111n
8
+ const knuthHash = require('./knuth-hash')
12
9
 
13
10
  /**
14
11
  * `Sampler` determines whether or not to sample a trace/span based on the trace ID.
@@ -52,7 +49,7 @@ class Sampler {
52
49
 
53
50
  span = typeof span.context === 'function' ? span.context() : span
54
51
 
55
- return (span._traceId.toBigInt() * SAMPLING_KNUTH_FACTOR) % UINT64_MODULO <= this.#threshold
52
+ return knuthHash(span._traceId.toBigInt()) <= this.#threshold
56
53
  }
57
54
  }
58
55
 
@@ -274,16 +274,12 @@ class SamplingRule {
274
274
  * Determines whether a span should be sampled based on the configured sampling rule.
275
275
  *
276
276
  * @param {DatadogSpan|DatadogSpanContext} span - The span or span context to evaluate.
277
+ * @param {boolean} [distinguishRateLimit] Return `undefined` instead of `false` for a rate-limit rejection.
277
278
  */
278
- sample (span) {
279
- if (!this._sampler.isSampled(span)) {
280
- return false
281
- }
282
-
283
- if (this._limiter) {
284
- return this._limiter.isAllowed()
285
- }
279
+ sample (span, distinguishRateLimit = false) {
280
+ if (!this._sampler.isSampled(span)) return false
286
281
 
282
+ if (this._limiter && !this._limiter.isAllowed()) return distinguishRateLimit ? undefined : false
287
283
  return true
288
284
  }
289
285
  }
@@ -47,6 +47,7 @@ const { IGNORE_OTEL_ERROR } = constants
47
47
  * @property {number} duration
48
48
  * @property {Array} links
49
49
  * @property {Array<SpanEvent> | undefined} span_events
50
+ * @property {string} [trace_state] W3C tracestate, populated only for OTLP export.
50
51
  *
51
52
  * @typedef {object} SpanEvent Raw span event as stored on the span; the encoder
52
53
  * layer derives `time_unix_nano` from `startTime` via `eventTimeNano`.
@@ -55,6 +56,11 @@ const { IGNORE_OTEL_ERROR } = constants
55
56
  * @property {Record<string, string>} [attributes]
56
57
  */
57
58
 
59
+ /**
60
+ * @param {import('./opentracing/span')} span
61
+ * @param {boolean} [isFirstSpanInChunk]
62
+ * @param {string | false} [tagForFirstSpanInChunk]
63
+ */
58
64
  function format (span, isFirstSpanInChunk = false, tagForFirstSpanInChunk = false) {
59
65
  const formatted = formatSpan(span)
60
66
 
@@ -7,17 +7,40 @@ const SpanSampler = require('./span_sampler')
7
7
  const GitMetadataTagger = require('./git_metadata_tagger')
8
8
  const processTags = require('./process-tags')
9
9
  const { applyHttpOtelSemantics } = require('./plugins/util/http-otel-semantics')
10
+ const TraceState = require('./opentracing/propagation/tracestate')
10
11
  const { APM_TRACING_ENABLED_KEY, SDK_OTLP_EXPORT_KEY } = require('./constants')
11
12
 
12
13
  const startedSpans = new WeakSet()
13
14
  const finishedSpans = new WeakSet()
14
15
 
16
+ let otelSampling
17
+
18
+ /**
19
+ * Adds first-class OTLP trace context to a DD-formatted span.
20
+ *
21
+ * @param {import('./opentracing/span')} span
22
+ * @param {boolean} isFirstSpanInChunk
23
+ * @param {string | false} processTagsValue
24
+ */
25
+ function formatOtlpSpan (span, isFirstSpanInChunk, processTagsValue) {
26
+ const formattedSpan = spanFormat(span, isFirstSpanInChunk, processTagsValue)
27
+ const context = span.context()
28
+ const traceState = context._tracestate?.clone() ?? new TraceState()
29
+ otelSampling ??= require('./otel-sampling')
30
+ otelSampling.updateOtelTraceState(context, traceState)
31
+ formattedSpan.trace_state = traceState.toString()
32
+ return formattedSpan
33
+ }
34
+
15
35
  class SpanProcessor {
16
- constructor (exporter, prioritySampler, config, otlpStatsExporter) {
36
+ #formatSpan
37
+
38
+ constructor (exporter, prioritySampler, config, otlpStatsExporter, exportOtlpTraces) {
17
39
  this._exporter = exporter
18
40
  this._prioritySampler = prioritySampler
19
41
  this._config = config
20
42
  this._killAll = false
43
+ this.#formatSpan = exportOtlpTraces ? formatOtlpSpan : spanFormat
21
44
 
22
45
  if (config.stats?.DD_TRACE_STATS_COMPUTATION_ENABLED &&
23
46
  !config.appsec?.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED) {
@@ -72,12 +95,13 @@ class SpanProcessor {
72
95
  let isFirstSpanInChunk = true
73
96
  const stampApmDisabled = this._config.apmTracingEnabled === false
74
97
  const discard = this.#isDiscarded(spanContext)
98
+ const formatSpan = this.#formatSpan
75
99
 
76
100
  for (const span of started) {
77
101
  if (span._duration === undefined) {
78
102
  active.push(span)
79
103
  } else if (!discard) {
80
- const formattedSpan = spanFormat(span, isFirstSpanInChunk, this._processTags)
104
+ const formattedSpan = formatSpan(span, isFirstSpanInChunk, this._processTags)
81
105
  if (stampApmDisabled) {
82
106
  formattedSpan.metrics[APM_TRACING_ENABLED_KEY] = 0
83
107
  }
@@ -23,11 +23,12 @@ function configure (config) {
23
23
  function onSpanExtract ({ spanContext = {} }) {
24
24
  if (!spanContext._trace?.tags || !spanContext._sampling) return
25
25
 
26
- // reset upstream priority if _dd.p.ts is not found
27
- if (!hasTraceSourcePropagationTag(spanContext._trace.tags)) {
28
- spanContext._sampling.priority = undefined
29
- } else if (spanContext._sampling.priority !== USER_KEEP) {
26
+ if (hasTraceSourcePropagationTag(spanContext._trace.tags)) {
30
27
  spanContext._sampling.priority = USER_KEEP
28
+ spanContext._sampling.isProbabilityDecision = false
29
+ } else {
30
+ // Reset upstream priority if _dd.p.ts is not found.
31
+ spanContext._sampling.priority = undefined
31
32
  }
32
33
  }
33
34