dd-trace 6.6.0 → 6.8.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 (209) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +8 -31
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +84 -22
  20. package/ci/test-optimization-validation/generated-test-contract.js +281 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +815 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/offline-output.js +6 -6
  28. package/ci/test-optimization-validation/plan-writer.js +243 -984
  29. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  30. package/ci/test-optimization-validation/redaction.js +2 -1
  31. package/ci/test-optimization-validation/report-writer.js +391 -1357
  32. package/ci/test-optimization-validation/result-semantics.js +86 -0
  33. package/ci/test-optimization-validation/runner-command.js +249 -0
  34. package/ci/test-optimization-validation/runner-contract.js +664 -0
  35. package/ci/test-optimization-validation/safe-files.js +4 -4
  36. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  39. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  40. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  41. package/ci/test-optimization-validation/scenarios/test-management.js +6 -5
  42. package/ci/test-optimization-validation/source-text.js +87 -0
  43. package/ci/test-optimization-validation/test-output.js +12 -22
  44. package/ci/vitest-no-worker-init-setup.mjs +348 -51
  45. package/ext/tags.d.ts +1 -0
  46. package/index.d.ts +84 -13
  47. package/package.json +10 -4
  48. package/packages/datadog-core/src/utils/src/kebabcase.js +8 -2
  49. package/packages/datadog-esbuild/index.js +4 -2
  50. package/packages/datadog-esbuild/src/utils.js +1 -1
  51. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +4 -4
  52. package/packages/datadog-instrumentations/src/cucumber.js +9 -6
  53. package/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +10 -0
  54. package/packages/datadog-instrumentations/src/helpers/hook.js +19 -2
  55. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  56. package/packages/datadog-instrumentations/src/helpers/register.js +9 -3
  57. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +1 -0
  58. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  59. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  60. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +57 -0
  61. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +50 -18
  62. package/packages/datadog-instrumentations/src/helpers/router-helper.js +1 -1
  63. package/packages/datadog-instrumentations/src/helpers/shared-utils.js +39 -0
  64. package/packages/datadog-instrumentations/src/http/client.js +1 -3
  65. package/packages/datadog-instrumentations/src/jest.js +8 -14
  66. package/packages/datadog-instrumentations/src/mocha/common.js +1 -3
  67. package/packages/datadog-instrumentations/src/mocha/main.js +2 -42
  68. package/packages/datadog-instrumentations/src/mocha/utils.js +68 -2
  69. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +19 -0
  70. package/packages/datadog-instrumentations/src/mocha/worker.js +446 -42
  71. package/packages/datadog-instrumentations/src/moleculer/client.js +1 -1
  72. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  73. package/packages/datadog-instrumentations/src/playwright.js +3 -2
  74. package/packages/datadog-instrumentations/src/rhea.js +1 -1
  75. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +525 -56
  76. package/packages/datadog-instrumentations/src/vitest-main.js +90 -10
  77. package/packages/datadog-instrumentations/src/webdriverio.js +914 -0
  78. package/packages/datadog-plugin-aerospike/src/index.js +1 -3
  79. package/packages/datadog-plugin-amqplib/src/client.js +1 -1
  80. package/packages/datadog-plugin-amqplib/src/producer.js +1 -1
  81. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +2 -2
  82. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +2 -2
  83. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -3
  84. package/packages/datadog-plugin-azure-service-bus/src/producer.js +5 -3
  85. package/packages/datadog-plugin-bullmq/src/producer.js +3 -3
  86. package/packages/datadog-plugin-child_process/src/index.js +3 -3
  87. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +5 -8
  88. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -1
  89. package/packages/datadog-plugin-fastify/src/tracing.js +1 -1
  90. package/packages/datadog-plugin-fs/src/index.js +0 -4
  91. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +14 -7
  92. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +19 -10
  93. package/packages/datadog-plugin-graphql/src/execute.js +1 -0
  94. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -1
  95. package/packages/datadog-plugin-http/src/client.js +2 -1
  96. package/packages/datadog-plugin-jest/src/index.js +2 -3
  97. package/packages/datadog-plugin-jest/src/util.js +2 -1
  98. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  99. package/packages/datadog-plugin-mocha/src/index.js +25 -6
  100. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  101. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  102. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  103. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  104. package/packages/datadog-plugin-prisma/src/index.js +5 -1
  105. package/packages/datadog-plugin-rhea/src/consumer.js +16 -13
  106. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  107. package/packages/datadog-plugin-undici/src/index.js +2 -1
  108. package/packages/datadog-plugin-vitest/src/index.js +84 -11
  109. package/packages/datadog-webpack/index.js +2 -2
  110. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -1
  111. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +1 -1
  112. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +1 -1
  113. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +1 -1
  114. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +1 -3
  115. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  116. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +2 -2
  117. package/packages/dd-trace/src/appsec/sdk/track_event.js +1 -1
  118. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +7 -6
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +50 -51
  121. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -1
  122. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +4 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/webdriverio.js +26 -0
  124. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -1
  125. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  126. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -3
  127. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +7 -5
  128. package/packages/dd-trace/src/config/defaults.js +14 -6
  129. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -2
  130. package/packages/dd-trace/src/config/index.js +15 -0
  131. package/packages/dd-trace/src/config/parsers.js +19 -1
  132. package/packages/dd-trace/src/config/supported-configurations.json +12 -6
  133. package/packages/dd-trace/src/datastreams/encoding.js +3 -4
  134. package/packages/dd-trace/src/debugger/devtools_client/log.js +2 -1
  135. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +1 -4
  136. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -2
  137. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +2 -2
  138. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +16 -2
  139. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  140. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  141. package/packages/dd-trace/src/exporter.js +16 -9
  142. package/packages/dd-trace/src/exporters/common/url.js +3 -5
  143. package/packages/dd-trace/src/exporters/electron/index.js +4 -1
  144. package/packages/dd-trace/src/external-logger/src/index.js +1 -2
  145. package/packages/dd-trace/src/flare/index.js +2 -2
  146. package/packages/dd-trace/src/id.js +1 -1
  147. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -1
  148. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -2
  149. package/packages/dd-trace/src/llmobs/experiments/dataset.js +66 -14
  150. package/packages/dd-trace/src/llmobs/experiments/experiment.js +289 -95
  151. package/packages/dd-trace/src/llmobs/experiments/index.js +51 -12
  152. package/packages/dd-trace/src/llmobs/experiments/noop.js +95 -10
  153. package/packages/dd-trace/src/llmobs/experiments/result.js +13 -2
  154. package/packages/dd-trace/src/llmobs/experiments/util.js +218 -0
  155. package/packages/dd-trace/src/llmobs/index.js +9 -1
  156. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  157. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +1 -1
  158. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +1 -1
  159. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  160. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  161. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  162. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  163. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  164. package/packages/dd-trace/src/llmobs/sdk.js +6 -12
  165. package/packages/dd-trace/src/llmobs/span_processor.js +14 -7
  166. package/packages/dd-trace/src/llmobs/tagger.js +24 -5
  167. package/packages/dd-trace/src/llmobs/util.js +56 -3
  168. package/packages/dd-trace/src/llmobs/writers/spans.js +12 -6
  169. package/packages/dd-trace/src/noop/proxy.js +2 -2
  170. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  171. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  172. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  173. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  174. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +2 -1
  175. package/packages/dd-trace/src/opentracing/span.js +1 -1
  176. package/packages/dd-trace/src/opentracing/span_context.js +1 -1
  177. package/packages/dd-trace/src/plugins/ci_plugin.js +114 -23
  178. package/packages/dd-trace/src/plugins/index.js +2 -0
  179. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  180. package/packages/dd-trace/src/plugins/plugin.js +1 -1
  181. package/packages/dd-trace/src/plugins/tracing.js +1 -0
  182. package/packages/dd-trace/src/plugins/util/ci.js +7 -6
  183. package/packages/dd-trace/src/plugins/util/git.js +1 -1
  184. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +1 -1
  185. package/packages/dd-trace/src/plugins/util/llm.js +2 -2
  186. package/packages/dd-trace/src/plugins/util/tags.js +2 -0
  187. package/packages/dd-trace/src/plugins/util/test.js +38 -12
  188. package/packages/dd-trace/src/plugins/util/url.js +5 -5
  189. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  190. package/packages/dd-trace/src/plugins/util/web.js +1 -1
  191. package/packages/dd-trace/src/priority_sampler.js +6 -3
  192. package/packages/dd-trace/src/profiling/profilers/poisson.js +1 -1
  193. package/packages/dd-trace/src/profiling/profilers/shared.js +2 -2
  194. package/packages/dd-trace/src/ritm.js +5 -20
  195. package/packages/dd-trace/src/span_processor.js +1 -1
  196. package/packages/dd-trace/src/span_stats.js +22 -4
  197. package/packages/dd-trace/src/startup-log.js +3 -0
  198. package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -1
  199. package/packages/dd-trace/src/util.js +16 -0
  200. package/vendor/dist/@apm-js-collab/code-transformer/index.js +12 -12
  201. package/vendor/dist/pprof-format/index.js +1 -1
  202. package/vendor/dist/source-map/index.js +1 -1
  203. package/ci/test-optimization-validation/command-suitability.js +0 -471
  204. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  205. package/ci/test-optimization-validation/init-probe.js +0 -246
  206. package/ci/test-optimization-validation/late-initialization.js +0 -63
  207. package/ci/test-optimization-validation/local-command.js +0 -163
  208. package/ci/test-optimization-validation/setup-runner.js +0 -97
  209. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -1,8 +1,90 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../../log')
