dd-trace 6.3.0 → 6.5.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 (207) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/index.electron.js +3 -0
  52. package/init.js +18 -0
  53. package/initialize.mjs +1 -1
  54. package/loader-hook.mjs +28 -18
  55. package/openfeature.d.ts +1 -0
  56. package/openfeature.js +4 -0
  57. package/package.json +15 -8
  58. package/packages/datadog-instrumentations/src/ai.js +37 -26
  59. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  60. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  61. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  62. package/packages/datadog-instrumentations/src/cucumber.js +24 -10
  63. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  64. package/packages/datadog-instrumentations/src/express.js +20 -2
  65. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  66. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  68. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  69. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  70. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  71. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  72. package/packages/datadog-instrumentations/src/jest.js +36 -4
  73. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  74. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  75. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  76. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  77. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  78. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  79. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  80. package/packages/datadog-instrumentations/src/next.js +24 -0
  81. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  82. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  83. package/packages/datadog-instrumentations/src/pg.js +2 -2
  84. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  85. package/packages/datadog-instrumentations/src/process.js +3 -0
  86. package/packages/datadog-instrumentations/src/router.js +191 -106
  87. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  88. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  89. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  90. package/packages/datadog-instrumentations/src/ws.js +2 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  92. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  94. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  95. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  96. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
  97. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
  99. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  100. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  101. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  102. package/packages/datadog-plugin-graphql/src/execute.js +177 -59
  103. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  104. package/packages/datadog-plugin-graphql/src/request.js +32 -1
  105. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  106. package/packages/datadog-plugin-graphql/src/validate.js +15 -3
  107. package/packages/datadog-plugin-http2/src/client.js +1 -1
  108. package/packages/datadog-plugin-http2/src/server.js +38 -6
  109. package/packages/datadog-plugin-jest/src/util.js +21 -6
  110. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  111. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  112. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  113. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  114. package/packages/datadog-plugin-router/src/index.js +11 -2
  115. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  116. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  117. package/packages/datadog-plugin-ws/src/close.js +2 -1
  118. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  119. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  120. package/packages/datadog-plugin-ws/src/server.js +2 -1
  121. package/packages/dd-trace/index.electron.js +3 -0
  122. package/packages/dd-trace/index.js +2 -37
  123. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  125. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  126. package/packages/dd-trace/src/azure_metadata.js +2 -1
  127. package/packages/dd-trace/src/bootstrap.js +39 -0
  128. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  129. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  132. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  135. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  137. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  138. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
  139. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
  140. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  141. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  142. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  143. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  144. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  145. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
  146. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  147. package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
  148. package/packages/dd-trace/src/config/helper.js +1 -0
  149. package/packages/dd-trace/src/config/index.js +10 -0
  150. package/packages/dd-trace/src/config/supported-configurations.json +83 -0
  151. package/packages/dd-trace/src/constants.js +7 -0
  152. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  153. package/packages/dd-trace/src/exporter.js +2 -0
  154. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  155. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  156. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  157. package/packages/dd-trace/src/feature-registry.js +29 -0
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  159. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  160. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  161. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  162. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  163. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  164. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  165. package/packages/dd-trace/src/llmobs/index.js +10 -2
  166. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  167. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  168. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  169. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  170. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  171. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  172. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  173. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  174. package/packages/dd-trace/src/llmobs/tagger.js +79 -7
  175. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  176. package/packages/dd-trace/src/llmobs/util.js +32 -0
  177. package/packages/dd-trace/src/noop/proxy.js +5 -4
  178. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  179. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  180. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  181. package/packages/dd-trace/src/openfeature/index.js +0 -2
  182. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  183. package/packages/dd-trace/src/openfeature/register.js +35 -0
  184. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  185. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  186. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  187. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  188. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  189. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  190. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  191. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  192. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  193. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  194. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  195. package/packages/dd-trace/src/plugins/util/test.js +65 -8
  196. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  197. package/packages/dd-trace/src/proxy.js +99 -10
  198. package/packages/dd-trace/src/span_processor.js +5 -0
  199. package/packages/dd-trace/src/standalone/index.js +0 -36
  200. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  201. package/packages/dd-trace/src/util.js +26 -0
  202. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  203. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -7,11 +7,13 @@ const {
7
7
  INSTRUMENTATION_METHOD_AUTO,
8
8
  UNKNOWN_MODEL_PROVIDER,
9
9
  } = require('../../constants/tags')
