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,77 @@
1
+ 'use strict'
2
+
3
+ const { parentPort, workerData } = require('node:worker_threads')
4
+ const port = /** @type {import('node:worker_threads').MessagePort} */ (parentPort)
5
+
6
+ // Bootstrap only the lifecycle registry, never the tracer or its preload entrypoints.
7
+ globalThis[Symbol.for('dd-trace')] = { beforeExitHandlers: new Set() }
8
+
9
+ const logWriter = require('../../log/writer')
10
+ const { configureWorkerTelemetry } = require('./flag-evaluation-telemetry')
11
+ const state = new Int32Array(workerData.state)
12
+ configureWorkerTelemetry(state)
13
+ const FlagEvaluationConsumer = require('./flag-evaluation-consumer')
14
+
15
+ /** @param {'debug' | 'info' | 'warn' | 'error'} level */
16
+ function forwardLog (level) {
17
+ /** @param {unknown} message */
18
+ return message => {
19
+ try {
20
+ port.postMessage({ type: 'log', level, message: message instanceof Error ? message.stack : String(message) })
21
+ } catch {
22
+ // Diagnostics are best-effort when the parent port is closing.
23
+ }
24
+ }
25
+ }
26
+
27
+ // Use the parent's effective settings, not a second environment-only configuration.
28
+ // The custom logger itself stays in the parent isolate because functions cannot be cloned.
29
+ const { enabled = false, level } = workerData.logging ?? {}
30
+ logWriter.configure(enabled, level, {
31
+ debug: forwardLog('debug'),
32
+ info: forwardLog('info'),
33
+ warn: forwardLog('warn'),
34
+ error: forwardLog('error'),
35
+ })
36
+
37
+ /** @param {import('./flag-evaluations').SerializedFlagEvaluationRoute} route */
38
+ function deserializeRoute (route) {
39
+ return {
40
+ url: new URL(route.url),
41
+ basePath: route.basePath,
42
+ headers: route.headers,
43
+ fallback: route.fallback && deserializeRoute(route.fallback),
44
+ onFallback: route.onFallback
45
+ ? () => port.postMessage({ type: 'route', id: route.id, status: 'fallback' })
46
+ : undefined,
47
+ onUnavailable: route.onUnavailable
48
+ ? () => port.postMessage({ type: 'route', id: route.id, status: 'unavailable' })
49
+ : undefined,
50
+ }
51
+ }
52
+
53
+ const route = deserializeRoute(workerData.route)
54
+ const consumer = new FlagEvaluationConsumer({ ...workerData.context, url: route.url }, route, {
55
+ onProcessed: count => Atomics.sub(state, 0, count),
56
+ onDelivered: count => Atomics.sub(state, 1, count),
57
+ onIdle: () => port.close(),
58
+ })
59
+ consumer.setEnabled(true, route)
60
+ port.on('message', message => {
61
+ switch (message.type) {
62
+ case 'batch':
63
+ for (const event of message.events) {
64
+ if (!consumer.enqueue(event)) Atomics.sub(state, 0, 1)
65
+ }
66
+ break
67
+ case 'enabled':
68
+ consumer.setEnabled(message.enabled, message.route && deserializeRoute(message.route))
69
+ break
70
+ case 'flush':
71
+ consumer.flush()
72
+ break
73
+ case 'close':
74
+ consumer.destroy()
75
+ break
76
+ }
77
+ })
@@ -0,0 +1,398 @@
1
+ 'use strict'
2
+
3
+ const { join } = require('node:path')
4
+
5
+ const { channel } = require('dc-polyfill')
6
+
7
+ const { FLAG_EVALUATION_FLUSH_INTERVAL, FLAG_EVALUATION_QUEUE_CAP } = require('../constants/constants')
8
+ const { EVP_PROXY_PATH_V2 } = require('../../evp_proxy/constants')
9
+ const { getEnvironmentVariables } = require('../../config/helper')
10
+ const log = require('../../log')
11
+ const { normalizeFlagEvaluationEvent, normalizeTargetingKey } = require('./flag-evaluation-pii')
12
+ const {
13
+ collectWorkerTelemetry, createWorkerState, recordDropped, recordTargetingKeyOmitted,
14
+ } = require('./flag-evaluation-telemetry')
15
+
16
+ const telemetryAppClosingCh = channel('datadog:telemetry:app-closing')
17
+ const flushCh = channel('ffe:writers:flush')
18
+
19
+ // Share clone/wakeup costs across event-loop turns without delaying sparse evaluations indefinitely.
20
+ const BATCH_SIZE = 64
21
+ const CONSENT_BATCH_SIZE = 8
22
+ const BATCH_DELAY_MS = 20
23
+ const permissionFlags = new Set([
24
+ '--permission', '--experimental-permission', '--permission-audit',
25
+ '--allow-addons', '--allow-child-process', '--allow-fs-read', '--allow-fs-write',
26
+ '--allow-ffi', '--allow-inspector', '--allow-net', '--allow-openssl-store', '--allow-wasi', '--allow-worker',
27
+ ])
28
+ const networkFlags = new Set([
29
+ '--dns-result-order', '--network-family-autoselection', '--enable-network-family-autoselection',
30
+ '--network-family-autoselection-attempt-timeout', '--max-http-header-size', '--insecure-http-parser',
31
+ // TLS version defaults are worker-local; other TLS/OpenSSL options may reject worker execArgv.
32
+ '--tls-min-v1.0', '--tls-min-v1.1', '--tls-min-v1.2', '--tls-min-v1.3',
33
+ '--tls-max-v1.2', '--tls-max-v1.3',
34
+ ])
35
+ const flagsWithValues = new Set([
36
+ '--allow-fs-read', '--allow-fs-write', '--dns-result-order', '--network-family-autoselection-attempt-timeout',
37
+ '--max-http-header-size',
38
+ ])
39
+
40
+ /** @param {string} [nodeOptions] */
41
+ function getWorkerArgs (nodeOptions = '') {
42
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins -- Feature detection also supports Node 18.
43
+ const permissionEnabled = Boolean(process.permission)
44
+ const args = []
45
+ let arg = ''
46
+ let quoted = false
47
+ // Match Node's NODE_OPTIONS parsing: spaces delimit arguments; double quotes allow spaces,
48
+ // and backslashes escape the next character only inside double quotes (not shell quoting).
49
+ for (let i = 0; i < nodeOptions.length; i++) {
50
+ const char = nodeOptions[i]
51
+ if (char === '\\' && quoted) {
52
+ if (++i === nodeOptions.length) throw new Error('Invalid escape in NODE_OPTIONS')
53
+ arg += nodeOptions[i]
54
+ } else if (char === '"') {
55
+ quoted = !quoted
56
+ } else if (char === ' ' && !quoted) {
57
+ if (arg) args.push(arg)
58
+ arg = ''
59
+ } else {
60
+ arg += char
61
+ }
62
+ }
63
+ if (quoted) throw new Error('Unterminated quote in NODE_OPTIONS')
64
+ if (arg) args.push(arg)
65
+ // Node processes environment options first, then command-line overrides.
66
+ args.push(...process.execArgv)
67
+ const workerArgs = []
68
+ for (let i = 0; i < args.length; i++) {
69
+ const flag = args[i].split('=')[0].replaceAll('_', '-').replace(/^--no-/, '--')
70
+ if (!networkFlags.has(flag) && !(permissionEnabled && permissionFlags.has(flag))) continue
71
+ workerArgs.push(args[i])
72
+ if (flagsWithValues.has(flag) && !args[i].includes('=')) {
73
+ workerArgs.push(args[++i])
74
+ }
75
+ }
76
+ return workerArgs
77
+ }
78
+
79
+ /** @typedef {import('./flag-evaluation-consumer').FlagEvaluationRoute} FlagEvaluationRoute */
80
+ /** @typedef {import('./flag-evaluation-aggregation').FlagEvaluationEvent} FlagEvaluationEvent */
81
+
82
+ /**
83
+ * @typedef {object} SerializedFlagEvaluationRoute
84
+ * @property {number} id
85
+ * @property {string} url
86
+ * @property {string} basePath
87
+ * @property {object} [headers]
88
+ * @property {boolean} onFallback
89
+ * @property {boolean} onUnavailable
90
+ * @property {SerializedFlagEvaluationRoute} [fallback]
91
+ */
92
+
93
+ /**
94
+ * @param {FlagEvaluationRoute} route
95
+ * @param {number} id - Generation used to reject reports from obsolete routes
96
+ */
97
+ function serializeRoute (route, id) {
98
+ // Live custom agents cannot cross isolates. The common request helper recreates env proxy agents in the worker.
99
+ if (route.agent) throw new Error('Custom EVP route agents are not supported in the flag evaluation worker')
100
+ return {
101
+ id,
102
+ url: route.url.href,
103
+ basePath: route.basePath,
104
+ headers: route.headers,
105
+ onFallback: typeof route.onFallback === 'function',
106
+ onUnavailable: typeof route.onUnavailable === 'function',
107
+ fallback: route.fallback ? serializeRoute(route.fallback, id) : undefined,
108
+ }
109
+ }
110
+
111
+ class FlagEvaluationsWriter {
112
+ #enabled = false
113
+ #discoveryPending = true
114
+ #flushPending = false
115
+ #closed = false
116
+ #failed = false
117
+ #worker
118
+ #state = createWorkerState()
119
+ #batch = []
120
+ #batchTimer
121
+ #periodic
122
+ #deadline
123
+ #destroyer
124
+ #onAppClosing
125
+ #onFlush
126
+ #route
127
+ #routeId = 0
128
+ #serializedRoute
129
+ #context
130
+ #logging
131
+ #failureReason = 'worker_failure'
132
+
133
+ /** @param {import('../../config/config-base')} config */
134
+ constructor (config) {
135
+ this.#route = { url: /** @type {URL} */ (config.url), basePath: EVP_PROXY_PATH_V2 }
136
+ this.#logging = { enabled: config.debug === true, level: config.logLevel }
137
+ this.#context = {
138
+ service: typeof config.service === 'string' ? config.service : '',
139
+ env: typeof config.env === 'string' ? config.env : undefined,
140
+ version: typeof config.version === 'string' ? config.version : undefined,
141
+ }
142
+ this.#destroyer = () => this.destroy()
143
+ // Telemetry registers its shutdown sender before providers. Collect existing worker metrics before that send.
144
+ // Metrics produced by the later final drain remain best-effort; this does not coordinate telemetry shutdown.
145
+ this.#onAppClosing = () => collectWorkerTelemetry(this.#state)
146
+ this.#onFlush = () => this.flush()
147
+ globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(this.#destroyer)
148
+ telemetryAppClosingCh.subscribe(this.#onAppClosing)
149
+ flushCh.subscribe(this.#onFlush)
150
+ }
151
+
152
+ hasCapacity () {
153
+ return this.getUnavailableReason() === undefined &&
154
+ Atomics.load(this.#state, 0) < FLAG_EVALUATION_QUEUE_CAP && Atomics.load(this.#state, 1) < 0x7F_FF_F0_00
155
+ }
156
+
157
+ /** @returns {'closed' | 'worker_failure' | 'unavailable' | undefined} */
158
+ getUnavailableReason () {
159
+ if (this.#closed) return 'closed'
160
+ if (this.#failed) return 'worker_failure'
161
+ return this.#enabled || this.#discoveryPending ? undefined : 'unavailable'
162
+ }
163
+
164
+ /**
165
+ * @param {boolean} enabled
166
+ * @param {FlagEvaluationRoute} [route]
167
+ */
168
+ setEnabled (enabled, route) {
169
+ if (this.#closed || this.#failed) return
170
+ const discoveryPending = this.#discoveryPending
171
+ this.#discoveryPending = false
172
+ this.#routeId++
173
+ if (route) this.#route = route
174
+ if (!enabled) {
175
+ this.#discardPartial('unavailable')
176
+ this.#post({ type: 'enabled', enabled: false })
177
+ this.#enabled = false
178
+ this.#flushPending = false
179
+ return
180
+ }
181
+ try {
182
+ this.#serializedRoute = serializeRoute(this.#route, this.#routeId)
183
+ this.#post({ type: 'enabled', enabled: true, route: this.#serializedRoute })
184
+ this.#enabled = !this.#failed
185
+ if (discoveryPending && this.#enabled) this.#drainDiscoveryQueue()
186
+ } catch {
187
+ this.#fail()
188
+ }
189
+ }
190
+
191
+ /** @param {FlagEvaluationEvent} event */
192
+ enqueue (event) {
193
+ if (!this.hasCapacity()) {
194
+ recordDropped(this.getUnavailableReason() ?? 'queue_overflow')
195
+ return false
196
+ }
197
+ const targetingKey = normalizeTargetingKey(event.targetingKey)
198
+ if (targetingKey === undefined && event.targetingKey !== undefined && event.targetingKey !== null) {
199
+ recordTargetingKeyOmitted()
200
+ }
201
+ const normalized = normalizeFlagEvaluationEvent(event, targetingKey)
202
+ const consent = normalized.observeFullEvaluationData
203
+ Atomics.add(this.#state, 0, 1)
204
+ Atomics.add(this.#state, 1, 1)
205
+ this.#batch.push(normalized)
206
+ // Discovery uses the same queue credits; no worker or timer is needed until a route is known.
207
+ if (this.#discoveryPending) return true
208
+ // A consented event flushes any batch of 8+, so a mixed batch contains at most 8 context snapshots.
209
+ if (this.#batch.length >= (consent ? CONSENT_BATCH_SIZE : BATCH_SIZE)) this.#sendBatch()
210
+ else if (this.#batchTimer === undefined) {
211
+ this.#batchTimer = setTimeout(() => this.#sendBatch(), BATCH_DELAY_MS)
212
+ this.#batchTimer.unref?.()
213
+ }
214
+ return true
215
+ }
216
+
217
+ flush () {
218
+ if (this.#closed || this.#failed) return
219
+ if (this.#discoveryPending) {
220
+ this.#flushPending = true
221
+ return
222
+ }
223
+ this.#sendBatch()
224
+ this.#post({ type: 'flush' })
225
+ collectWorkerTelemetry(this.#state)
226
+ }
227
+
228
+ destroy () {
229
+ if (this.#closed) return
230
+ if (this.#discoveryPending) this.#discardPartial('closed')
231
+ else this.#sendBatch()
232
+ this.#closed = true
233
+ this.#enabled = false
234
+ this.#cleanup()
235
+ if (!this.#worker || this.#failed) return
236
+ // Keep only a bounded final drain alive. An unresponsive worker can delay graceful
237
+ // exit by up to five seconds; unref'ing the deadline would not bypass the ref'd worker.
238
+ // Ordinary traffic and idle workers never keep the app alive.
239
+ this.#worker.ref()
240
+ this.#deadline = setTimeout(() => {
241
+ this.#failureReason = 'shutdown_timeout'
242
+ this.#fail()
243
+ }, 5000)
244
+ this.#post({ type: 'close' })
245
+ }
246
+
247
+ #sendBatch () {
248
+ this.#cancelBatchTimer()
249
+ if (this.#batch.length === 0) return
250
+ if (!this.#worker) {
251
+ this.#startWorker()
252
+ if (this.#failed) return
253
+ }
254
+ const events = this.#batch
255
+ this.#batch = []
256
+ this.#post({ type: 'batch', events })
257
+ }
258
+
259
+ #drainDiscoveryQueue () {
260
+ const events = this.#batch
261
+ this.#batch = []
262
+ // Keep the normal message limits when releasing a potentially queue-sized startup backlog.
263
+ for (const event of events) {
264
+ this.#batch.push(event)
265
+ if (this.#batch.length >= (event.observeFullEvaluationData ? CONSENT_BATCH_SIZE : BATCH_SIZE)) {
266
+ this.#sendBatch()
267
+ if (this.#failed) return
268
+ }
269
+ }
270
+ this.#sendBatch()
271
+ if (this.#flushPending) {
272
+ this.#flushPending = false
273
+ this.flush()
274
+ }
275
+ }
276
+
277
+ // Providers that never evaluate need no worker. A first batch during destroy still uses the bounded drain.
278
+ #startWorker () {
279
+ try {
280
+ const { Worker } = require('node:worker_threads')
281
+ // Intentionally use the tracer's supported-config filter (which retains non-DD/OTEL env).
282
+ // Strip application preloads, but retain permissions and supported HTTP/TLS/network settings.
283
+ const { NODE_OPTIONS, ...env } = getEnvironmentVariables()
284
+ const execArgv = getWorkerArgs(NODE_OPTIONS)
285
+ // PnP has no node_modules fallback: retain only its active resolver, not application preloads.
286
+ // eslint-disable-next-line n/no-missing-require -- Yarn supplies this virtual module only in PnP applications.
287
+ if (process.versions.pnp) execArgv.push('--require', require.resolve('pnpapi'))
288
+ this.#worker = new Worker(join(__dirname, 'flag-evaluation-worker.js'), {
289
+ name: 'dd-flag-evaluation',
290
+ execArgv,
291
+ env,
292
+ workerData: {
293
+ route: this.#serializedRoute, context: this.#context, state: this.#state.buffer, logging: this.#logging,
294
+ },
295
+ })
296
+ this.#worker.on('error', error => this.#fail(
297
+ error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND' ? 'missing_module' : 'worker_error'
298
+ ))
299
+ this.#worker.on('messageerror', () => this.#fail('message_error'))
300
+ this.#worker.on('message', message => {
301
+ if (message?.type === 'log') this.#onLogMessage(message)
302
+ else this.#onRouteMessage(message)
303
+ })
304
+ this.#worker.once('exit', () => this.#exited())
305
+ this.#worker.unref?.()
306
+ this.#periodic = setInterval(() => collectWorkerTelemetry(this.#state), FLAG_EVALUATION_FLUSH_INTERVAL)
307
+ this.#periodic.unref?.()
308
+ } catch {
309
+ this.#fail('startup_error')
310
+ }
311
+ }
312
+
313
+ /** @param {{ level: string, message: string }} message */
314
+ #onLogMessage ({ level, message }) {
315
+ if (typeof message !== 'string') return
316
+ // Relay diagnostics without introducing new error-telemetry reports.
317
+ if (level === 'error') log.errorWithoutTelemetry('%s', message)
318
+ else if (level === 'debug' || level === 'info' || level === 'warn') log[level]('%s', message)
319
+ }
320
+
321
+ /** @param {{ type: 'route', id: number, status: 'fallback' | 'unavailable' }} message */
322
+ #onRouteMessage (message) {
323
+ if (this.#closed || this.#failed || !this.#enabled ||
324
+ message?.type !== 'route' || message.id !== this.#routeId) return
325
+ const callback = message.status === 'fallback'
326
+ ? this.#route.onFallback
327
+ : message.status === 'unavailable' ? this.#route.onUnavailable : undefined
328
+ if (!callback) return
329
+ // Consume the transition once. The strategy may synchronously publish a replacement route.
330
+ this.#routeId++
331
+ try {
332
+ callback()
333
+ } catch {
334
+ this.#fail('route_error')
335
+ }
336
+ }
337
+
338
+ /** @param {object} message */
339
+ #post (message) {
340
+ if (!this.#worker || this.#failed) return
341
+ try {
342
+ this.#worker.postMessage(message)
343
+ } catch {
344
+ this.#fail()
345
+ }
346
+ }
347
+
348
+ #cancelBatchTimer () {
349
+ if (this.#batchTimer !== undefined) clearTimeout(this.#batchTimer)
350
+ this.#batchTimer = undefined
351
+ }
352
+
353
+ /** @param {string} reason */
354
+ #discardPartial (reason) {
355
+ this.#cancelBatchTimer()
356
+ const count = this.#batch.length
357
+ this.#batch = []
358
+ Atomics.sub(this.#state, 0, count)
359
+ Atomics.sub(this.#state, 1, count)
360
+ recordDropped(reason, count)
361
+ }
362
+
363
+ #cleanup () {
364
+ clearInterval(this.#periodic)
365
+ globalThis[Symbol.for('dd-trace')].beforeExitHandlers.delete(this.#destroyer)
366
+ telemetryAppClosingCh.unsubscribe(this.#onAppClosing)
367
+ flushCh.unsubscribe(this.#onFlush)
368
+ collectWorkerTelemetry(this.#state)
369
+ }
370
+
371
+ /** @param {string} [reason] - Internal diagnostic only; never include raw error messages or paths. */
372
+ #fail (reason = 'worker_error') {
373
+ if (this.#failed) return
374
+ this.#failed = true
375
+ this.#enabled = false
376
+ this.#cancelBatchTimer()
377
+ this.#batch = []
378
+ this.#cleanup()
379
+ log.warn('Flag evaluation counts disabled after worker failure (%s)', reason)
380
+ if (this.#worker) this.#worker.terminate()
381
+ else this.#exited()
382
+ }
383
+
384
+ #exited () {
385
+ clearTimeout(this.#deadline)
386
+ this.#failed = true
387
+ this.#enabled = false
388
+ this.#worker = undefined
389
+ this.#cancelBatchTimer()
390
+ this.#batch = []
391
+ this.#cleanup()
392
+ // Only settle ownership once the worker cannot mutate counters anymore.
393
+ recordDropped(this.#failureReason, Atomics.exchange(this.#state, 1, 0))
394
+ Atomics.store(this.#state, 0, 0)
395
+ }
396
+ }
397
+
398
+ module.exports = FlagEvaluationsWriter
@@ -34,7 +34,7 @@ function warnExposureDeliveryUnavailable () {
34
34
  /**
35
35
  * Preserves the one-time EVP v2 capability check for the Remote Configuration source.
36
36
  *
37
- * @param {import('../../config')} config - Tracer configuration object
37
+ * @param {import('../../config/config-base')} config - Tracer configuration object
38
38
  * @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
39
39
  */
40
40
  function setAgentStrategy (config, setWriterEnabledValue) {
@@ -64,7 +64,7 @@ function setAgentStrategy (config, setWriterEnabledValue) {
64
64
  * Local discovery is optional for delivery. A missing listener, discovery
65
65
  * error, or incompatible receiver selects direct intake when credentials exist.
66
66
  *
67
- * @param {import('../../config')} config - Tracer configuration object
67
+ * @param {import('../../config/config-base')} config - Tracer configuration object
68
68
  * @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
69
69
  */
70
70
  function setAgentlessStrategy (config, setWriterEnabledValue) {
@@ -169,8 +169,9 @@ function setAgentlessStrategy (config, setWriterEnabledValue) {
169
169
  /**
170
170
  * Applies one event-delivery strategy that can be shared by all Feature Flags writers.
171
171
  *
172
- * @param {import('../../config')} config - Tracer configuration object
173
- * @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
172
+ * @param {import('../../config/config-base')} config - Tracer configuration object
173
+ * @param {(enabled: boolean, route?: import('./flag-evaluations').FlagEvaluationRoute) => void} setWriterEnabledValue
174
+ * Callback to set the writer enabled state
174
175
  */
175
176
  function setEventDeliveryStrategy (config, setWriterEnabledValue) {
176
177
  if (config.featureFlags?.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE === 'agentless') {
@@ -93,6 +93,9 @@ class ContextManager {
93
93
  }
94
94
 
95
95
  bind (context, target) {
96
+ // Per the OTel contract, only functions are wrapped; other targets are returned as-is.
97
+ if (typeof target !== 'function') return target
98
+
96
99
  const self = this
97
100
  return function (...args) {
98
101
  return self.with(context, target, this, ...args)
@@ -2,7 +2,9 @@
2
2
 
3
3
  const api = require('@opentelemetry/api')
4
4
  const { AUTO_KEEP } = require('../../../../ext/priority')
5
+ const { updateOtelTraceState } = require('../otel-sampling')
5
6
  const DatadogSpanContext = require('../opentracing/span_context')
7
+ const TraceState = require('../opentracing/propagation/tracestate')
6
8
  const id = require('../id')
7
9
 
8
10
  function newContext () {
@@ -37,8 +39,10 @@ class SpanContext {
37
39
  }
38
40
 
39
41
  get traceState () {
40
- const ts = this._ddContext._tracestate
41
- return api.createTraceState(ts ? ts.toString() : '')
42
+ this._ddContext._ensureSamplingPriority()
43
+ const traceState = this._ddContext._tracestate?.clone() ?? new TraceState()
44
+ updateOtelTraceState(this._ddContext, traceState)
45
+ return api.createTraceState(traceState.toString())
42
46
  }
43
47
  }
44
48
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  const OtlpTransformerBase = require('../otlp/otlp_transformer_base')
4
4
  const { getProtobufTypes } = require('../otlp/protobuf_loader')
5
+ const { AUTO_KEEP } = require('../../../../../ext/priority')
5
6
  const { VERSION } = require('../../../../../version')
7
+ const { SAMPLING_PRIORITY_KEY } = require('../../constants')
6
8
  const id = require('../../id')
7
9
  const { eventTimeNano } = require('../../encode/tags-processors')
8
10
 
@@ -52,6 +54,7 @@ const TRACE_ID_128 = '_dd.p.tid'
52
54
  * @property {number} start - Start time in nanoseconds since epoch
53
55
  * @property {number} duration - Duration in nanoseconds
54
56
  * @property {DDSpanEvent[]} [span_events] - Span events
57
+ * @property {string} [trace_state] - W3C tracestate for OTLP export
55
58
  */
56
59
 
57
60
  // Map DD span.kind string values to OTLP SpanKind numeric values
@@ -161,11 +164,13 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
161
164
  #transformSpan (span, traceIdHigh) {
162
165
  const parentId = span.parent_id
163
166
  const links = this.#extractLinks(span.meta?.['_dd.span_links'])
167
+ const samplingPriority = span.metrics?.[SAMPLING_PRIORITY_KEY]
164
168
 
165
169
  return {
166
170
  traceId: span.trace_id.toTraceIdHex(traceIdHigh).padStart(32, '0'),
167
171
  spanId: this.#idToBytes(span.span_id, 8),
168
172
  parentSpanId: (parentId && !parentId.equals(ZERO_ID)) ? this.#idToBytes(parentId, 8) : undefined,
173
+ traceState: span.trace_state,
169
174
  name: span.resource,
170
175
  kind: this.#mapSpanKind(span.meta?.['span.kind']),
171
176
  startTimeUnixNano: span.start,
@@ -177,6 +182,7 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
177
182
  links: links.length ? links : undefined,
178
183
  droppedLinksCount: 0,
179
184
  status: this.#mapStatus(span),
185
+ flags: typeof samplingPriority === 'number' ? (samplingPriority >= AUTO_KEEP ? 1 : 0) : undefined,
180
186
  }
181
187
  }
182
188
 
@@ -8,6 +8,7 @@ const tracer = require('../../')
8
8
 
9
9
  const id = require('../id')
10
10
  const log = require('../log')
11
+ const { normalizeOtelTraceState } = require('../otel-sampling')
11
12
  const TraceState = require('../opentracing/propagation/tracestate')
12
13
  const SpanContext = require('./span_context')
13
14
  const Span = require('./span')
@@ -74,12 +75,12 @@ class Tracer {
74
75
  _convertOtelContextToDatadog (traceId, spanId, traceFlag, ts, meta = {}) {
75
76
  let origin = null
76
77
  let samplingPriority = traceFlag
78
+ let samplingMechanism
79
+ const traceStateValue = typeof ts?.serialize === 'function' ? ts.serialize() : ts?.traceparent
80
+ const traceState = TraceState.fromString(traceStateValue)
81
+ normalizeOtelTraceState(traceState)
77
82
 
78
- ts = ts?.traceparent
79
-
80
- if (ts) {
81
- // Use TraceState.fromString to parse the tracestate header
82
- const traceState = TraceState.fromString(ts)
83
+ if (traceStateValue) {
83
84
  let ddTraceStateData = null
84
85
 
85
86
  // Extract Datadog specific trace state data
@@ -89,29 +90,26 @@ class Tracer {
89
90
  })
90
91
 
91
92
  if (ddTraceStateData) {
92
- // Assuming ddTraceStateData is now a Map or similar structure containing Datadog trace state data
93
- // Extract values as needed, similar to the original logic
94
- const samplingPriorityTs = ddTraceStateData.get('s')
95
- origin = ddTraceStateData.get('o') ?? null
96
- // Convert Map to object for meta
97
- const otherPropagatedTags = Object.fromEntries(ddTraceStateData.entries())
98
-
99
- // Update meta and samplingPriority based on extracted values
100
- Object.assign(meta, otherPropagatedTags)
101
- // Guard against an undefined/empty `s:` field that would result in NaN.
102
- const tracestateSamplingPriority = samplingPriorityTs ? Math.trunc(samplingPriorityTs) : undefined
93
+ const priority = Number.parseInt(ddTraceStateData.get('s'), 10)
94
+ const tracestateSamplingPriority = Number.isInteger(priority) ? priority : undefined
95
+ origin = ddTraceStateData.get('o')?.replaceAll('~', '=') ?? null
96
+
97
+ const mechanism = Math.abs(Number.parseInt(ddTraceStateData.get('t.dm'), 10))
98
+ if (Number.isInteger(mechanism)) samplingMechanism = mechanism
99
+
103
100
  samplingPriority = getSamplingPriority(traceFlag, tracestateSamplingPriority, origin)
104
101
  } else {
105
- log.debug('No dd list member in tracestate from incoming request:', ts)
102
+ log.debug('No dd list member in tracestate from incoming request:', traceStateValue)
106
103
  }
107
104
  }
108
105
 
109
106
  const spanContext = new SpanContext({
110
- traceId: id(traceId, 16), spanId: id(), tags: meta, parentId: id(spanId, 16),
107
+ traceId: id(traceId, 16), spanId: id(), tags: meta, parentId: id(spanId, 16), tracestate: traceState,
111
108
  })
112
109
 
113
110
  spanContext._ddContext._sampling = { priority: samplingPriority }
114
111
  spanContext._ddContext._trace = { ...spanContext._ddContext._trace, origin }
112
+ if (samplingMechanism !== undefined) spanContext._ddContext._sampling.mechanism = samplingMechanism
115
113
  return spanContext
116
114
  }
117
115