4
+
5
+ class NoopDataset {
6
+ #name
7
+ #records
8
+
9
+ constructor (name = '', options = {}) {
10
+ this.#name = name
11
+ this.#records = (typeof options === 'string' ? [] : (options.records ?? [])).map(record => ({
12
+ id: record.id ?? null,
13
+ input: record.inputData,
14
+ expectedOutput: record.expectedOutput ?? null,
15
+ metadata: record.metadata ?? {},
16
+ }))
17
+ }
18
+
19
+ addRecord (input, expectedOutput, metadata) {
20
+ this.#records.push({ id: null, input, expectedOutput: expectedOutput ?? null, metadata: metadata ?? {} })
21
+ return this
22
+ }
23
+
24
+ push () {
25
+ return Promise.resolve({ pushedCount: 0, totalCount: 0 })
26
+ }
27
+
28
+ name () {
29
+ return this.#name
30
+ }
31
+
32
+ id () {
33
+ return null
34
+ }
35
+
36
+ projectId () {
37
+ return null
38
+ }
39
+
40
+ version () {
41
+ return null
42
+ }
43
+
44
+ latestVersion () {
45
+ return null
46
+ }
47
+
48
+ records () {
49
+ return [...this.#records]
50
+ }
51
+
52
+ recordIds () {
53
+ return []
54
+ }
55
+
56
+ url () {
57
+ return null
58
+ }
59
+ }
60
+
61
+ class NoopExperiment {
62
+ #name
63
+
64
+ constructor (name = '') {
65
+ this.#name = name
66
+ }
67
+
68
+ name () {
69
+ return this.#name
70
+ }
71
+
72
+ experimentId () {
73
+ return null
74
+ }
75
+
76
+ url () {
77
+ return null
78
+ }
79
+
80
+ run () {
81
+ return Promise.resolve({ experimentId: null, rows: [], url: null })
82
+ }
83
+ }
84
+
3
85
  // No-op Experiments used when LLM Observability is disabled or the API/APP keys