10
- const { safeJsonParse } = require('../../util')
10
+ const { audioMimeTypeFromFormat, formatAudioPart, safeJsonParse } = require('../../util')
11
+ const { AUDIO_MIME_TYPES } = require('./constants')
11
12
  const {
12
13
  extractChatTemplateFromInstructions,
13
14
  normalizePromptVariables,
14
15
  extractTextFromContentItem,
16
+ extractContentParts,
15
17
  hasMultimodalInputs,
16
18
  } = require('./utils')
17
19
 
@@ -29,6 +31,22 @@ function isIterable (obj) {
29
31
  return typeof obj[Symbol.iterator] === 'function'
30
32
  }
31
33
 
34
+ // Flattens multimodal chat input messages (array `content`) into readable text plus structured
35
+ // `audioParts`, leaving plain-string messages untouched. Model-agnostic: keys off message
36
+ // structure, so it works for any audio-capable chat model (gpt-audio*, gpt-4o-audio-preview, ...).
37
+ function flattenChatInputMessages (messages) {
38
+ if (!Array.isArray(messages)) return messages
39
+
40
+ return messages.map(message => {
41
+ if (!Array.isArray(message?.content)) return message
42
+
43
+ const { content, audioParts } = extractContentParts(message.content)
44
+ const flattenedMessage = { ...message, content }
45
+ if (audioParts.length) flattenedMessage.audioParts = audioParts
46
+ return flattenedMessage
47
+ })
48
+ }
49
+
32
50
  class OpenAiLLMObsPlugin extends LLMObsPlugin {
33
51
  static id = 'openai'
34
52
  static integration = 'openai'
@@ -193,47 +211,63 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
193
211
 
194
212
  this._tagger.tagMetadata(span, metadata)
195
213
 
214
+ const inputMessages = flattenChatInputMessages(messages)
215
+
196
216
  if (error) {
197
- this._tagger.tagLLMIO(span, messages, [{ content: '' }])
217
+ this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
198
218
  return
199
219
  }
200
220
 
201
221
  const outputMessages = []
202
222
  const { choices } = response
203
223
  if (!isIterable(choices)) {
204
- this._tagger.tagLLMIO(span, messages, [{ content: '' }])
224
+ this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
205
225
  return
206
226
  }
207
227
 
228
+ // Output audio (non-streamed) is returned in the requested format; chat-completions
229
+ const outputAudioFormat = inputs.audio?.format
230
+
208
231
  for (const choice of choices) {
209
232
  const message = choice.message || choice.delta
210
- const content = message.content || ''
233
+ let content = message.content || ''
211
234
  const role = message.role
212
235
 
236
+ const audio = message.audio
237
+ let audioParts
238
+ if (audio) {
239
+ if (audio.data) {
240
+ audioParts = [formatAudioPart(audio.data, audioMimeTypeFromFormat(outputAudioFormat, AUDIO_MIME_TYPES))]
241
+ }
242
+ // gpt-audio* / gpt-4o-audio-preview return null content; surface the transcript as text.
243
+ if (!content) content = audio.transcript || ''
244
+ }
245
+
246
+ const outputMessage = { content, role }
247
+ if (audioParts) outputMessage.audioParts = audioParts
248
+
213
249
  if (message.function_call) {
214
- const functionCallInfo = {
250
+ outputMessage.toolCalls = [{
215
251
  name: message.function_call.name,
216
252
  arguments: safeJsonParse(message.function_call.arguments),
217
- }
218
- outputMessages.push({ content, role, toolCalls: [functionCallInfo] })
253
+ }]
219
254
  } else if (message.tool_calls) {
220
255
  const toolCallsInfo = []
221
256
  for (const toolCall of message.tool_calls) {
222
- const toolCallInfo = {
257
+ toolCallsInfo.push({
223
258
  arguments: safeJsonParse(toolCall.function.arguments),
224
259
  name: toolCall.function.name,
225
260
  toolId: toolCall.id,
226
261
  type: toolCall.type,
227
- }
228
- toolCallsInfo.push(toolCallInfo)
262
+ })
229
263
  }
230
- outputMessages.push({ content, role, toolCalls: toolCallsInfo })
231
- } else {
232
- outputMessages.push({ content, role })
264
+ outputMessage.toolCalls = toolCallsInfo
233
265
  }
266
+
267
+ outputMessages.push(outputMessage)
234
268
  }
235
269
 
236
- this._tagger.tagLLMIO(span, messages, outputMessages)
270
+ this._tagger.tagLLMIO(span, inputMessages, outputMessages)
237
271
  }
