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
@@ -0,0 +1,321 @@
1
+ 'use strict'
2
+
3
+ const { extractContentParts, extractTextFromContentItem } = require('../openai/utils')
4
+ const { safeJsonParse } = require('../../util')
5
+
6
+ /**
7
+ * Flatten Responses and Chat Completions content parts without dropping
8
+ * provider-specific image or audio inputs.
9
+ *
10
+ * @param {Array<object>} parts
11
+ * @returns {{ content: string, audioParts: Array<{ mimeType: string, content: string }> }}
12
+ */
13
+ function extractMessageContent (parts) {
14
+ const contentParts = []
15
+ const audioParts = []
16
+
17
+ for (const part of parts) {
18
+ if (!part) continue
19
+ const text = extractTextFromContentItem(part)
20
+ if (text) {
21
+ contentParts.push(text)
22
+ continue
23
+ }
24
+
25
+ const extracted = extractContentParts([part])
26
+ if (extracted.content) contentParts.push(extracted.content)
27
+ if (extracted.audioParts.length > 0) audioParts.push(...extracted.audioParts)
28
+ }
29
+
30
+ return { content: contentParts.join(''), audioParts }
31
+ }
32
+
33
+ /**
34
+ * Normalize OpenAI Chat Completions tool calls to the LLMObs message schema.
35
+ *
36
+ * @param {object} message
37
+ * @returns {Array<{ toolId?: string, name?: string, arguments: object, type?: string }>}
38
+ */
39
+ function extractChatCompletionToolCalls (message) {
40
+ if (message.function_call) {
41
+ return [{
42
+ name: message.function_call.name,
43
+ arguments: safeJsonParse(message.function_call.arguments, {}),
44
+ type: 'function',
45
+ }]
46
+ }
47
+
48
+ const toolCalls = []
49
+ if (!Array.isArray(message.tool_calls)) return toolCalls
50
+
51
+ for (const toolCall of message.tool_calls) {
52
+ if (!toolCall) continue
53
+ toolCalls.push({
54
+ toolId: toolCall.id,
55
+ name: toolCall.function?.name,
56
+ arguments: safeJsonParse(toolCall.function?.arguments, {}),
57
+ type: toolCall.type,
58
+ })
59
+ }
60
+ return toolCalls
61
+ }
62
+
63
+ /**
64
+ * Normalize a Chat Completions message to the LLMObs message schema.
65
+ *
66
+ * @param {object} message
67
+ * @returns {object}
68
+ */
69
+ function normalizeChatCompletionMessage (message) {
70
+ const normalized = {
71
+ role: message.role || 'assistant',
72
+ content: message.content ?? '',
73
+ }
74
+ if (message.audioParts?.length > 0) normalized.audioParts = message.audioParts
75
+ const toolCalls = extractChatCompletionToolCalls(message)
76
+ if (toolCalls.length > 0) normalized.toolCalls = toolCalls
77
+ if (message.tool_call_id) normalized.toolId = message.tool_call_id
78
+ return normalized
79
+ }
80
+
81
+ /**
82
+ * Extracts input messages for an LLM span. agents-openai stores only
83
+ * `request.input` on `spanData._input` (string or message-array), and the
84
+ * system instructions are echoed back on the response as `instructions`.
85
+ *
86
+ * @param {string|Array<unknown>} input - The raw `request.input` (`spanData._input`).
87
+ * @param {string} [instructions] - System instructions echoed on `response.instructions`.
88
+ * @returns {Array<{ role: string, content: string }>}
89
+ */
90
+ function extractInputMessages (input, instructions) {
91
+ const messages = []
92
+
93
+ if (instructions) {
94
+ messages.push({ role: 'system', content: instructions })
95
+ }
96
+
97
+ if (typeof input === 'string') {
98
+ messages.push({ role: 'user', content: input })
99
+ } else if (Array.isArray(input)) {
100
+ for (const item of input) {
101
+ if (!item) continue
102
+ if (item.type === 'message' || item.role) {
103
+ const role = item.role
104
+ if (!role) continue
105
+
106
+ let content = ''
107
+ let audioParts
108
+ if (Array.isArray(item.content)) {
109
+ const extracted = extractMessageContent(item.content)
110
+ content = extracted.content
111
+ audioParts = extracted.audioParts
112
+ } else if (typeof item.content === 'string') {
113
+ content = item.content
114
+ }
115
+
116
+ const message = normalizeChatCompletionMessage({ ...item, content, role, audioParts })
117
+ if (content || message.audioParts || message.toolCalls || message.toolId) {
118
+ messages.push(message)
119
+ }
120
+ } else if (item.type === 'function_call') {
121
+ let args = item.arguments
122
+ if (typeof args === 'string') {
123
+ try {
124
+ args = JSON.parse(args)
125
+ } catch {
126
+ args = {}
127
+ }
128
+ }
129
+ messages.push({
130
+ role: 'assistant',
131
+ toolCalls: [{
132
+ toolId: item.call_id,
133
+ name: item.name,
134
+ arguments: args,
135
+ type: item.type,
136
+ }],
137
+ })
138
+ } else if (item.type === 'function_call_output') {
139
+ messages.push({
140
+ role: 'user',
141
+ toolResults: [{
142
+ toolId: item.call_id,
143
+ result: item.output,
144
+ name: item.name || '',
145
+ type: item.type,
146
+ }],
147
+ })
148
+ }
149
+ }
150
+ }
151
+
152
+ return messages.length > 0 ? messages : [{ role: 'user', content: '' }]
153
+ }
154
+
155
+ /**
156
+ * Extracts output messages from the model response.
157
+ *
158
+ * @param {{ output?: Array<unknown> }} result - The model response
159
+ * @returns {Array<{ role: string, content: string }>}
160
+ */
161
+ function extractOutputMessages (result) {
162
+ const messages = []
163
+
164
+ if (result?.output) {
165
+ for (const item of result.output) {
166
+ if (!item) continue
167
+ if (item.type === 'message') {
168
+ let content = ''
169
+ if (Array.isArray(item.content)) {
170
+ const textParts = []
171
+ for (const contentItem of item.content) {
172
+ if (contentItem?.type === 'output_text' && contentItem.text) {
173
+ textParts.push(contentItem.text)
174
+ }
175
+ }
176
+ content = textParts.join('')
177
+ } else if (typeof item.content === 'string') {
178
+ content = item.content
179
+ }
180
+
181
+ messages.push({ role: item.role || 'assistant', content })
182
+ } else if (item.type === 'function_call') {
183
+ let args = item.arguments
184
+ if (typeof args === 'string') {
185
+ try {
186
+ args = JSON.parse(args)
187
+ } catch {
188
+ args = {}
189
+ }
190
+ }
191
+ messages.push({
192
+ role: 'assistant',
193
+ toolCalls: [{
194
+ toolId: item.call_id,
195
+ name: item.name,
196
+ arguments: args,
197
+ type: item.type,
198
+ }],
199
+ })
200
+ }
201
+ }
202
+ }
203
+
204
+ return messages.length > 0 ? messages : [{ content: '', role: '' }]
205
+ }
206
+
207
+ /**
208
+ * Extracts output messages from an OpenAI Chat Completions response.
209
+ *
210
+ * @param {Array<{ choices?: Array<{ message?: object }> }>} result - The model responses
211
+ * @returns {Array<object>}
212
+ */
213
+ function extractGenerationOutputMessages (result) {
214
+ const messages = []
215
+
216
+ if (Array.isArray(result)) {
217
+ for (const response of result) {
218
+ if (!Array.isArray(response?.choices)) continue
219
+ for (const choice of response.choices) {
220
+ const message = choice?.message
221
+ if (!message) continue
222
+ messages.push(normalizeChatCompletionMessage(message))
223
+ }
224
+ }
225
+ }
226
+
227
+ return messages.length > 0 ? messages : [{ content: '', role: '' }]
228
+ }
229
+
230
+ /**
231
+ * Extracts token usage metrics from the model response. Returns `undefined`
232
+ * when there's nothing to tag, so callers can skip the tagger call without
233
+ * allocating an Object.keys array.
234
+ *
235
+ * @param {{ usage?: { inputTokens?: number, outputTokens?: number, totalTokens?: number,
236
+ * outputTokensDetails?: { reasoningTokens?: number },
237
+ * completion_tokens_details?: { reasoning_tokens?: number } } }} result
238
+ * @returns {{ inputTokens?: number, outputTokens?: number, totalTokens?: number,
239
+ * reasoningOutputTokens?: number } | undefined}
240
+ */
241
+ function extractMetrics (result) {
242
+ const usage = result?.usage
243
+ if (!usage) return
244
+
245
+ const inputTokens = usage.inputTokens ?? usage.input_tokens ?? usage.prompt_tokens
246
+ const outputTokens = usage.outputTokens ?? usage.output_tokens ?? usage.completion_tokens
247
+ const totalTokens = usage.totalTokens ?? usage.total_tokens
248
+ const reasoningTokens = usage.outputTokensDetails?.reasoningTokens ??
249
+ usage.output_tokens_details?.reasoning_tokens ??
250
+ usage.completion_tokens_details?.reasoning_tokens
251
+
252
+ if (inputTokens === undefined && outputTokens === undefined &&
253
+ totalTokens === undefined && !reasoningTokens) return
254
+
255
+ const metrics = {}
256
+ if (inputTokens !== undefined) metrics.inputTokens = inputTokens
257
+ if (outputTokens !== undefined) metrics.outputTokens = outputTokens
258
+ // Tagger maps `reasoningOutputTokens` → `reasoning_output_tokens` in the
259
+ // LLMObs span event. Skip when zero — emitting a zero just adds noise.
260
+ if (reasoningTokens) metrics.reasoningOutputTokens = reasoningTokens
261
+
262
+ if (totalTokens !== undefined) {
263
+ metrics.totalTokens = totalTokens
264
+ } else if (metrics.inputTokens !== undefined && metrics.outputTokens !== undefined) {
265
+ metrics.totalTokens = metrics.inputTokens + metrics.outputTokens
266
+ }
267
+
268
+ return metrics
269
+ }
270
+
271
+ // Fields the OpenAI Responses API echoes back from the request configuration.
272
+ // agents-openai only stores `request.input` on the span — the user's
273
+ // `modelSettings` aren't directly observable, so we read the response-echoed
274
+ // values. Matches dd-trace-py's openai-agents integration (see
275
+ // `OaiSpanAdapter.llmobs_metadata`); both ship without filtering OpenAI's
276
+ // default values.
277
+ const RESPONSE_METADATA_FIELDS = [
278
+ 'temperature',
279
+ 'max_output_tokens',
280
+ 'top_p',
281
+ 'tools',
282
+ 'tool_choice',
283
+ 'truncation',
284
+ ]
285
+
286
+ /**
287
+ * Extracts metadata from the model response. Mirrors Python's
288
+ * `OaiSpanAdapter.llmobs_metadata` — emits all response-echoed configuration
289
+ * fields plus `text` when present. Returns `undefined` when nothing was
290
+ * captured, so callers can skip the tagger call without allocating.
291
+ *
292
+ * @param {object | undefined} response
293
+ * @returns {object | undefined}
294
+ */
295
+ function extractMetadata (response) {
296
+ if (!response) return
297
+
298
+ let metadata
299
+ for (const field of RESPONSE_METADATA_FIELDS) {
300
+ const value = response[field]
301
+ if (value !== undefined && value !== null) {
302
+ metadata ??= {}
303
+ metadata[field] = value
304
+ }
305
+ }
306
+
307
+ if (response.text) {
308
+ metadata ??= {}
309
+ metadata.text = response.text
310
+ }
311
+
312
+ return metadata
313
+ }
314
+
315
+ module.exports = {
316
+ extractInputMessages,
317
+ extractOutputMessages,
318
+ extractGenerationOutputMessages,
319
+ extractMetrics,
320
+ extractMetadata,
321
+ }
@@ -11,6 +11,7 @@ const {
11
11
  SPAN_KIND,
12
12
  OUTPUT_VALUE,
13
13
  INPUT_VALUE,
14
+ TRACE_ID,
14
15
  } = require('./constants/tags')