4
- // are not configured. Every operation throws a clear, actionable error rather
5
- // than silently doing nothing, so misconfiguration surfaces immediately.
86
+ // are not configured. Operations warn and return inert objects rather than
87
+ // throwing, so intentionally disabled experiments remain graceful.
6
88
  class NoopExperiments {
7
89
  #reason
8
90
 
@@ -10,20 +92,23 @@ class NoopExperiments {
10
92
  this.#reason = reason || 'LLMObs experiments are not available'
11
93
  }
12
94
 
13
- #unavailable () {
14
- return new Error(`LLMObs experiments unavailable: ${this.#reason}`)
95
+ #warn () {
96
+ log.warn('LLMObs experiments unavailable: %s', this.#reason)
15
97
  }
16
98
 
17
- createDataset () {
18
- throw this.#unavailable()
99
+ createDataset (name, options = {}) {
100
+ this.#warn()
101
+ return new NoopDataset(name, options)
19
102
  }
20
103
 
21
- pullDataset () {
22
- return Promise.reject(this.#unavailable())
104
+ pullDataset (name) {
105
+ this.#warn()
106
+ return Promise.resolve(new NoopDataset(name))
23
107
  }
24
108
 
25
- experiment () {
26
- throw this.#unavailable()
109
+ experiment (options = {}) {
110
+ this.#warn()
111
+ return new NoopExperiment(options.name)
27
112
  }
28
113
  }
29
114
 
@@ -22,13 +22,24 @@ class Row {
22
22
  }
23
23
  }
24
24
 
25
+ class ExperimentRun {
26
+ constructor (fields) {
27
+ this.runId = fields.runId
28
+ this.runIteration = fields.runIteration
29
+ this.rows = fields.rows
30
+ this.summaryEvaluations = fields.summaryEvaluations
31
+ }
32
+ }
33
+
25
34
  // Returned by Experiment.run().
26
35
  class ExperimentResult {
27
- constructor (experimentId, rows, url) {
36
+ constructor (experimentId, rows, url, runs = [], summaryEvaluations = {}) {
28
37
  this.experimentId = experimentId
29
38
  this.rows = rows
30
39
  this.url = url
40
+ this.runs = runs
41
+ this.summaryEvaluations = summaryEvaluations
31
42
  }
32
43
  }
33
44
 
34
- module.exports = { Row, ExperimentResult }
45
+ module.exports = { Row, ExperimentRun, ExperimentResult }
@@ -0,0 +1,218 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../log')
4
+
5
+ // Matches the backend and dd-trace-py evaluator metric label contract.
6
+ const EVALUATOR_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/
7
+
8
+ /**
9
+ * @typedef {null | string | number | boolean | NormalizedJsonValue[] | { [key: string]: NormalizedJsonValue }}
10
+ * NormalizedJsonValue
11
+ */
12
+
13
+ /**
14
+ * @param {object | null | undefined} value
15
+ * @returns {boolean}
16
+ */
17
+ function hasEntries (value) {
18
+ if (!value) return false
19
+ for (const key of Object.keys(value)) {
20
+ if (Object.hasOwn(value, key)) return true
21
+ }
22
+ return false
23
+ }
24
+
25
+ /**
26
+ * @param {string} name
27
+ */
28
+ function validateEvaluatorName (name) {
29
+ if (typeof name !== 'string') throw new TypeError('Evaluator name must be a string')
30
+ if (name.length === 0) throw new Error('Evaluator name cannot be empty')
31
+ if (!EVALUATOR_NAME_PATTERN.test(name)) {
32
+ throw new Error(
33
+ `Evaluator name '${name}' is invalid. Name must contain only alphanumeric characters, underscores, and hyphens.`
34
+ )
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @param {(...args: unknown[]) => unknown} fn
40
+ * @param {string} fallback
41
+ * @returns {string}
42
+ */
43
+ function functionName (fn, fallback) {
44
+ return typeof fn.name === 'string' && fn.name.length > 0 ? fn.name : fallback
45
+ }
46
+
47
+ /**
48
+ * @param {unknown} evaluators
49
+ * @param {string} kind
50
+ * @returns {Array<[string, (...args: unknown[]) => unknown]>}
51
+ */
52
+ function normalizeEvaluators (evaluators, kind) {
53
+ if (evaluators == null) return []
54
+
55
+ const normalized = []
56
+ if (Array.isArray(evaluators)) {
57
+ const indexesByName = new Map()
58
+ for (let i = 0; i < evaluators.length; i++) {
59
+ const evaluator = evaluators[i]
60
+ if (typeof evaluator !== 'function') throw new TypeError(`${kind} evaluator must be a function`)
61
+ const name = functionName(evaluator, `${kind}_evaluator_${i}`)
62
+ validateEvaluatorName(name)
63
+ if (indexesByName.has(name)) {
64
+ log.warn('Duplicate %s evaluator name %s; previous evaluator will be overwritten', kind, name)
65
+ normalized[indexesByName.get(name)] = [name, evaluator]
66
+ } else {
67
+ indexesByName.set(name, normalized.length)
68
+ normalized.push([name, evaluator])
69
+ }
70
+ }
71
+ return normalized
72
+ }
73
+
74
+ if (typeof evaluators !== 'object') {
75
+ throw new TypeError(`${kind} evaluators must be an array of functions or an object keyed by evaluator name`)
76
+ }
77
+
78
+ for (const [name, evaluator] of Object.entries(evaluators)) {
79
+ validateEvaluatorName(name)
80
+ if (typeof evaluator !== 'function') throw new TypeError(`${kind} evaluator '${name}' must be a function`)
81
+ normalized.push([name, evaluator])
82
+ }
83
+ return normalized
84
+ }
85
+
86
+ /**
87
+ * @param {unknown} value
88
+ * @returns {string}
89
+ */
90
+ function inferMetricType (value) {
91
+ if (typeof value === 'boolean') return 'boolean'
92
+ if (typeof value === 'number' && Number.isFinite(value)) return 'score'
93
+ if (typeof value === 'string') return 'categorical'
94
+ return 'json'
95
+ }
96
+
97
+ /**
98
+ * @param {unknown} value
99
+ * @param {WeakSet<object>} seen
100
+ * @returns {NormalizedJsonValue}
101
+ */
102
+ function normalizeJsonValue (value, seen) {
103
+ if (value === null) return null
104
+
105
+ const valueType = typeof value
106
+ if (valueType === 'string' || valueType === 'boolean') return value
107
+ if (valueType === 'number') return Number.isFinite(value) ? value : String(value)
108
+ if (valueType === 'bigint') return value.toString()
109
+ if (valueType === 'undefined') return null
110
+ if (valueType === 'symbol') return value.toString()
111
+ if (valueType === 'function') return { type: 'function', name: value.name || '<anonymous>' }
112
+
113
+ if (value instanceof Date) return value.toISOString()
114
+ if (value instanceof Error) {
115
+ return { type: value.name, message: value.message, stack: value.stack ?? '' }
116
+ }
117
+
118
+ if (seen.has(value)) return '[Circular]'
119
+ seen.add(value)
120
+
121
+ if (Array.isArray(value)) {
122
+ const normalized = new Array(value.length)
123
+ for (let i = 0; i < value.length; i++) normalized[i] = normalizeJsonValue(value[i], seen)
124
+ seen.delete(value)
125
+ return normalized
126
+ }
127
+
128
+ const normalized = {}
129
+ for (const [key, nestedValue] of Object.entries(value)) {
130
+ normalized[key] = normalizeJsonValue(nestedValue, seen)
131
+ }
132
+ seen.delete(value)
133
+ return normalized
134
+ }
135
+
136
+ /**
137
+ * @param {unknown} value
138
+ * @returns {{ [key: string]: NormalizedJsonValue }}
139
+ */
140
+ function normalizeJsonMetricValue (value) {
141
+ const normalized = normalizeJsonValue(value, new WeakSet())
142
+ if (normalized !== null && typeof normalized === 'object' && !Array.isArray(normalized)) return normalized
143
+ return { value: normalized }
144
+ }
145
+
146
+ /**
147
+ * @param {unknown} value
148
+ * @returns {string}
149
+ */
150
+ function stringify (value) {
151
+ if (value == null) return ''
152
+ if (typeof value === 'string') return value.toLowerCase()
153
+ if (typeof value === 'object') {
154
+ try {
155
+ const stringified = JSON.stringify(value)
156
+ if (stringified !== undefined) return stringified.toLowerCase()
157
+ } catch {}
158
+ }
159
+ return String(value).toLowerCase()
160
+ }
161
+
162
+ /**
163
+ * @param {Record<string, unknown> | undefined} userTags
164
+ * @param {Record<string, unknown>} autoTags
165
+ * @returns {string[]}
166
+ */
167
+ function buildTags (userTags, autoTags) {
168
+ const tags = new Map()
169
+ for (const [key, value] of Object.entries(userTags ?? {})) {
170
+ tags.set(key, `${key}:${value}`)
171
+ }
172
+ for (const [key, value] of Object.entries(autoTags)) {
173
+ if (value !== undefined && value !== null && value !== '') tags.set(key, `${key}:${value}`)
174
+ }
175
+ return [...tags.values()]
176
+ }
177
+
178
+ /**
179
+ * @param {Record<string, unknown> | undefined} userTags
180
+ * @param {Record<string, unknown>} autoTags
181
+ * @returns {Record<string, unknown>}
182
+ */
183
+ function buildExperimentTagObject (userTags, autoTags) {
184
+ return userTags ? { ...userTags, ...autoTags } : { ...autoTags }
185
+ }
186
+
187
+ /**
188
+ * @param {number} ms
189
+ * @returns {Promise<void>}
190
+ */
191
+ function sleep (ms) {
192
+ if (ms <= 0) return Promise.resolve()
193
+ return new Promise((resolve) => setTimeout(resolve, ms))
194
+ }
195
+
196
+ /**
197
+ * @param {Record<string, unknown> | undefined} recordMetadata
198
+ * @param {Record<string, unknown>} config
199
+ * @returns {Record<string, unknown>}
200
+ */
201
+ function buildSpanMetadata (recordMetadata, config) {
202
+ return recordMetadata
203
+ ? { ...recordMetadata, experiment_config: config }
204
+ : { experiment_config: config }
205
+ }
206
+
207
+ module.exports = {
208
+ buildExperimentTagObject,
209
+ buildSpanMetadata,
210
+ buildTags,
211
+ hasEntries,
212
+ inferMetricType,
213
+ normalizeEvaluators,
214
+ normalizeJsonMetricValue,
215
+ sleep,
216
+ stringify,
217
+ validateEvaluatorName,
218
+ }
@@ -16,6 +16,8 @@ const {
16
16
  SAMPLING_DECISION,
17
17
  PROPAGATED_SAMPLE_RATE_KEY,
18
18
  PROPAGATED_SAMPLING_DECISION_KEY,
19
+ TRACE_ID,
20
+ PROPAGATED_TRACE_ID_KEY,
19
21
  } = require('./constants/tags')
20
22
  const { storage } = require('./storage')
21
23
  const telemetry = require('./telemetry')
@@ -25,6 +27,7 @@ const LLMObsTagger = require('./tagger')
25
27
  const LLMObsSpanWriter = require('./writers/spans')
26
28
  const { setAgentStrategy } = require('./writers/util')
27
29
  const { INCOMPATIBLE_INITIALIZATION } = require('./constants/text')
30
+ const { llmObsTraceIdToWire } = require('./util')
28
31
 
29
32
  const spanFinishCh = channel('dd-trace:span:finish')
30
33
  const evalMetricAppendCh = channel('llmobs:eval-metric:append')
@@ -137,8 +140,12 @@ function handleLLMObsInjection ({ carrier }) {
137
140
  mlObsSpanTags?.[SESSION_ID] ??
138
141
  parentContext?._trace?.tags?.[SESSION_ID_TRACE_DEFAULT_KEY] ??
139
142
  parentContext?._trace?.tags?.[PROPAGATED_SESSION_ID_KEY]
143
+ const llmobsTraceId = mlObsSpanTags?.[TRACE_ID]
144
+ const propagatedTraceId = llmobsTraceId === undefined
145
+ ? parentContext?._trace?.tags?.[PROPAGATED_TRACE_ID_KEY]
146
+ : llmObsTraceIdToWire(llmobsTraceId)
140
147
 
141
- if (!parentId && !mlApp && samplingDecision == null && !sessionId) return
148
+ if (!parentId && !mlApp && samplingDecision == null && !sessionId && !propagatedTraceId) return
142
149
 
143
150
  // `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*`
144
151
  // tags, so it may be undefined here — coalesce before appending.
@@ -149,6 +156,7 @@ function handleLLMObsInjection ({ carrier }) {
149
156
  if (sessionId) tags += `${tags ? ',' : ''}${PROPAGATED_SESSION_ID_KEY}=${sessionId}`
150
157
  if (sampleRate != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLE_RATE_KEY}=${sampleRate}`
151
158
  if (samplingDecision != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLING_DECISION_KEY}=${samplingDecision}`
159
+ if (propagatedTraceId != null) tags += `${tags ? ',' : ''}${PROPAGATED_TRACE_ID_KEY}=${propagatedTraceId}`
152
160
  if (tags !== existing) carrier['x-datadog-tags'] = tags
153
161
  }
154
162
 
@@ -13,6 +13,8 @@ const MODEL_METADATA_KEYS = new Set([
13
13
  const VERCEL_AI_TELEMETRY_METADATA_PREFIX = 'ai.telemetry.metadata.'
14
14
  const VERCEL_AI_MODEL_METADATA_PREFIX = 'gen_ai.request.'
15
15
  const VERCEL_AI_GENERATION_METADATA_PREFIX = 'ai.settings.'
16
+ const UNPARSABLE_TOOL_RESULT = '[Unparsable Tool Result]'
17
+ const UNSUPPORTED_TOOL_RESULT = '[Unsupported Tool Result]'
16
18
 
17
19
  /**
18
20
  * @typedef {import('../../../opentracing/span')} Span
@@ -26,8 +28,31 @@ const VERCEL_AI_GENERATION_METADATA_PREFIX = 'ai.settings.'
26
28
 
27
29
  /**
28
30
  * @typedef {{
29
- * type: string,
30
- * value?: unknown
31
+ * type: 'text',
32
+ * text: string
33
+ * } | {
34
+ * type: 'media' | 'file',
35
+ * mediaType: string
36
+ * } | {
37
+ * type: 'file-data' | 'file-url' | 'file-id' | 'file-reference'
38
+ * } | {
39
+ * type: 'image-data' | 'image-url' | 'image-file-id' | 'image-file-reference'
40
+ * } | {
41
+ * type: 'custom'
42
+ * }} ToolCallContentPart
43
+ *
44
+ * @typedef {{
45
+ * type: 'text' | 'error-text',
46
+ * value: string
47
+ * } | {
48
+ * type: 'json' | 'error-json',
49
+ * value: unknown
50
+ * } | {
51
+ * type: 'content',
52
+ * value: ToolCallContentPart[]
53
+ * } | {
54
+ * type: 'execution-denied',
55
+ * reason?: string
31
56
  * }} ToolCallOutput
32
57
  *
33
58
  * @typedef {{ output?: ToolCallOutput, result?: unknown } & Record<string, unknown>} ToolCallResultContent
@@ -304,32 +329,89 @@ function getToolNameFromTags (tags) {
304
329
  }
305
330
 
306
331
  /**
307
- * Get the content of a tool call result.
308
- * Version 5 of the ai sdk sets this tag as `content.output`, with a `
309
- * @param {{ output?: { type: string, value: unknown }, result?: unknown }} content
332
+ * @param {unknown} value
333
+ * @returns {string}
334
+ */
335
+ function stringifyToolCallResult (value) {
336
+ return JSON.stringify(value) ?? UNPARSABLE_TOOL_RESULT
337
+ }
338
+
339
+ /**
340
+ * @param {ToolCallContentPart[]} value
341
+ * @returns {string}
342
+ */
343
+ function formatToolCallContent (value) {
344
+ if (!Array.isArray(value)) return UNPARSABLE_TOOL_RESULT
345
+
346
+ let result = ''
347
+ for (const part of value) {
348
+ if (typeof part !== 'object' || part === null) return UNPARSABLE_TOOL_RESULT
349
+
350
+ const { type } = part
351
+ if (type === 'text') {
352
+ if (typeof part.text !== 'string') return UNPARSABLE_TOOL_RESULT
353
+ result += part.text
354
+ } else if (type === 'media' || type === 'file') {
355
+ const { mediaType } = part
356
+ if (typeof mediaType !== 'string') return UNPARSABLE_TOOL_RESULT
357
+ result += mediaType === 'image' || mediaType.startsWith('image/') ? '[Image]' : '[File]'
358
+ } else if (
359
+ type === 'file-data' ||
360
+ type === 'file-url' ||
361
+ type === 'file-id' ||
362
+ type === 'file-reference'
363
+ ) {
364
+ result += '[File]'
365
+ } else if (
366
+ type === 'image-data' ||
367
+ type === 'image-url' ||
368
+ type === 'image-file-id' ||
369
+ type === 'image-file-reference'
370
+ ) {
371
+ result += '[Image]'
372
+ } else if (type === 'custom') {
373
+ result += '[Custom Content]'
374
+ } else {
375
+ return UNPARSABLE_TOOL_RESULT
376
+ }
377
+ }
378
+
379
+ return result
380
+ }
381
+
382
+ /**
383
+ * @param {ToolCallResultContent | null | undefined} content
310
384
  * @returns {string}
311
385
  */
312
386
  function getToolCallResultContent (content) {
313
- const { output, result } = content
314
- if (output) {
315
- if (output.type === 'text') {
316
- return output.value
317
- } else if (output.type === 'json') {
318
- return JSON.stringify(output.value)
387
+ try {
388
+ if (typeof content !== 'object' || content === null) return UNPARSABLE_TOOL_RESULT
389
+
390
+ const { output, result } = content
391
+ if (output !== undefined) {
392
+ if (typeof output !== 'object' || output === null) return UNPARSABLE_TOOL_RESULT
393
+
394
+ const { type, value } = output
395
+ if (type === 'text' || type === 'error-text') {
396
+ return typeof value === 'string' ? value : UNPARSABLE_TOOL_RESULT
397
+ } else if (type === 'json' || type === 'error-json') {
398
+ return stringifyToolCallResult(value)
399
+ } else if (type === 'content') {
400
+ return formatToolCallContent(value)
401
+ } else if (type === 'execution-denied') {
402
+ const { reason } = output
403
+ if (reason === undefined) return '[Tool Execution Denied]'
404
+ return typeof reason === 'string' ? reason : UNPARSABLE_TOOL_RESULT
405
+ }
406
+ return UNPARSABLE_TOOL_RESULT
319
407
  }
320
- return '[Unparsable Tool Result]'
321
- } else if (result) {
322
- if (typeof result === 'string') {
323
- return result
408
+ if (result !== undefined) {
409
+ return typeof result === 'string' ? result : stringifyToolCallResult(result)
324
410
  }
325
411
 
326
- try {
327
- return JSON.stringify(result)
328
- } catch {
329
- return '[Unparsable Tool Result]'
330
- }
331
- } else {
332
- return '[Unsupported Tool Result]'
412
+ return UNSUPPORTED_TOOL_RESULT
413
+ } catch {
414
+ return UNPARSABLE_TOOL_RESULT
333
415
  }
334
416
  }
335
417
 
@@ -93,7 +93,7 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
93
93
 
94
94
  const { usage } = chunk
95
95
  if (usage) {
96
- const responseUsage = response.usage ?? (response.usage = { input_tokens: 0, output_tokens: 0 })
96
+ const responseUsage = (response.usage ??= { input_tokens: 0, output_tokens: 0 })
97
97
  responseUsage.output_tokens = usage.output_tokens
98
98
 
99
99
  const cacheCreationTokens = usage.cache_creation_input_tokens
@@ -8,7 +8,7 @@ const { splitModel } = require('../../../../../datadog-plugin-claude-agent-sdk/s
8
8
  const subagentToolIds = new Set()
9
9
 
10
10
  function normalizeToolOutputString (raw) {
11
- const footerIndex = raw.search(/\s*agentId: [^\s]+ \(use SendMessage\b/)
11
+ const footerIndex = raw.search(/agentId: \S+ \(use SendMessage\b/)
12
12
  if (footerIndex === -1) return raw
13
13
 
14
14
  return raw.slice(0, footerIndex).trimEnd()
@@ -24,7 +24,7 @@ class GenAiLLMObsPlugin extends LLMObsPlugin {
24
24
  // Subscribe to streaming chunk events
25
25
  this.addSub('apm:google:genai:request:chunk', ({ ctx, chunk, done }) => {
26
26
  ctx.isStreaming = true
27
- ctx.chunks = ctx.chunks || []
27
+ ctx.chunks ||= []
28
28
 
29
29
  if (chunk) ctx.chunks.push(chunk)
30
30
  if (!done) return
@@ -36,7 +36,7 @@ class LangChainLLMObsHandler {
36
36
  const runIdBase = runId ? runId.split('-').slice(0, -1).join('-') : ''
37
37
 
38
38
  const responseMetadata = message.response_metadata || {}
39
- usage = usage || responseMetadata.usage || responseMetadata.tokenUsage || {}
39
+ usage ||= responseMetadata.usage || responseMetadata.tokenUsage || {}
40
40
 
41
41
  const inputTokens = usage.promptTokens || usage.inputTokens || usage.prompt_tokens || usage.input_tokens || 0
42
42
  const outputTokens =
@@ -5,7 +5,6 @@ const {
5
5
  PROMPT_TRACKING_INSTRUMENTATION_METHOD,
6
6
  PROMPT_MULTIMODAL,
7
7
  INSTRUMENTATION_METHOD_AUTO,
8
- UNKNOWN_MODEL_PROVIDER,
9
8
  } = require('../../constants/tags')
10
9
  const { audioMimeTypeFromFormat, formatAudioPart, safeJsonParse } = require('../../util')
11
10
  const { AUDIO_MIME_TYPES } = require('./constants')
@@ -15,6 +14,7 @@ const {
15
14
  extractTextFromContentItem,
16
15
  extractContentParts,
17
16
  hasMultimodalInputs,
17
+ getOpenAIModelProvider,
18
18
  } = require('./utils')
19
19
 
20
20
  const allowedParamKeys = new Set([
@@ -108,14 +108,10 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
108
108
  }
109
109
 
110
110
  _getModelProviderAndClient (baseUrl = '') {
111
- if (baseUrl.includes('azure')) {
112
- return { modelProvider: 'azure_openai', client: 'AzureOpenAI' }
113
- } else if (baseUrl.includes('openai')) {
114
- return { modelProvider: 'openai', client: 'OpenAI' }
115
- } else if (baseUrl.includes('deepseek')) {
116
- return { modelProvider: 'deepseek', client: 'DeepSeek' }
117
- }
118
- return { modelProvider: UNKNOWN_MODEL_PROVIDER, client: 'OpenAI' }
111
+ const modelProvider = getOpenAIModelProvider(baseUrl)
112
+ if (modelProvider === 'azure_openai') return { modelProvider, client: 'AzureOpenAI' }
113
+ if (modelProvider === 'deepseek') return { modelProvider, client: 'DeepSeek' }
114
+ return { modelProvider, client: 'OpenAI' }
119
115
  }
120
116
 
121
117
  _extractMetrics (response) {
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { UNKNOWN_MODEL_PROVIDER } = require('../../constants/tags')
3
4
  const { audioMimeTypeFromFormat, formatAudioPart } = require('../../util')
4
5
  const {
5
6
  INPUT_TYPE_IMAGE,
@@ -160,10 +161,29 @@ function extractContentParts (parts) {
160
161
  return { content: extracted.join('\n'), audioParts }
161
162
  }
162
163
 
164
+ /**
165
+ * Maps an OpenAI-compatible base URL to a model provider string. Covers
166
+ * OpenAI, Azure OpenAI, and DeepSeek; falls back to UNKNOWN_MODEL_PROVIDER
167
+ * for unrecognised hosts (e.g. local proxies or custom deployments).
168
+ *
169
+ * Shared with the openai-agents integration since both consume the same
170
+ * client baseURL convention.
171
+ *
172
+ * @param {string} baseUrl
173
+ * @returns {string}
174
+ */
175
+ function getOpenAIModelProvider (baseUrl = '') {
176
+ if (baseUrl.includes('azure')) return 'azure_openai'
177
+ if (baseUrl.includes('deepseek')) return 'deepseek'
178
+ if (baseUrl.includes('openai')) return 'openai'
179
+ return UNKNOWN_MODEL_PROVIDER
180
+ }
181
+
163
182
  module.exports = {
164
183
  extractChatTemplateFromInstructions,
165
184
  normalizePromptVariables,
166
185
  extractTextFromContentItem,
167
186
  extractContentParts,
168
187
  hasMultimodalInputs,
188
+ getOpenAIModelProvider,
169
189
  }