238
272
 
239
273
  #tagResponse (span, inputs, response, error) {
@@ -1,10 +1,13 @@
1
1
  'use strict'
2
2
 
3
+ const { audioMimeTypeFromFormat, formatAudioPart } = require('../../util')
3
4
  const {
4
5
  INPUT_TYPE_IMAGE,
5
6
  INPUT_TYPE_FILE,
6
7
  IMAGE_FALLBACK,
7
8
  FILE_FALLBACK,
9
+ AUDIO_FALLBACK,
10
+ AUDIO_MIME_TYPES,
8
11
  } = require('./constants')
9
12
 
10
13
  const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g
@@ -118,9 +121,49 @@ function hasMultimodalInputs (variables) {
118
121
  )
119
122
  }
120
123
 
124
+ /**
125
+ * Flattens an array of OpenAI chat message content parts into readable text plus structured audio.
126
+ *
127
+ * Text parts are concatenated (newline-joined),
128
+ * images collapse to an `[image]` marker, and `input_audio` parts with data are captured as audio
129
+ * parts (rendered as a player). The `[audio]` marker is only emitted as a fallback when an audio
130
+ * part carries no data.
131
+ *
132
+ * @param {Array<object>} parts - Array of content parts from a chat message `content`
133
+ * @returns {{ content: string, audioParts: Array<{ mimeType: string, content: string }> }}
134
+ */
135
+ function extractContentParts (parts) {
136
+ const extracted = []
137
+ const audioParts = []
138
+
139
+ for (const part of parts) {
140
+ const partType = part?.type ?? ''
141
+ if (partType === 'text') {
142
+ extracted.push(part.text ?? '')
143
+ } else if (partType === 'image_url') {
144
+ extracted.push(IMAGE_FALLBACK)
145
+ } else if (partType === 'input_audio') {
146
+ const inputAudio = part.input_audio ?? {}
147
+ const data = inputAudio.data
148
+ if (data) {
149
+ // Audio is captured as a structured audio part (rendered as a player), so no text marker
150
+ // is needed. Only fall back to "[audio]" when there's no audio to capture.
151
+ audioParts.push(formatAudioPart(data, audioMimeTypeFromFormat(inputAudio.format, AUDIO_MIME_TYPES)))
152
+ } else {
153
+ extracted.push(AUDIO_FALLBACK)
154
+ }
155
+ } else {
156
+ extracted.push(`[${partType}]`)
157
+ }
158
+ }
159
+
160
+ return { content: extracted.join('\n'), audioParts }
161
+ }
162
+
121
163
  module.exports = {
122
164
  extractChatTemplateFromInstructions,
123
165
  normalizePromptVariables,
124
166
  extractTextFromContentItem,
167
+ extractContentParts,
125
168
  hasMultimodalInputs,
126
169
  }
