dd-trace 6.18.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 (123) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/vitest-no-worker-init-setup.mjs +22 -12
  3. package/index.d.ts +166 -24
  4. package/package.json +7 -7
  5. package/packages/datadog-esbuild/index.js +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +26 -0
  7. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  9. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +40 -4
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
  16. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  17. package/packages/datadog-instrumentations/src/jest.js +3 -10
  18. package/packages/datadog-instrumentations/src/playwright.js +26 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  21. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  22. package/packages/datadog-plugin-cucumber/src/index.js +19 -4
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
  24. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  25. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  26. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  27. package/packages/datadog-plugin-undici/src/index.js +5 -0
  28. package/packages/dd-trace/src/aiguard/client.js +38 -19
  29. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  30. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  31. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  32. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  33. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  34. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
  35. package/packages/dd-trace/src/config/remote_config.js +12 -0
  36. package/packages/dd-trace/src/config/supported-configurations.json +26 -5
  37. package/packages/dd-trace/src/constants.js +2 -0
  38. package/packages/dd-trace/src/debugger/constants.js +13 -4
  39. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  41. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  42. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  43. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  44. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  45. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  46. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  47. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  48. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  49. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  50. package/packages/dd-trace/src/debugger/index.js +60 -28
  51. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  52. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  53. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  54. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  55. package/packages/dd-trace/src/knuth-hash.js +17 -0
  56. package/packages/dd-trace/src/llmobs/constants/tags.js +33 -8
  57. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  58. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  59. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  60. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  61. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  62. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  63. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  64. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  65. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  66. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  67. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  68. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  69. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  71. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  72. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  73. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  74. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  75. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  76. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
  77. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  78. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  79. package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
  80. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  81. package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
  82. package/packages/dd-trace/src/llmobs/tagger.js +37 -36
  83. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  84. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  85. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  86. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  87. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  88. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  89. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  90. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  91. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  92. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  93. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  94. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  95. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  96. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  97. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  98. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  99. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  100. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  101. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  102. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  103. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  104. package/packages/dd-trace/src/opentracing/span.js +14 -9
  105. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  106. package/packages/dd-trace/src/otel-sampling.js +171 -0
  107. package/packages/dd-trace/src/plugins/index.js +0 -1
  108. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  109. package/packages/dd-trace/src/priority_sampler.js +155 -33
  110. package/packages/dd-trace/src/profiler.js +51 -5
  111. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  112. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  113. package/packages/dd-trace/src/ritm.js +12 -9
  114. package/packages/dd-trace/src/sampler.js +2 -5
  115. package/packages/dd-trace/src/sampling_rule.js +4 -8
  116. package/packages/dd-trace/src/span_format.js +6 -0
  117. package/packages/dd-trace/src/span_processor.js +32 -3
  118. package/packages/dd-trace/src/standalone/index.js +5 -4
  119. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  120. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  121. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  122. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  123. 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') },