15
16
  const {
16
17
  getFunctionArguments,
@@ -34,12 +35,6 @@ class LLMObs extends NoopLLMObs {
34
35
  */
35
36
  #hasUserSpanProcessor = false
36
37
 
37
- /**
38
- * Lazily-created experiments facade (see ./experiments).
39
- * @type {import('./experiments').Experiments | undefined}
40
- */
41
- #experiments
42
-
43
38
  /**
44
39
  * @param {import('../tracer')} tracer - Tracer instance
45
40
  * @param {import('./index')} llmobsModule - LLMObs module instance
@@ -65,8 +60,7 @@ class LLMObs extends NoopLLMObs {
65
60
  * a clear message on use.
66
61
  */
67
62
  get experiments () {
68
- this.#experiments ??= createExperiments(this._config)
69
- return this.#experiments
63
+ return createExperiments(this._config, this)
70
64
  }
71
65
 
72
66
  enable (options = {}) {
@@ -317,14 +311,14 @@ class LLMObs extends NoopLLMObs {
317
311
  }
318
312
  throw e
319
313
  } finally {
320
- if (autoinstrumented === false) {
314
+ if (!autoinstrumented) {
321
315
  telemetry.recordLLMObsAnnotate(span, err)
322
316
  }
323
317
  }
324
318
  }
325
319
 
326
320
  exportSpan (span) {
327
- span = span || this._active()
321
+ span ||= this._active()
328
322
  let err = ''
329
323
  try {
330
324
  if (!span) {
@@ -345,7 +339,7 @@ class LLMObs extends NoopLLMObs {
345
339
  }
346
340
  try {
347
341
  return {
348
- traceId: span.context().toTraceId(true),
342
+ traceId: LLMObsTagger.tagMap.get(span)[TRACE_ID],
349
343
  spanId: span.context().toSpanId(),
350
344
  }
351
345
  } catch {
@@ -425,7 +419,7 @@ class LLMObs extends NoopLLMObs {
425
419
  err = 'invalid_metric_value'
426
420
  throw new Error('value must be a boolean for a boolean metric')
427
421
  }
428
- if (metricType === 'json' && !(typeof value === 'object' && value != null && !Array.isArray(value))) {
422
+ if (metricType === 'json' && (typeof value !== 'object' || value == null || Array.isArray(value))) {
429
423
  err = 'invalid_metric_value'
430
424
  throw new Error('value must be a JSON object for a json metric')
431
425
  }
@@ -35,6 +35,7 @@ const {
35
35
  LLMOBS_SUBMITTED_TAG_KEY,
36
36
  SAMPLE_RATE,
37
37
  SAMPLING_DECISION,
38
+ TRACE_ID,
38
39
  } = require('./constants/tags')
39
40
  const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
40
41
  const telemetry = require('./telemetry')
@@ -236,8 +237,19 @@ class LLMObsSpanProcessor {
236
237
  meta.input.prompt = prompt
237
238
  }
238
239
 
240
+ const apmTraceId = span.context().toTraceId(true)
241
+ const llmobsTraceId = mlObsTags[TRACE_ID] ?? apmTraceId
242
+ const dd = {
243
+ span_id: span.context().toSpanId(),
244
+ trace_id: apmTraceId,
245
+ sample_rate: mlObsTags[SAMPLE_RATE],
246
+ sampling_decision: mlObsTags[SAMPLING_DECISION],
247
+ apm_trace_id: apmTraceId,
248
+ }
249
+ if (tags.experiment_id) dd.scope = 'experiments'
250
+
239
251
  const llmObsSpanEvent = {
240
- trace_id: span.context().toTraceId(true),
252
+ trace_id: llmobsTraceId,
241
253
  span_id: span.context().toSpanId(),
242
254
  parent_id: parentId,
243
255
  name,
@@ -247,12 +259,7 @@ class LLMObsSpanProcessor {
247
259
  status: error ? 'error' : 'ok',
248
260
  meta,
249
261
  metrics,
250
- _dd: {
251
- span_id: span.context().toSpanId(),
252
- trace_id: span.context().toTraceId(true),
253
- sample_rate: mlObsTags[SAMPLE_RATE],
254
- sampling_decision: mlObsTags[SAMPLING_DECISION],
255
- },
262
+ _dd: dd,
256
263
  }
257
264
 
258
265
  if (sessionId) llmObsSpanEvent.session_id = sessionId
@@ -51,9 +51,18 @@ const {
51
51
  SAMPLING_DECISION_DROPPED,
52
52
  PROPAGATED_SAMPLE_RATE_KEY,
53
53
  PROPAGATED_SAMPLING_DECISION_KEY,
54
+ TRACE_ID,
55
+ PROPAGATED_TRACE_ID_KEY,
54
56
  } = require('./constants/tags')
55
57
  const { storage } = require('./storage')
56
- const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags, validateToolDefinitions } = require('./util')
58
+ const {
59
+ findGenAIAncestorSpanId,
60
+ validateCostTags,
61
+ writeBridgeTags,
62
+ validateToolDefinitions,
63
+ generateLlmObsTraceId,
64
+ normalizeLlmObsTraceId,
65
+ } = require('./util')
57
66
 
58
67
  // global registry of LLMObs spans
59
68
  // maps LLMObs spans to their annotations
@@ -126,12 +135,20 @@ class LLMObsTagger {
126
135
 
127
136
  this._register(span)
128
137
 
138
+ const traceTags = span.context()._trace.tags
139
+
140
+ const llmobsTraceId =
141
+ registry.get(parent)?.[TRACE_ID] ??
142
+ normalizeLlmObsTraceId(traceTags[PROPAGATED_TRACE_ID_KEY]) ??
143
+ generateLlmObsTraceId(span._startTime)
144
+ this._setTag(span, TRACE_ID, llmobsTraceId)
145
+
129
146
  // When the registering span sits below an OTel `gen_ai.*` ancestor, use
130
147
  // that ancestor as the parent_id fallback and suppress the bridge
131
148
  // parent_id tag so the indexer doesn't invert the trace.
132
149
  const genAIAncestorSpanId = findGenAIAncestorSpanId(span)
133
150
 
134
- writeBridgeTags(span, { includeParentId: genAIAncestorSpanId === null })
151
+ writeBridgeTags(span, { includeParentId: genAIAncestorSpanId === null, llmobsTraceId })
135
152
 
136
153
  this._setTag(span, ML_APP, spanMlApp)
137
154
 
@@ -141,9 +158,7 @@ class LLMObsTagger {
141
158
  if (modelName) this.tagModelName(span, modelName)
142
159
  if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
143
160
 
144
- const traceTags = span.context()._trace.tags
145
- sessionId = sessionId ||
146
- registry.get(parent)?.[SESSION_ID] ||
161
+ sessionId ||= registry.get(parent)?.[SESSION_ID] ||
147
162
  traceTags[SESSION_ID_TRACE_DEFAULT_KEY] ||
148
163
  traceTags[PROPAGATED_SESSION_ID_KEY]
149
164
  if (sessionId) this._setTag(span, SESSION_ID, sessionId)
@@ -500,6 +515,10 @@ class LLMObsTagger {
500
515
  this._setTag(span, MODEL_NAME, modelName)
501
516
  }
502
517
 
518
+ setName (span, name) {
519
+ this._setTag(span, NAME, name)
520
+ }
521
+
503
522
  #tagText (span, data, key) {
504
523
  if (data) {
505
524
  if (typeof data === 'string') {
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const id = require('../id')
3
4
  const log = require('../log')
4
5
  const {
5
6
  LLMOBS_PARENT_ID_BRIDGE_KEY,
@@ -7,6 +8,10 @@ const {
7
8
  SPAN_KINDS,
8
9
  } = require('./constants/tags')
9
10
 
11
+ const DECIMAL_TRACE_ID_REGEX = /^\d+$/
12
+ const HEX_TRACE_ID_REGEX = /^[0-9a-f]{32}$/i
13
+ const MAX_UINT_64 = (1n << 64n) - 1n
14
+
10
15
  // LLM I/O is overwhelmingly ASCII (English prompts and code). Walk once
11
16
  // looking for the first non-ASCII char; if there is none, hand the input
12
17
  // straight back. Otherwise pick up the slow path from the byte that needed
@@ -312,12 +317,12 @@ function safeJsonParse (value, fallback) {
312
317
  // LLMObs root and hoists the gen_ai ancestors under it, inverting the trace.
313
318
  /**
314
319
  * @param {import('../opentracing/span')} span
315
- * @param {{ includeParentId?: boolean }} [opts]
320
+ * @param {{ includeParentId?: boolean, llmobsTraceId?: string }} [opts]
316
321
  */
317
- function writeBridgeTags (span, { includeParentId = true } = {}) {
322
+ function writeBridgeTags (span, { includeParentId = true, llmobsTraceId } = {}) {
318
323
  const traceTags = span?.context?.()._trace?.tags
319
324
  if (!traceTags || traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY]) return
320
- traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = span.context().toTraceId(true)
325
+ traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = llmobsTraceId ?? span.context().toTraceId(true)
321
326
  if (includeParentId) {
322
327
  traceTags[LLMOBS_PARENT_ID_BRIDGE_KEY] = span.context().toSpanId()
323
328
  }
@@ -362,6 +367,51 @@ function findGenAIAncestorSpanId (span) {
362
367
  return null
363
368
  }
364
369
 
370
+ /**
371
+ * Generate a 128-bit LLMObs trace ID with the span start time encoded in its high bits.
372
+ * @param {number} startTime
373
+ * @returns {string}
374
+ */
375
+ function generateLlmObsTraceId (startTime) {
376
+ const identifier = id()
377
+ const traceIdHigh = Math.floor(startTime / 1000)
378
+ .toString(16)
379
+ .padStart(8, '0')
380
+ .padEnd(16, '0')
381
+
382
+ return identifier.toTraceIdHex(traceIdHigh).padStart(32, '0')
383
+ }
384
+
385
+ /**
386
+ * Convert an internally stored hexadecimal LLMObs trace ID to its distributed wire representation.
387
+ * @param {string | undefined} traceId
388
+ * @returns {string | undefined}
389
+ */
390
+ function llmObsTraceIdToWire (traceId) {
391
+ if (!traceId) return
392
+ if (!HEX_TRACE_ID_REGEX.test(traceId)) return traceId
393
+
394
+ return BigInt(`0x${traceId}`).toString(10)
395
+ }
396
+
397
+ /**
398
+ * Normalize a distributed LLMObs trace ID to the representation expected by LLMObs span events.
399
+ * @param {string | undefined} traceId
400
+ * @returns {string | undefined}
401
+ */
402
+ function normalizeLlmObsTraceId (traceId) {
403
+ if (!traceId) return
404
+
405
+ if (HEX_TRACE_ID_REGEX.test(traceId) && (traceId[0] === '0' || !DECIMAL_TRACE_ID_REGEX.test(traceId))) {
406
+ return traceId
407
+ }
408
+
409
+ if (!DECIMAL_TRACE_ID_REGEX.test(traceId)) return traceId
410
+
411
+ const identifier = BigInt(traceId)
412
+ return identifier > MAX_UINT_64 ? identifier.toString(16).padStart(32, '0') : traceId
413
+ }
414
+
365
415
  // Maps an audio `format` (e.g. "wav", "mp3") to a MIME type. Defaults to `audio/wav` when the
366
416
  // format is missing. Provider-specific overrides (e.g. OpenAI's mp3 -> audio/mpeg) are passed in
367
417
  // via `mimeTypeLookup` so this stays provider-agnostic. A non-string `format` is treated as missing
@@ -396,6 +446,9 @@ module.exports = {
396
446
  audioMimeTypeFromFormat,
397
447
  encodeUnicode,
398
448
  findGenAIAncestorSpanId,
449
+ generateLlmObsTraceId,
450
+ llmObsTraceIdToWire,
451
+ normalizeLlmObsTraceId,
399
452
  formatAudioPart,
400
453
  validateCostTags,
401
454
  validateKind,
@@ -49,12 +49,18 @@ class LLMObsSpanWriter extends BaseWriter {
49
49
  }
50
50
 
51
51
  makePayload (events) {
52
- return events.map(event => ({
53
- '_dd.stage': 'raw',
54
- '_dd.tracer_version': tracerVersion,
55
- event_type: this._eventType,
56
- spans: [event],
57
- }))
52
+ return events.map(event => {
53
+ const eventData = {
54
+ '_dd.stage': 'raw',
55
+ '_dd.tracer_version': tracerVersion,
56
+ event_type: this._eventType,
57
+ spans: [event],
58
+ }
59
+ if (event?._dd?.scope === 'experiments') {
60
+ eventData['_dd.scope'] = 'experiments'
61
+ }
62
+ return eventData
63
+ })
58
64
  }
59
65
 
60
66
  _truncateSpanEvent (event) {
@@ -55,7 +55,7 @@ class NoopProxy {
55
55
 
56
56
  if (typeof fn !== 'function') return
57
57
 
58
- options = options || {}
58
+ options ||= {}
59
59
 
60
60
  return this._tracer.trace(name, options, fn)
61
61
  }
@@ -68,7 +68,7 @@ class NoopProxy {
68
68
 
69
69
  if (typeof fn !== 'function') return fn
70
70
 
71
- options = options || {}
71
+ options ||= {}
72
72
 
73
73
  return this._tracer.wrap(name, options, fn)
74
74
  }
@@ -20,7 +20,7 @@ const RETRY_JITTER = 0.2
20
20
  * @property {URL} endpoint
21
21
  * @property {number} pollIntervalMs
22
22
  * @property {number} requestTimeoutMs
23
- * @property {string} apiKey
23
+ * @property {string | undefined} apiKey
24
24
  */
25
25
 
26
26
  /**
@@ -138,7 +138,7 @@ class AgentlessConfigurationSource {
138
138
  #request (signal) {
139
139
  const headers = getClientLibraryHeaders()
140
140
  headers['Accept-Encoding'] = 'gzip'
141
- headers['DD-API-KEY'] = this.#config.apiKey
141
+ if (this.#config.apiKey) headers['DD-API-KEY'] = this.#config.apiKey
142
142
  if (this.#etag) headers['If-None-Match'] = this.#etag
143
143
 
144
144
  /**
@@ -228,10 +228,7 @@ class AgentlessConfigurationSource {
228
228
  this.#failureWarnings.add(category)
229
229
 
230
230
  if (statusCode === 401 || statusCode === 403) {
231
- log.warn(
232
- 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid',
233
- statusCode
234
- )
231
+ log.warn('Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', statusCode)
235
232
  } else if (statusCode) {
236
233
  log.warn('Feature Flagging agentless endpoint returned HTTP %d after %d attempts', statusCode, attempts)
237
234
  } else if (attempts > 1) {