dd-trace 6.9.0 → 6.11.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 (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -1,13 +1,20 @@
1
1
  'use strict'
2
2
 
3
3
  const log = require('../../log')
4
+ const { ExternalExperiment } = require('./experiment')
5
+
6
+ const NOOP_EXPERIMENT_ID = '00000000-0000-0000-0000-000000000000'
7
+ const NOOP_SPAN_ID = '0000000000000000'
8
+ const NOOP_TRACE_ID = '00000000000000000000000000000000'
4
9
 
5
10
  class NoopDataset {
6
11
  #name
12
+ #description
7
13
  #records
8
14
 
9
15
  constructor (name = '', options = {}) {
10
16
  this.#name = name
17
+ this.#description = typeof options === 'string' ? options : (options.description ?? '')
11
18
  this.#records = (typeof options === 'string' ? [] : (options.records ?? [])).map(record => ({
12
19
  id: record.id ?? null,
13
20
  input: record.inputData,
@@ -21,6 +28,20 @@ class NoopDataset {
21
28
  return this
22
29
  }
23
30
 
31
+ update (index, fields) {
32
+ const record = this.#records[index]
33
+ if (record == null) return this
34
+ if (Object.hasOwn(fields, 'input')) record.input = fields.input
35
+ if (Object.hasOwn(fields, 'expectedOutput')) record.expectedOutput = fields.expectedOutput ?? null
36
+ if (Object.hasOwn(fields, 'metadata')) record.metadata = fields.metadata ?? {}
37
+ return this
38
+ }
39
+
40
+ delete (index) {
41
+ this.#records.splice(index, 1)
42
+ return this
43
+ }
44
+
24
45
  push () {
25
46
  return Promise.resolve({ pushedCount: 0, totalCount: 0 })
26
47
  }
@@ -29,6 +50,10 @@ class NoopDataset {
29
50
  return this.#name
30
51
  }
31
52
 
53
+ description () {
54
+ return this.#description
55
+ }
56
+
32
57
  id () {
33
58
  return null
34
59
  }
@@ -60,9 +85,11 @@ class NoopDataset {
60
85
 
61
86
  class NoopExperiment {
62
87
  #name
88
+ #external
63
89
 
64
- constructor (name = '') {
90
+ constructor (name = '', external = false) {
65
91
  this.#name = name
92
+ this.#external = external
66
93
  }
67
94
 
68
95
  name () {
@@ -70,7 +97,7 @@ class NoopExperiment {
70
97
  }
71
98
 
72
99
  experimentId () {
73
- return null
100
+ return this.#external ? NOOP_EXPERIMENT_ID : null
74
101
  }
75
102
 
76
103
  url () {
@@ -80,6 +107,32 @@ class NoopExperiment {
80
107
  run () {
81
108
  return Promise.resolve({ experimentId: null, rows: [], url: null })
82
109
  }
110
+
111
+ /**
112
+ * @returns {Promise<{experimentId: string, spanId: string, traceId: string, url: null}>}
113
+ */
114
+ submitSpan () {
115
+ return Promise.resolve({
116
+ experimentId: NOOP_EXPERIMENT_ID,
117
+ spanId: NOOP_SPAN_ID,
118
+ traceId: NOOP_TRACE_ID,
119
+ url: null,
120
+ })
121
+ }
122
+
123
+ /**
124
+ * @returns {Promise<void>}
125
+ */
126
+ submitEvaluationMetrics () {
127
+ return Promise.resolve()
128
+ }
129
+
130
+ /**
131
+ * @returns {Promise<void>}
132
+ */
133
+ close () {
134
+ return Promise.resolve()
135
+ }
83
136
  }
84
137
 
85
138
  // No-op Experiments used when LLM Observability is disabled or the API/APP keys
@@ -87,9 +140,11 @@ class NoopExperiment {
87
140
  // throwing, so intentionally disabled experiments remain graceful.
88
141
  class NoopExperiments {
89
142
  #reason
143
+ #startExperiment
90
144
 
91
- constructor (reason) {
145
+ constructor (reason, options = {}) {
92
146
  this.#reason = reason || 'LLMObs experiments are not available'
147
+ this.#startExperiment = options.startExperiment
93
148
  }
94
149
 
95
150
  #warn () {
@@ -110,6 +165,19 @@ class NoopExperiments {
110
165
  this.#warn()
111
166
  return new NoopExperiment(options.name)
112
167
  }
168
+
169
+ /**
170
+ * @param {object} options
171
+ * @returns {Promise<ExternalExperiment>}
172
+ */
173
+ startExperiment (options = {}) {
174
+ if (this.#startExperiment !== undefined && options.projectName) {
175
+ return this.#startExperiment(options)
176
+ }
177
+
178
+ this.#warn()
179
+ return Promise.resolve(new ExternalExperiment(new NoopExperiment(options.name, true)))
180
+ }
113
181
  }
114
182
 
115
183
  module.exports = NoopExperiments
@@ -13,6 +13,7 @@ class Row {
13
13
  this.expectedOutput = fields.expectedOutput
14
14
  this.errorType = fields.errorType
15
15
  this.errorMessage = fields.errorMessage
16
+ this.errorStack = fields.errorStack
16
17
  this.evaluations = fields.evaluations
17
18
  this.evaluationErrors = fields.evaluationErrors
18
19
  }
@@ -178,12 +178,12 @@ function buildTags (userTags, autoTags) {
178
178
  }
179
179
 
180
180
  /**
181
- * @param {Record<string, unknown> | undefined} userTags
182
- * @param {Record<string, unknown>} autoTags
181
+ * @param {Record<string, unknown> | undefined} baseTags
182
+ * @param {Record<string, unknown> | undefined} overrideTags
183
183
  * @returns {Record<string, unknown>}
184
184
  */
185
- function buildExperimentTagObject (userTags, autoTags) {
186
- return userTags ? { ...userTags, ...autoTags } : { ...autoTags }
185
+ function mergeTags (baseTags, overrideTags) {
186
+ return { ...baseTags, ...overrideTags }
187
187
  }
188
188
 
189
189
  /**
@@ -195,6 +195,40 @@ function sleep (ms) {
195
195
  return new Promise((resolve) => setTimeout(resolve, ms))
196
196
  }
197
197
 
198
+ /**
199
+ * @param {unknown} value
200
+ * @param {number} fallback
201
+ * @returns {number}
202
+ */
203
+ function timestampMs (value, fallback = Date.now()) {
204
+ if (value === null || value === undefined) return fallback
205
+ if (value instanceof Date) {
206
+ const timestamp = value.getTime()
207
+ return Number.isFinite(timestamp) ? timestamp : fallback
208
+ }
209
+ if (typeof value === 'number' && Number.isFinite(value)) return value
210
+ const parsed = Date.parse(String(value))
211
+ return Number.isFinite(parsed) ? parsed : fallback
212
+ }
213
+
214
+ /**
215
+ * @param {{ durationMs?: unknown, completedAt?: unknown }} row
216
+ * @param {number} startMs
217
+ * @returns {number}
218
+ */
219
+ function durationNs (row, startMs) {
220
+ if (typeof row.durationMs === 'number' && Number.isFinite(row.durationMs)) {
221
+ return Math.max(0, Math.round(row.durationMs * 1e6))
222
+ }
223
+
224
+ if (row.completedAt !== undefined) {
225
+ const completedMs = timestampMs(row.completedAt, startMs)
226
+ return Math.max(0, Math.round((completedMs - startMs) * 1e6))
227
+ }
228
+
229
+ return 0
230
+ }
231
+
198
232
  /**
199
233
  * @param {Record<string, unknown> | undefined} recordMetadata
200
234
  * @param {Record<string, unknown>} config
@@ -207,14 +241,16 @@ function buildSpanMetadata (recordMetadata, config) {
207
241
  }
208
242
 
209
243
  module.exports = {
210
- buildExperimentTagObject,
211
244
  buildSpanMetadata,
212
245
  buildTags,
246
+ durationNs,
213
247
  hasEntries,
214
248
  inferMetricType,
249
+ mergeTags,
215
250
  normalizeEvaluators,
216
251
  normalizeJsonMetricValue,
217
252
  sleep,
218
253
  stringify,
254
+ timestampMs,
219
255
  validateEvaluatorName,
220
256
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
+ const { readDatadogTags, writeDatadogTags } = require('../carrier')
5
6
  const log = require('../log')
6
7
  const { DD_MAJOR } = require('../../../../version')
7
8
  const startupLogs = require('../startup-log')
@@ -12,6 +13,8 @@ const {
12
13
  PROPAGATED_ML_APP_KEY,
13
14
  PROPAGATED_PARENT_ID_KEY,
14
15
  PROPAGATED_SESSION_ID_KEY,
16
+ PROPAGATED_PARENT_AGENT_ID_KEY,
17
+ PROPAGATED_PARENT_AGENT_NAME_KEY,
15
18
  SAMPLE_RATE,
16
19
  SAMPLING_DECISION,
17
20
  PROPAGATED_SAMPLE_RATE_KEY,
@@ -20,6 +23,7 @@ const {
20
23
  PROPAGATED_TRACE_ID_KEY,
21
24
  } = require('./constants/tags')
22
25
  const { storage } = require('./storage')
26
+ const { agentNameWireSafe, appendOptionalPropagatedTag, resolveAgentAttribution, stripTagsetEntry } = require('./util')
23
27
  const telemetry = require('./telemetry')
24
28
  const LLMObsSpanProcessor = require('./span_processor')
25
29
  const LLMObsEvalMetricsWriter = require('./writers/evaluations')
@@ -147,9 +151,17 @@ function handleLLMObsInjection ({ carrier }) {
147
151
 
148
152
  if (!parentId && !mlApp && samplingDecision == null && !sessionId && !propagatedTraceId) return
149
153
 
154
+ // Propagate the nearest agent so spans in the downstream process attribute correctly. When the
155
+ // active span sits under a distributed agent, `resolveAgentAttribution` inherits the propagated
156
+ // id/name already on the parent's registry entry, so the chain survives multiple hops. Resolved
157
+ // after the bail-out above so we don't allocate when there is nothing to inject.
158
+ const { name: parentAgentName, spanId: parentAgentSpanId } = resolveAgentAttribution(
159
+ mlObsSpanTags, parent
160
+ )
161
+
150
162
  // `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*`
151
163
  // tags, so it may be undefined here — coalesce before appending.
152
- const existing = carrier['x-datadog-tags']
164
+ const existing = readDatadogTags(carrier)
153
165
  let tags = existing || ''
154
166
  if (parentId) tags += `${tags ? ',' : ''}${PROPAGATED_PARENT_ID_KEY}=${parentId}`
155
167
  if (mlApp) tags += `${tags ? ',' : ''}${PROPAGATED_ML_APP_KEY}=${mlApp}`
@@ -157,7 +169,27 @@ function handleLLMObsInjection ({ carrier }) {
157
169
  if (sampleRate != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLE_RATE_KEY}=${sampleRate}`
158
170
  if (samplingDecision != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLING_DECISION_KEY}=${samplingDecision}`
159
171
  if (propagatedTraceId != null) tags += `${tags ? ',' : ''}${PROPAGATED_TRACE_ID_KEY}=${propagatedTraceId}`
160
- if (tags !== existing) carrier['x-datadog-tags'] = tags
172
+ // When a local agent attribution is resolved, strip any stale upstream pagent entries that
173
+ // `_injectTags` may have already written into the carrier (it propagates all `_dd.p.*` from
174
+ // `_trace.tags`). This ensures the downstream sees a consistent id-only or id+name pair
175
+ // rather than a mix from different hops. The id is always digit-safe; an unsafe name is wiped.
176
+ if (parentAgentSpanId) {
177
+ tags = stripTagsetEntry(tags, PROPAGATED_PARENT_AGENT_ID_KEY)
178
+ tags = stripTagsetEntry(tags, PROPAGATED_PARENT_AGENT_NAME_KEY)
179
+ }
180
+ const maxLength = globalTracerConfig.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH
181
+ const tagsWithId = appendOptionalPropagatedTag(
182
+ tags, PROPAGATED_PARENT_AGENT_ID_KEY, parentAgentSpanId, null, maxLength
183
+ )
184
+ // Only append the name when the id fit: a name without an id is unresolvable by the backend.
185
+ if (tagsWithId === tags) {
186
+ tags = tagsWithId
187
+ } else {
188
+ tags = appendOptionalPropagatedTag(
189
+ tagsWithId, PROPAGATED_PARENT_AGENT_NAME_KEY, parentAgentName, agentNameWireSafe, maxLength
190
+ )
191
+ }
192
+ if (tags !== existing) writeDatadogTags(carrier, tags)
161
193
  }
162
194
 
163
195
  function handleFlush () {
@@ -228,6 +228,7 @@ function getJsonStringValue (str, defaultValue) {
228
228
  function getModelMetadata (tags) {
229
229
  /** @type {Record<string, unknown>} */
230
230
  const modelMetadata = {}
231
+ let hasModelMetadata = false
231
232
  for (const tag of Object.keys(tags)) {
232
233
  const isModelMetadata = tag.startsWith(VERCEL_AI_MODEL_METADATA_PREFIX)
233
234
  if (isModelMetadata) {
@@ -235,6 +236,7 @@ function getModelMetadata (tags) {
235
236
  const metadataKey = lastCommaPosition === -1 ? tag : tag.slice(lastCommaPosition + 1)
236
237
  if (metadataKey && MODEL_METADATA_KEYS.has(metadataKey)) {
237
238
  modelMetadata[metadataKey] = tags[tag]
239
+ hasModelMetadata = true
238
240
  }
239
241
  } else {
240
242
  const isTelemetryMetadata = tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)
@@ -242,12 +244,13 @@ function getModelMetadata (tags) {
242
244
  const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
243
245
  if (metadataKey) {
244
246
  modelMetadata[metadataKey] = tags[tag]
247
+ hasModelMetadata = true
245
248
  }
246
249
  }
247
250
  }
248
251
  }
249
252
 
250
- return Object.keys(modelMetadata).length ? modelMetadata : null
253
+ return hasModelMetadata ? modelMetadata : null
251
254
  }
252
255
 
253
256
  /**
@@ -259,6 +262,7 @@ function getModelMetadata (tags) {
259
262
  function getGenerationMetadata (tags) {
260
263
  /** @type {Record<string, unknown>} */
261
264
  const metadata = {}
265
+ let hasMetadata = false
262
266
 
263
267
  for (const tag of Object.keys(tags)) {
264
268
  const isGenerationMetadata = tag.startsWith(VERCEL_AI_GENERATION_METADATA_PREFIX)
@@ -270,18 +274,20 @@ function getGenerationMetadata (tags) {
270
274
 
271
275
  const settingValue = tags[tag]
272
276
  metadata[settingKey] = settingValue
277
+ hasMetadata = true
273
278
  } else {
274
279
  const isTelemetryMetadata = tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)
275
280
  if (isTelemetryMetadata) {
276
281
  const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
277
282
  if (metadataKey) {
278
283
  metadata[metadataKey] = tags[tag]
284
+ hasMetadata = true
279
285
  }
280
286
  }
281
287
  }
282
288
  }
283
289
 
284
- return Object.keys(metadata).length ? metadata : null
290
+ return hasMetadata ? metadata : null
285
291
  }
286
292
 
287
293
  /**
@@ -307,6 +313,7 @@ function getGenerationMetadataFromEvent (event) {
307
313
  metadata[transformedKey] = value
308
314
  }
309
315
 
316
+ // eslint-disable-next-line no-restricted-syntax -- manual tracking would duplicate Object.assign semantics
310
317
  return Object.keys(metadata).length ? metadata : null
311
318
  }
312
319
 
@@ -432,6 +439,7 @@ function getLlmObsSpanName (operation, functionId) {
432
439
  */
433
440
  function getTelemetryMetadata (tags) {
434
441
  const metadata = {}
442
+ let hasMetadata = false
435
443
 
436
444
  for (const tag of Object.keys(tags)) {
437
445
  if (!tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)) continue
@@ -439,10 +447,11 @@ function getTelemetryMetadata (tags) {
439
447
  const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
440
448
  if (metadataKey) {
441
449
  metadata[metadataKey] = tags[tag]
450
+ hasMetadata = true
442
451
  }
443
452
  }
444
453
 
445
- return Object.keys(metadata).length ? metadata : null
454
+ return hasMetadata ? metadata : null
446
455
  }
447
456
 
448
457
  module.exports = {
@@ -84,9 +84,11 @@ class LLMObsPlugin extends TracingPlugin {
84
84
  }
85
85
 
86
86
  configure (config) {
87
- // we do not want to enable any LLMObs plugins if it is disabled on the tracer
87
+ // we do not want to enable any LLMObs plugins if it is disabled on the tracer, or if the
88
+ // integration opted out via `tracer.use(<name>, { llmobs: false })`. Opting out only disables
89
+ // the LLMObs layer: the integration keeps emitting APM spans and propagating trace context.
88
90
  const llmobsEnabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
89
- if (llmobsEnabled === false) {
91
+ if (llmobsEnabled === false || config?.llmobs === false) {
90
92
  config = typeof config === 'boolean' ? false : { ...config, enabled: false } // override to false
91
93
  }
92
94
  super.configure(config)
@@ -27,6 +27,8 @@ const {
27
27
  ML_APP,
28
28
  TAGS,
29
29
  PARENT_ID_KEY,
30
+ PARENT_AGENT_NAME,
31
+ PARENT_AGENT_SPAN_ID,
30
32
  SESSION_ID,
31
33
  NAME,
32
34
  INPUT_PROMPT,
@@ -157,6 +159,20 @@ class LLMObsSpanProcessor {
157
159
  this.#addObject(mlObsTags[TOOL_DEFINITIONS], meta.tool_definitions)
158
160
  }
159
161
 
162
+ // Surface the agent attribution resolved at registration, but only on spans that actually
163
+ // have an agent ancestor. The id is always present in that case; the name may be missing when
164
+ // it arrived id-only over distributed propagation (older/unsafe upstream). Emit the name as
165
+ // explicit `null` then, matching the cross-language wire shape (dd-trace-py sends null, not an
166
+ // absent key) so the shared backend and system-tests see the same payload.
167
+ const parentAgentName = mlObsTags[PARENT_AGENT_NAME]
168
+ const parentAgentSpanId = mlObsTags[PARENT_AGENT_SPAN_ID]
169
+ if (parentAgentName != null || parentAgentSpanId != null) {
170
+ meta.agent_attribution = {
171
+ pagent_name: parentAgentName ?? null,
172
+ pagent_span_id: parentAgentSpanId,
173
+ }
174
+ }
175
+
160
176
  const llmObsSpan = new LLMObservabilitySpan(spanKind)
161
177
 
162
178
  if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
@@ -23,7 +23,11 @@ const {
23
23
  OUTPUT_MESSAGES,
24
24
  TAGS,
25
25
  NAME,
26
+ PARENT_AGENT_NAME,
27
+ PARENT_AGENT_SPAN_ID,
26
28
  PROPAGATED_PARENT_ID_KEY,
29
+ PROPAGATED_PARENT_AGENT_ID_KEY,
30
+ PROPAGATED_PARENT_AGENT_NAME_KEY,
27
31
  ROOT_PARENT_ID,
28
32
  CACHE_READ_INPUT_TOKENS_METRIC_KEY,
29
33
  CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
@@ -57,6 +61,7 @@ const {
57
61
  const { storage } = require('./storage')
58
62
  const {
59
63
  findGenAIAncestorSpanId,
64
+ resolveAgentAttribution,
60
65
  validateCostTags,
61
66
  writeBridgeTags,
62
67
  validateToolDefinitions,
@@ -173,6 +178,7 @@ class LLMObsTagger {
173
178
  this._setTag(span, PARENT_ID_KEY, parentId)
174
179
 
175
180
  this.#tagSamplingDecision(span, parent)
181
+ this.#tagAgentAttribution(span, parent)
176
182
 
177
183
  // apply annotation context
178
184
  const annotationContext = storage.getStore()?.annotationContext
@@ -229,6 +235,34 @@ class LLMObsTagger {
229
235
  if (samplingDecision != null) this._setTag(span, SAMPLING_DECISION, samplingDecision)
230
236
  }
231
237
 
238
+ /**
239
+ * Store the nearest agent ancestor on the span so it can be surfaced as
240
+ * `meta.agent_attribution` at finish. Resolved once here, at registration, so downstream
241
+ * children inherit it with a single lookup rather than walking the ancestor chain.
242
+ *
243
+ * @param {import('../opentracing/span')} span
244
+ * @param {import('../opentracing/span')} [parent] the LLMObs parent span, if any
245
+ */
246
+ // TODO: spans whose kind changes after registration (e.g. claude-agent-sdk tools promoted to
247
+ // sub-agents) will not retroactively update already-finished children's attribution. Follow up.
248
+ #tagAgentAttribution (span, parent) {
249
+ let name, spanId
250
+ if (registry.has(parent)) {
251
+ // Local LLMObs parent: attribute to it if it is an agent, else inherit its resolution.
252
+ ({ name, spanId } = resolveAgentAttribution(registry.get(parent), parent))
253
+ } else if (span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY]) {
254
+ // Distributed LLMObs parent: inherit the nearest agent propagated from upstream. The
255
+ // name may be absent when the upstream hop ran an older SDK or its name was not
256
+ // wire-safe; the id-only case is expected and the backend resolves the name by span id.
257
+ const traceTags = span.context()._trace.tags
258
+ name = traceTags[PROPAGATED_PARENT_AGENT_NAME_KEY]
259
+ spanId = traceTags[PROPAGATED_PARENT_AGENT_ID_KEY]
260
+ }
261
+
262
+ if (name != null) this._setTag(span, PARENT_AGENT_NAME, name)
263
+ if (spanId != null) this._setTag(span, PARENT_AGENT_SPAN_ID, spanId)
264
+ }
265
+
232
266
  // TODO: similarly for the following `tag` methods,
233
267
  // how can we transition from a span weakmap to core API functionality
234
268
  tagLLMIO (span, inputData, outputData) {
@@ -696,6 +730,59 @@ class LLMObsTagger {
696
730
  return filteredAudioParts
697
731
  }
698
732
 
733
+ // Image counterpart of #filterAudioParts, with the same wire shape and validation.
734
+ #filterImageParts (imageParts) {
735
+ if (!Array.isArray(imageParts)) {
736
+ imageParts = [imageParts]
737
+ }
738
+
739
+ const filteredImageParts = []
740
+ for (const imagePart of imageParts) {
741
+ if (imagePart == null || typeof imagePart !== 'object') {
742
+ this.#handleFailure('Image part must be an object.', 'invalid_io_messages')
743
+ continue
744
+ }
745
+
746
+ const { mimeType, content, attachmentKey } = imagePart
747
+
748
+ if (typeof mimeType !== 'string' || !mimeType) {
749
+ this.#handleFailure('Image part mimeType must be a non-empty string.', 'invalid_io_messages')
750
+ continue
751
+ }
752
+
753
+ if (content == null && attachmentKey == null) {
754
+ this.#handleFailure("Image part must have either 'content' or 'attachmentKey'.", 'invalid_io_messages')
755
+ continue
756
+ }
757
+
758
+ if (content != null && attachmentKey != null) {
759
+ this.#handleFailure(
760
+ "Image part must have only one of 'content' or 'attachmentKey', not both.", 'invalid_io_messages'
761
+ )
762
+ continue
763
+ }
764
+
765
+ const imagePartObj = { mime_type: mimeType }
766
+
767
+ if (content == null) {
768
+ if (typeof attachmentKey !== 'string') {
769
+ this.#handleFailure('Image part attachmentKey must be a string.', 'invalid_io_messages')
770
+ continue
771
+ }
772
+ imagePartObj.attachment_key = attachmentKey
773
+ } else {
774
+ if (typeof content !== 'string') {
775
+ this.#handleFailure('Image part content must be a base64-encoded string.', 'invalid_io_messages')
776
+ continue
777
+ }
778
+ imagePartObj.content = content
779
+ }
780
+
781
+ filteredImageParts.push(imagePartObj)
782
+ }
783
+ return filteredImageParts
784
+ }
785
+
699
786
  #tagMessages (span, data, key) {
700
787
  if (!data) {
701
788
  return
@@ -723,6 +810,7 @@ class LLMObsTagger {
723
810
  toolResults,
724
811
  toolId,
725
812
  audioParts,
813
+ imageParts,
726
814
  } = message
727
815
  const messageObj = {}
728
816
 
@@ -764,6 +852,14 @@ class LLMObsTagger {
764
852
  }
765
853
  }
766
854
 
855
+ if (imageParts != null) {
856
+ const filteredImageParts = this.#filterImageParts(imageParts)
857
+
858
+ if (filteredImageParts.length) {
859
+ messageObj.image_parts = filteredImageParts
860
+ }
861
+ }
862
+
767
863
  if (toolId) {
768
864
  if (role === 'tool') {
769
865
  condition = this.#tagConditionalString(toolId, 'Tool ID', messageObj, 'tool_id') && condition
@@ -6,6 +6,10 @@ const {
6
6
  LLMOBS_PARENT_ID_BRIDGE_KEY,
7
7
  LLMOBS_TRACE_ID_BRIDGE_KEY,
8
8
  SPAN_KINDS,
9
+ SPAN_KIND,
10
+ NAME,
11
+ PARENT_AGENT_NAME,
12
+ PARENT_AGENT_SPAN_ID,
9
13
  } = require('./constants/tags')
10
14
 
11
15
  const DECIMAL_TRACE_ID_REGEX = /^\d+$/
@@ -294,6 +298,76 @@ function getFunctionArguments (fn, args = []) {
294
298
  }
295
299
  }
296
300
 
301
+ // The `x-datadog-tags` tagset encoder rejects commas (the entry delimiter) and any byte
302
+ // outside 0x20-0x7E, and a raise there drops the ENTIRE header (taking ml_app,
303
+ // llmobs_trace_id, parent_id, sampling with it). An agent name is arbitrary user text, so it
304
+ // must be skipped rather than sanitized when unsafe: only the digit-safe id then propagates and
305
+ // the backend resolves the real name by span id. `=` is legal in tagset values (illegal only in
306
+ // keys), so a name containing `=` is safe and must not be dropped.
307
+ /**
308
+ * @param {string} name
309
+ * @returns {boolean}
310
+ */
311
+ function agentNameWireSafe (name) {
312
+ // Conservative slice of the 512B shared tagset budget, mirroring dd-trace-py.
313
+ if (Buffer.byteLength(name, 'utf8') > 256) return false
314
+ for (let index = 0; index < name.length; index++) {
315
+ const code = name.charCodeAt(index)
316
+ if (code < 0x20 || code > 0x7E || code === 0x2C /* comma */) return false
317
+ }
318
+ return true
319
+ }
320
+
321
+ /**
322
+ * Resolve the nearest agent that a *child* of `span` should be attributed to.
323
+ *
324
+ * If `span` is itself an agent, the child attributes directly to `span`. Otherwise `span`
325
+ * already resolved its own nearest agent at registration, so the child inherits that with a
326
+ * single lookup rather than walking the ancestor chain. An agent never attributes itself.
327
+ *
328
+ * @param {Record<string, unknown> | undefined} tags - Registry entry for the parent span.
329
+ * @param {import('../opentracing/span')} [span] - The parent span itself (needed for span id / name).
330
+ * @returns {{ name: string | undefined, spanId: string | undefined }}
331
+ */
332
+ function resolveAgentAttribution (tags, span) {
333
+ if (!tags) return { name: undefined, spanId: undefined }
334
+ if (tags[SPAN_KIND] === 'agent') {
335
+ return { name: tags[NAME] || span._name, spanId: span.context().toSpanId() }
336
+ }
337
+ return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID] }
338
+ }
339
+
340
+ /**
341
+ * Removes all `key=value` entries for the given key from a comma-separated tagset string.
342
+ *
343
+ * @param {string} tags - Existing tagset string (may be empty).
344
+ * @param {string} key
345
+ * @returns {string}
346
+ */
347
+ function stripTagsetEntry (tags, key) {
348
+ if (!tags.includes(key)) return tags
349
+ return tags.split(',').filter(entry => !entry.startsWith(`${key}=`)).join(',')
350
+ }
351
+
352
+ /**
353
+ * Appends `key=value` to the tagset string with a comma separator, but only when `value` is
354
+ * truthy, passes the optional `safeguard` predicate, and fits within `maxTagSetLength`. Returns
355
+ * the original `tags` unchanged when the value is absent, unsafe, or would overflow the budget.
356
+ *
357
+ * @param {string} tags - Existing tagset string (may be empty).
358
+ * @param {string} key
359
+ * @param {string | undefined} value
360
+ * @param {((v: string) => boolean) | null} [safeguard]
361
+ * @param {number} [maxTagSetLength]
362
+ * @returns {string}
363
+ */
364
+ function appendOptionalPropagatedTag (tags, key, value, safeguard, maxTagSetLength) {
365
+ if (!value || (safeguard && !safeguard(value))) return tags
366
+ const entry = `${tags ? ',' : ''}${key}=${value}`
367
+ if (maxTagSetLength != null && tags.length + entry.length > maxTagSetLength) return tags
368
+ return `${tags}${entry}`
369
+ }
370
+
297
371
  function spanHasError (span) {
298
372
  const spanContext = span.context()
299
373
  return !!(spanContext.getTag('error') || spanContext.getTag('error.type'))
@@ -443,6 +517,8 @@ function formatAudioPart (data, mimeType) {
443
517
  }
444
518
 
445
519
  module.exports = {
520
+ agentNameWireSafe,
521
+ appendOptionalPropagatedTag,
446
522
  audioMimeTypeFromFormat,
447
523
  encodeUnicode,
448
524
  findGenAIAncestorSpanId,
@@ -450,6 +526,8 @@ module.exports = {
450
526
  llmObsTraceIdToWire,
451
527
  normalizeLlmObsTraceId,
452
528
  formatAudioPart,
529
+ resolveAgentAttribution,
530
+ stripTagsetEntry,
453
531
  validateCostTags,
454
532
  validateKind,
455
533
  getFunctionArguments,