@@ -2025,6 +2025,10 @@ function getModifiedFilesFromDiff (diff) {
2025
2025
  if (lineMatch && currentFile) {
2026
2026
  const start = Number(lineMatch.groups.start)
2027
2027
  const count = lineMatch.groups.count ? Number(lineMatch.groups.count) : 1
2028
+ if (count === 0) {
2029
+ // Keep the surviving line before a deletion so line-based consumers can match its scope.
2030
+ result[currentFile].push(start)
2031
+ }
2028
2032
  for (let j = 0; j < count; j++) {
2029
2033
  result[currentFile].push(start + j)
2030
2034
  }
@@ -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,8 +12,21 @@ let activeSSIHeuristics
12
12
  /** @type {import('./profiling') | undefined} */
13
13
  let profilingModule
14
14
 
15
+ /**
16
+ * Configuration published while the profiler was finishing a shutdown export, waiting for that
17
+ * export to settle before it is acted on.
18
+ *
19
+ * @type {import('./config/config-base') | undefined}
20
+ */
21
+ let deferredConfig
22
+
15
23
  function getProfilingModule () {
16
- profilingModule ??= require('./profiling')
24
+ if (profilingModule === undefined) {
25
+ profilingModule = require('./profiling')
26
+ // The profiling layer never restarts itself; it only reports that a shutdown export has
27
+ // settled, which is when a configuration deferred during that window gets its decision.
28
+ profilingModule.profiler.on('stopped', applyDeferredConfig)
29
+ }
17
30
  return profilingModule
18
31
  }
19
32
 
@@ -25,6 +38,12 @@ function isStarted () {
25
38
  return profilingModule !== undefined && profilingModule.profiler.enabled
26
39
  }
27
40
 
41
+ // A stopped profiler still exports one final profile, and until that settles it reports neither
42
+ // the state a decision would be based on nor any willingness to start again.
43
+ function isStopping () {
44
+ return profilingModule !== undefined && profilingModule.profiler.isStopping()
45
+ }
46
+
28
47
  /** @type {typeof import('./profiling/ssi-heuristics') | undefined} */
29
48
  let ssiHeuristicsModule
30
49
 
@@ -57,8 +76,11 @@ function start (config) {
57
76
  }
58
77
 
59
78
  function stop () {
60
- // A stop command for a profiler that has never been loaded is already satisfied. Once loaded,
61
- // always forward it so the profiling layer can also cancel a restart queued during shutdown.
79
+ // An explicit stop is the latest desired state, so it retracts a configuration still waiting
80
+ // for an in-flight shutdown to settle.
81
+ deferredConfig = undefined
82
+
83
+ // A stop command for a profiler that has never been loaded is already satisfied.
62
84
  if (profilingModule === undefined) return
63
85
 
64
86
  try {
@@ -93,7 +115,17 @@ function runWithLabels (labels, fn) {
93
115
  return getProfilingModule().profiler.runWithLabels(labels, fn)
94
116
  }
95
117
 
96
- configUpdateChannel.subscribe((config) => {
118
+ function applyDeferredConfig () {
119
+ const config = deferredConfig
120
+ if (config === undefined) return
121
+ deferredConfig = undefined
122
+ applyConfig(config)
123
+ }
124
+
125
+ /**
126
+ * @param {import('./config/config-base')} config - Tracer configuration
127
+ */
128
+ function applyConfig (config) {
97
129
  const enabled = config.profiling.DD_PROFILING_ENABLED
98
130
  if (enabled === 'true') {
99
131
  // A non-auto value means the SSI heuristics no longer get a say; disable them so a trigger that
@@ -118,7 +150,9 @@ configUpdateChannel.subscribe((config) => {
118
150
  heuristics.start()
119
151
  heuristics.onTriggered(() => {
120
152
  // Explicit true/false publishes disable the heuristics, so reaching this callback guarantees
121
- // the latest valid published value is still 'auto'.
153
+ // the latest valid published value is still 'auto'. A heuristic is only ever created for a
154
+ // profiler that is neither running nor stopping, and nothing can start that profiler behind
155
+ // an active heuristic's back, so this start can not collide with a shutdown in flight.
122
156
  if (!isStarted()) start(config)
123
157
  // The heuristic has made its decision, so release all of its listeners and timer before
124
158
  // dropping the module-level reference. This does not stop the profiler that was just
@@ -132,6 +166,18 @@ configUpdateChannel.subscribe((config) => {
132
166
  // like 'auto' or crash the customer application.
133
167
  log.warn('Unexpected DD_PROFILING_ENABLED value: %o', enabled)
134
168
  }
169
+ }
170
+
171
+ configUpdateChannel.subscribe((config) => {
172
+ // A shutdown export in flight makes every decision here unsafe: the profiler can not be
173
+ // started, and it does not yet report the state the auto branch would branch on. Defer instead,
174
+ // keeping only the latest configuration, exactly as a sequence of updates outside such a window
175
+ // would have left it.
176
+ if (isStopping()) {
177
+ deferredConfig = config
178
+ return
179
+ }
180
+ applyConfig(config)
135
181
  })
136
182
 
137
183
  globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(stop)
@@ -61,7 +61,6 @@ class Profiler extends EventEmitter {
61
61
  #exporters
62
62
  #flushInterval
63
63
  #lastStart
64
- #pendingStart
65
64
  #profileSeq = 0
66
65
  #profilers
67
66
  #spanFinishListener
@@ -87,6 +86,14 @@ class Profiler extends EventEmitter {
87
86
  return this.#enabled
88
87
  }
89
88
 
89
+ /**
90
+ * True while a stop()'s final collection is still encoding and exporting. The profiler no
91
+ * longer samples, but it also can not be started again until that settles and 'stopped' fires.
92
+ */
93
+ isStopping () {
94
+ return this.#stopping !== undefined
95
+ }
96
+
90
97
  /**
91
98
  * Declares the set of custom label keys that will be used with
92
99
  * {@link runWithLabels}. This is used for profile upload metadata and
@@ -184,12 +191,12 @@ class Profiler extends EventEmitter {
184
191
  if (this.#startFailed) return false
185
192
 
186
193
  // A prior stop()'s shutdown collection may still be encoding/exporting via #tags,
187
- // #exporters and #endpointCounts. Wait for it to finish before this start() overwrites
188
- // that shared state out from under it. Record the desired config rather than chaining
189
- // straight onto #stopping, so a stop() arriving before it settles can cancel this restart.
194
+ // #exporters and #endpointCounts, which this start() would overwrite out from under it.
195
+ // Refuse rather than queue: the caller owns the decision of what to do once the shutdown
196
+ // settles, and by then its configuration may no longer be the one requested here. This is
197
+ // thrown outside the setup try/catch below so it does not count as a permanent start failure.
190
198
  if (this.#stopping) {
191
- this.#pendingStart = config
192
- return true
199
+ throw new Error('Cannot start the profiler while a shutdown collection is still in flight')
193
200
  }
194
201
 
195
202
  try {
@@ -294,11 +301,12 @@ class Profiler extends EventEmitter {
294
301
  this._timeoutInterval = this.#flushInterval
295
302
  }
296
303
 
304
+ /**
305
+ * Stops the profiler after collecting and exporting one final profile. That export outlives
306
+ * this call: {@link isStopping} returns true and start() throws until it settles, at which
307
+ * point a 'stopped' event is emitted.
308
+ */
297
309
  stop () {
298
- // A stop() always reflects the latest desired state, so it cancels any restart queued by a
299
- // start() that arrived while a prior shutdown collection was still in flight.
300
- this.#pendingStart = undefined
301
-
302
310
  if (!this.enabled) return
303
311
 
304
312
  // collect and export current profiles
@@ -311,16 +319,12 @@ class Profiler extends EventEmitter {
311
319
  })
312
320
  .finally(() => {
313
321
  this.#stopping = undefined
314
- if (this.#pendingStart) {
315
- const config = this.#pendingStart
316
- this.#pendingStart = undefined
317
- // Unlike the synchronous entry point, nothing awaits this promise, so a throw here would
318
- // become an unhandled rejection instead of a caught error.
319
- try {
320
- this.start(config)
321
- } catch (error) {
322
- log.error(error)
323
- }
322
+ // Nothing awaits this promise, so a listener throwing here would become an unhandled
323
+ // rejection instead of reaching whoever requested the stop.
324
+ try {
325
+ this.emit('stopped')
326
+ } catch (error) {
327
+ log.error(error)
324
328
  }
325
329
  })
326
330
  this.#stop()