@@ -19,6 +19,7 @@ const {
19
19
  const { storage } = require('./storage')
20
20
  const telemetry = require('./telemetry')
21
21
  const LLMObsTagger = require('./tagger')
22
+ const { createExperiments } = require('./experiments')
22
23
 
23
24
  // communicating with writer
24
25
  const evalMetricAppendCh = channel('llmobs:eval-metric:append')
@@ -33,6 +34,12 @@ class LLMObs extends NoopLLMObs {
33
34
  */
34
35
  #hasUserSpanProcessor = false
35
36
 
37
+ /**
38
+ * Lazily-created experiments facade (see ./experiments).
39
+ * @type {import('./experiments').Experiments | undefined}
40
+ */
41
+ #experiments
42
+
36
43
  /**
37
44
  * @param {import('../tracer')} tracer - Tracer instance
38
45
  * @param {import('./index')} llmobsModule - LLMObs module instance
@@ -52,6 +59,16 @@ class LLMObs extends NoopLLMObs {
52
59
  return this._config.llmobs.DD_LLMOBS_ENABLED ?? false
53
60
  }
54
61
 
62
+ /**
63
+ * Datasets & Experiments API. Requires LLM Observability to be enabled and
64
+ * DD_API_KEY / DD_APP_KEY to be set; otherwise the returned facade throws with
65
+ * a clear message on use.
66
+ */
67
+ get experiments () {
68
+ this.#experiments ??= createExperiments(this._config)
69
+ return this.#experiments
70
+ }
71
+
55
72
  enable (options = {}) {
56
73
  logger.warn(
57
74
  'Enabling LLM Observability via `llmobs.enable()` is deprecated and will be removed in dd-trace@7.0.0. ' +
@@ -7,6 +7,7 @@ const {
7
7
  MODEL_NAME,
8
8
  MODEL_PROVIDER,
9
9
  SESSION_ID,
10
+ SESSION_ID_TRACE_DEFAULT_KEY,
10
11
  ML_APP,
11
12
  SPAN_KIND,
12
13
  INPUT_VALUE,
@@ -141,7 +142,10 @@ class LLMObsTagger {
141
142
  if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
142
143
 
143
144
  const traceTags = span.context()._trace.tags
144
- sessionId = sessionId || registry.get(parent)?.[SESSION_ID] || traceTags[PROPAGATED_SESSION_ID_KEY]
145
+ sessionId = sessionId ||
146
+ registry.get(parent)?.[SESSION_ID] ||
147
+ traceTags[SESSION_ID_TRACE_DEFAULT_KEY] ||
148
+ traceTags[PROPAGATED_SESSION_ID_KEY]
145
149
  if (sessionId) this._setTag(span, SESSION_ID, sessionId)
146
150
  if (integration) this._setTag(span, INTEGRATION, integration)
147
151
  if (_decorator) this._setTag(span, DECORATOR, _decorator)
@@ -614,6 +618,65 @@ class LLMObsTagger {
614
618
  return filteredToolResults
615
619
  }
616
620
 
621
+ // Validates audio segments on a message and emits the snake_case wire shape
622
+ // `{ mime_type, content | attachment_key }`. Mirrors dd-trace-py's
623
+ // `_extract_audio_part`: a part requires `mimeType` and exactly one of
624
+ // `content` (inline base64) or `attachmentKey` (backend-offloaded).
625
+ #filterAudioParts (audioParts) {
626
+ if (!Array.isArray(audioParts)) {
627
+ audioParts = [audioParts]
628
+ }
629
+
630
+ const filteredAudioParts = []
631
+ for (const audioPart of audioParts) {
632
+ if (audioPart == null || typeof audioPart !== 'object') {
633
+ this.#handleFailure('Audio part must be an object.', 'invalid_io_messages')
634
+ continue
635
+ }
636
+
637
+ const { mimeType, content, attachmentKey } = audioPart
638
+
639
+ if (typeof mimeType !== 'string' || !mimeType) {
640
+ this.#handleFailure('Audio part mimeType must be a non-empty string.', 'invalid_io_messages')
641
+ continue
642
+ }
643
+
644
+ if (content == null && attachmentKey == null) {
645
+ this.#handleFailure("Audio part must have either 'content' or 'attachmentKey'.", 'invalid_io_messages')
646
+ continue
647
+ }
648
+
649
+ if (content != null && attachmentKey != null) {
650
+ this.#handleFailure(
651
+ "Audio part must have only one of 'content' or 'attachmentKey', not both.", 'invalid_io_messages'
652
+ )
653
+ continue
654
+ }
655
+
656
+ const audioPartObj = { mime_type: mimeType }
657
+
658
+ // Exactly one of content / attachmentKey is set here (guarded above). Validate its type
659
+ // explicitly so the failure carries the `invalid_io_messages` tag for telemetry, instead
660
+ // of routing through `#tagConditionalString` which omits it.
661
+ if (content == null) {
662
+ if (typeof attachmentKey !== 'string') {
663
+ this.#handleFailure('Audio part attachmentKey must be a string.', 'invalid_io_messages')
664
+ continue
665
+ }
666
+ audioPartObj.attachment_key = attachmentKey
667
+ } else {
668
+ if (typeof content !== 'string') {
669
+ this.#handleFailure('Audio part content must be a base64-encoded string.', 'invalid_io_messages')
670
+ continue
671
+ }
672
+ audioPartObj.content = content
673
+ }
674
+
675
+ filteredAudioParts.push(audioPartObj)
676
+ }
677
+ return filteredAudioParts
678
+ }
679
+
617
680
  #tagMessages (span, data, key) {
618
681
  if (!data) {
619
682
  return
@@ -640,6 +703,7 @@ class LLMObsTagger {
640
703
  toolCalls,
641
704
  toolResults,
642
705
  toolId,
706
+ audioParts,
643
707
  } = message
644
708
  const messageObj = {}
645
709
 
@@ -673,6 +737,14 @@ class LLMObsTagger {
673
737
  }
674
738
  }
675
739
 
740
+ if (audioParts != null) {
741
+ const filteredAudioParts = this.#filterAudioParts(audioParts)
742
+
743
+ if (filteredAudioParts.length) {
744
+ messageObj.audio_parts = filteredAudioParts
745
+ }
746
+ }
747
+
676
748
  if (toolId) {
677
749
  if (role === 'tool') {
678
750
  condition = this.#tagConditionalString(toolId, 'Tool ID', messageObj, 'tool_id') && condition
@@ -756,14 +828,14 @@ class LLMObsTagger {
756
828
  tagsCarrier[key] = value
757
829
 
758
830
  // The first session set in a trace becomes the trace-level default, stored on the trace-shared
759
- // propagating tags so later spans (incl. those under a session-less parent) inherit it and it
760
- // rides `x-datadog-tags` across service boundaries. Established here, the single choke point for
761
- // session writes, so sessions post-populated by integrations after span start also seed it.
762
- // First-writer wins, so an explicit session still overrides locally.
831
+ // tags so later spans (incl. those under a session-less parent) inherit it in-process.
832
+ // Established here, the single choke point for session writes, so sessions post-populated by
833
+ // integrations after span start also seed it. First-writer wins, so an explicit session still
834
+ // overrides locally. Cross-service injection is handled centrally in `handleLLMObsInjection`.
763
835
  if (key === SESSION_ID && value) {
764
836
  const traceTags = span.context()._trace.tags
765
- if (traceTags[PROPAGATED_SESSION_ID_KEY] === undefined) {
766
- traceTags[PROPAGATED_SESSION_ID_KEY] = value
837
+ if (traceTags[SESSION_ID_TRACE_DEFAULT_KEY] === undefined) {
838
+ traceTags[SESSION_ID_TRACE_DEFAULT_KEY] = value
767
839
  }
768
840
  }
769
841
  }
@@ -3,6 +3,7 @@
3
3
  const ERROR_TYPE = require('../constants')
4
4
 
5
5
  const telemetryMetrics = require('../telemetry/metrics')
6
+ const { getSegment } = require('../util')
6
7
  const {
7
8
  SPAN_KIND,
8
9
  MODEL_PROVIDER,
@@ -22,7 +23,7 @@ function extractIntegrationFromTags (tags) {
22
23
  if (!Array.isArray(tags)) return null
23
24
  const integrationTag = tags.find(tag => tag.startsWith('integration:'))
24
25
  if (!integrationTag) return null
25
- return integrationTag.split(':')[1] || null
26
+ return getSegment(integrationTag, ':', 1) || null
26
27
  }
27
28
 
28
29
  function extractTagsFromSpanEvent (event) {
@@ -362,9 +362,41 @@ function findGenAIAncestorSpanId (span) {
362
362
  return null
363
363
  }
364
364
 
365
+ // Maps an audio `format` (e.g. "wav", "mp3") to a MIME type. Defaults to `audio/wav` when the
366
+ // format is missing. Provider-specific overrides (e.g. OpenAI's mp3 -> audio/mpeg) are passed in
367
+ // via `mimeTypeLookup` so this stays provider-agnostic. A non-string `format` is treated as missing
368
+ // so a malformed auto-instrumented payload can't throw and disable the plugin.
369
+ /**
370
+ * @param {string} fmt
371
+ * @param {Record<string, string>} [mimeTypeLookup]
372
+ * @returns {string}
373
+ */
374
+ function audioMimeTypeFromFormat (fmt, mimeTypeLookup = {}) {
375
+ fmt = typeof fmt === 'string' ? fmt.trim().toLowerCase() : ''
376
+ if (!fmt) return 'audio/wav'
377
+ return mimeTypeLookup[fmt] ?? `audio/${fmt}`
378
+ }
379
+
380
+ // Builds an audio part from raw audio bytes (base64-encoded) or an existing base64 string. Only
381
+ // Buffer/Uint8Array inputs are base64-encoded; any other shape is passed through so a malformed
382
+ // auto-instrumented payload can't throw (the tagger soft-skips a non-string `content`).
383
+ /**
384
+ * @param {Buffer | Uint8Array | string} data
385
+ * @param {string} mimeType
386
+ * @returns {{ mimeType: string, content: string }}
387
+ */
388
+ function formatAudioPart (data, mimeType) {
389
+ const content = Buffer.isBuffer(data) || ArrayBuffer.isView(data)
390
+ ? Buffer.from(data).toString('base64')
391
+ : data
392
+ return { mimeType, content }
393
+ }
394
+
365
395
  module.exports = {
396
+ audioMimeTypeFromFormat,
366
397
  encodeUnicode,
367
398
  findGenAIAncestorSpanId,
399
+ formatAudioPart,
368
400
  validateCostTags,
369
401
  validateKind,
370
402
  getFunctionArguments,
@@ -1,8 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const { features } = require('../feature-registry')
3
4
  const NoopAppsecSdk = require('../appsec/sdk/noop')
4
5
  const NoopLLMObsSDK = require('../llmobs/noop')
5
- const NoopFlaggingProvider = require('../openfeature/noop')
6
6
  const NoopAIGuardSDK = require('../aiguard/noop')
7
7
  const NoopDogStatsDClient = require('./dogstatsd')
8
8
  const NoopTracer = require('./tracer')
@@ -11,7 +11,6 @@ const noop = new NoopTracer()
11
11
  const noopAppsec = new NoopAppsecSdk()
12
12
  const noopDogStatsDClient = new NoopDogStatsDClient()
13
13
  const noopLLMObs = new NoopLLMObsSDK(noop)
14
- const noopOpenFeatureProvider = new NoopFlaggingProvider()
15
14
  const noopAIGuard = new NoopAIGuardSDK()
16
15
  const noopProfiling = {
17
16
  setCustomLabelKeys () {},
@@ -25,8 +24,10 @@ class NoopProxy {
25
24
  this.appsec = noopAppsec
26
25
  this.dogstatsd = noopDogStatsDClient
27
26
  this.llmobs = noopLLMObs
28
- this.openfeature = noopOpenFeatureProvider
29
27
  this.aiguard = noopAIGuard
28
+ for (const { name, noop } of Object.values(features)) {
29
+ this[name] = noop
30
+ }
30
31
  this.setBaggageItem = (key, value) => {}
31
32
  this.getBaggageItem = (key) => {}
32
33
  this.getAllBaggageItems = () => {}
@@ -82,7 +83,7 @@ class NoopProxy {
82
83
  }
83
84
 
84
85
  inject () {
85
- return this._tracer.inject.apply(this._tracer, arguments)
86
+ this._tracer.inject.apply(this._tracer, arguments)
86
87
  }
87
88
 
88
89
  extract () {