dd-trace 5.104.0 → 5.106.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 (159) hide show
  1. package/LICENSE-3rdparty.csv +90 -102
  2. package/index.d.ts +82 -3
  3. package/package.json +15 -15
  4. package/packages/datadog-core/src/storage.js +1 -1
  5. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  6. package/packages/datadog-instrumentations/src/ai.js +8 -7
  7. package/packages/datadog-instrumentations/src/aws-sdk.js +16 -2
  8. package/packages/datadog-instrumentations/src/azure-cosmos.js +7 -0
  9. package/packages/datadog-instrumentations/src/azure-functions.js +3 -0
  10. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +19 -0
  11. package/packages/datadog-instrumentations/src/cucumber.js +390 -157
  12. package/packages/datadog-instrumentations/src/dns.js +54 -18
  13. package/packages/datadog-instrumentations/src/fastify.js +142 -82
  14. package/packages/datadog-instrumentations/src/graphql.js +188 -62
  15. package/packages/datadog-instrumentations/src/helpers/ai-messages.js +322 -14
  16. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  17. package/packages/datadog-instrumentations/src/helpers/instrument.js +2 -1
  18. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +269 -0
  19. package/packages/datadog-instrumentations/src/helpers/promise-instrumentor.js +42 -0
  20. package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -3
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-cosmos.js +50 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langgraph.js +4 -2
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +85 -0
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +37 -236
  27. package/packages/datadog-instrumentations/src/hono.js +54 -3
  28. package/packages/datadog-instrumentations/src/http/server.js +9 -4
  29. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +163 -0
  30. package/packages/datadog-instrumentations/src/jest.js +360 -150
  31. package/packages/datadog-instrumentations/src/kafkajs.js +120 -16
  32. package/packages/datadog-instrumentations/src/mocha/main.js +128 -17
  33. package/packages/datadog-instrumentations/src/nats.js +182 -0
  34. package/packages/datadog-instrumentations/src/nyc.js +38 -1
  35. package/packages/datadog-instrumentations/src/openai.js +33 -18
  36. package/packages/datadog-instrumentations/src/oracledb.js +6 -1
  37. package/packages/datadog-instrumentations/src/pino.js +17 -5
  38. package/packages/datadog-instrumentations/src/playwright.js +515 -292
  39. package/packages/datadog-instrumentations/src/router.js +76 -32
  40. package/packages/datadog-instrumentations/src/stripe.js +1 -1
  41. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -1
  42. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +1 -1
  43. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +218 -4
  44. package/packages/datadog-plugin-azure-cosmos/src/index.js +144 -0
  45. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -1
  46. package/packages/datadog-plugin-azure-functions/src/index.js +5 -2
  47. package/packages/datadog-plugin-azure-service-bus/src/producer.js +1 -1
  48. package/packages/datadog-plugin-bunyan/src/index.js +28 -0
  49. package/packages/datadog-plugin-cucumber/src/index.js +17 -3
  50. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +199 -28
  51. package/packages/datadog-plugin-cypress/src/support.js +69 -1
  52. package/packages/datadog-plugin-dns/src/lookup.js +8 -6
  53. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +1 -1
  54. package/packages/datadog-plugin-graphql/src/execute.js +2 -0
  55. package/packages/datadog-plugin-graphql/src/resolve.js +64 -67
  56. package/packages/datadog-plugin-http/src/server.js +40 -15
  57. package/packages/datadog-plugin-jest/src/index.js +11 -3
  58. package/packages/datadog-plugin-jest/src/util.js +15 -8
  59. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +1 -1
  60. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -0
  61. package/packages/datadog-plugin-langgraph/src/stream.js +1 -1
  62. package/packages/datadog-plugin-mocha/src/index.js +19 -4
  63. package/packages/datadog-plugin-mongodb-core/src/index.js +281 -40
  64. package/packages/datadog-plugin-nats/src/consumer.js +43 -0
  65. package/packages/datadog-plugin-nats/src/index.js +20 -0
  66. package/packages/datadog-plugin-nats/src/producer.js +62 -0
  67. package/packages/datadog-plugin-nats/src/util.js +33 -0
  68. package/packages/datadog-plugin-next/src/index.js +5 -3
  69. package/packages/datadog-plugin-openai/src/tracing.js +15 -2
  70. package/packages/datadog-plugin-oracledb/src/index.js +13 -2
  71. package/packages/datadog-plugin-pino/src/index.js +42 -0
  72. package/packages/datadog-plugin-playwright/src/index.js +4 -4
  73. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -1
  74. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  75. package/packages/datadog-plugin-router/src/index.js +33 -44
  76. package/packages/datadog-plugin-selenium/src/index.js +1 -1
  77. package/packages/datadog-plugin-vitest/src/index.js +5 -13
  78. package/packages/datadog-plugin-winston/src/index.js +30 -0
  79. package/packages/datadog-shimmer/src/shimmer.js +33 -40
  80. package/packages/dd-trace/src/aiguard/index.js +1 -1
  81. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  82. package/packages/dd-trace/src/appsec/api_security_sampler.js +1 -1
  83. package/packages/dd-trace/src/appsec/index.js +1 -1
  84. package/packages/dd-trace/src/appsec/reporter.js +5 -6
  85. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -1
  86. package/packages/dd-trace/src/appsec/sdk/utils.js +1 -1
  87. package/packages/dd-trace/src/appsec/user_tracking.js +5 -4
  88. package/packages/dd-trace/src/baggage.js +7 -1
  89. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +0 -1
  90. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +25 -13
  91. package/packages/dd-trace/src/ci-visibility/test-optimization-cache.js +70 -6
  92. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -2
  93. package/packages/dd-trace/src/config/supported-configurations.json +27 -8
  94. package/packages/dd-trace/src/datastreams/writer.js +2 -4
  95. package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -8
  96. package/packages/dd-trace/src/encode/0.4.js +124 -108
  97. package/packages/dd-trace/src/encode/0.5.js +114 -26
  98. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +31 -23
  99. package/packages/dd-trace/src/encode/agentless-json.js +4 -2
  100. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +32 -13
  101. package/packages/dd-trace/src/encode/span-stats.js +16 -16
  102. package/packages/dd-trace/src/encode/tags-processors.js +16 -0
  103. package/packages/dd-trace/src/id.js +15 -0
  104. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +92 -6
  105. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +43 -21
  106. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  107. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  108. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +9 -7
  109. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  110. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +1 -1
  111. package/packages/dd-trace/src/llmobs/sdk.js +0 -16
  112. package/packages/dd-trace/src/llmobs/span_processor.js +3 -3
  113. package/packages/dd-trace/src/llmobs/tagger.js +9 -1
  114. package/packages/dd-trace/src/llmobs/telemetry.js +1 -1
  115. package/packages/dd-trace/src/llmobs/util.js +66 -3
  116. package/packages/dd-trace/src/log/index.js +1 -1
  117. package/packages/dd-trace/src/msgpack/chunk.js +394 -10
  118. package/packages/dd-trace/src/msgpack/index.js +96 -2
  119. package/packages/dd-trace/src/openfeature/encoding.js +70 -0
  120. package/packages/dd-trace/src/openfeature/flagging_provider.js +20 -0
  121. package/packages/dd-trace/src/openfeature/span-enrichment-hook.js +143 -0
  122. package/packages/dd-trace/src/openfeature/span-enrichment.js +149 -0
  123. package/packages/dd-trace/src/opentelemetry/span-helpers.js +4 -3
  124. package/packages/dd-trace/src/opentelemetry/span.js +1 -1
  125. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +22 -3
  126. package/packages/dd-trace/src/opentracing/propagation/log.js +18 -7
  127. package/packages/dd-trace/src/opentracing/propagation/text_map.js +64 -77
  128. package/packages/dd-trace/src/opentracing/span.js +59 -19
  129. package/packages/dd-trace/src/opentracing/span_context.js +50 -3
  130. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -20
  131. package/packages/dd-trace/src/plugins/database.js +7 -6
  132. package/packages/dd-trace/src/plugins/index.js +4 -0
  133. package/packages/dd-trace/src/plugins/log_injection.js +56 -0
  134. package/packages/dd-trace/src/plugins/log_plugin.js +3 -48
  135. package/packages/dd-trace/src/plugins/outbound.js +1 -1
  136. package/packages/dd-trace/src/plugins/plugin.js +15 -17
  137. package/packages/dd-trace/src/plugins/tracing.js +43 -5
  138. package/packages/dd-trace/src/plugins/util/test.js +236 -13
  139. package/packages/dd-trace/src/plugins/util/web.js +79 -65
  140. package/packages/dd-trace/src/priority_sampler.js +2 -2
  141. package/packages/dd-trace/src/profiling/config.js +10 -23
  142. package/packages/dd-trace/src/profiling/exporters/agent.js +11 -10
  143. package/packages/dd-trace/src/profiling/profiler.js +21 -11
  144. package/packages/dd-trace/src/profiling/profilers/wall.js +12 -7
  145. package/packages/dd-trace/src/sampling_rule.js +7 -7
  146. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +10 -0
  147. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +8 -0
  148. package/packages/dd-trace/src/service-naming/source-resolver.js +46 -0
  149. package/packages/dd-trace/src/span_format.js +190 -58
  150. package/packages/dd-trace/src/spanleak.js +1 -1
  151. package/packages/dd-trace/src/standalone/index.js +3 -3
  152. package/packages/dd-trace/src/tagger.js +0 -2
  153. package/vendor/dist/@apm-js-collab/code-transformer/index.js +70 -39
  154. package/vendor/dist/@datadog/sketches-js/LICENSE +10 -36
  155. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  156. package/vendor/dist/protobufjs/index.js +1 -1
  157. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  158. package/packages/dd-trace/src/msgpack/encoder.js +0 -308
  159. package/packages/dd-trace/src/plugins/structured_log_plugin.js +0 -9
@@ -1,5 +1,53 @@
1
1
  'use strict'
2
2
 
3
+ /**
4
+ * Returns the value as a string, JSON-stringifying it when it is not already a string.
5
+ * Returns the value unchanged when it is `null` or `undefined`.
6
+ *
7
+ * @param {unknown} value
8
+ * @returns {string|undefined|null}
9
+ */
10
+ function stringifyIfNeeded (value) {
11
+ if (value == null) return value
12
+ return typeof value === 'string' ? value : JSON.stringify(value)
13
+ }
14
+
15
+ const FILE_FALLBACK = '[file]'
16
+ const IMAGE_FALLBACK = '[image]'
17
+
18
+ const OPENAI_RESPONSE_TOOL_CALL_TYPES = new Set([
19
+ 'apply_patch_call',
20
+ 'code_interpreter_call',
21
+ 'computer_call',
22
+ 'custom_tool_call',
23
+ 'file_search_call',
24
+ 'function_call',
25
+ 'image_generation_call',
26
+ 'local_shell_call',
27
+ 'mcp_call',
28
+ 'shell_call',
29
+ 'web_search_call',
30
+ ])
31
+
32
+ const OPENAI_RESPONSE_TOOL_OUTPUT_TYPES = new Set([
33
+ 'apply_patch_call_output',
34
+ 'computer_call_output',
35
+ 'custom_tool_call_output',
36
+ 'function_call_output',
37
+ 'local_shell_call_output',
38
+ 'shell_call_output',
39
+ ])
40
+
41
+ /**
42
+ * Returns a stringified value, falling back to an empty string for absent values.
43
+ *
44
+ * @param {unknown} value
45
+ * @returns {string}
46
+ */
47
+ function stringifyOrEmpty (value) {
48
+ return stringifyIfNeeded(value) ?? ''
49
+ }
50
+
3
51
  /**
4
52
  * Converts a LanguageModelV2FilePart with an image mediaType to an AI guard style image_url content part.
5
53
  *
@@ -79,12 +127,11 @@ function convertVercelPromptToMessages (prompt) {
79
127
  if (part.type === 'text') {
80
128
  textParts.push(part.text)
81
129
  } else if (part.type === 'tool-call') {
82
- const args = part.args ?? part.input
83
130
  toolCalls.push({
84
131
  id: part.toolCallId,
85
132
  function: {
86
133
  name: part.toolName,
87
- arguments: typeof args === 'string' ? args : JSON.stringify(args),
134
+ arguments: stringifyIfNeeded(part.args ?? part.input),
88
135
  },
89
136
  })
90
137
  }
@@ -103,11 +150,10 @@ function convertVercelPromptToMessages (prompt) {
103
150
 
104
151
  for (const part of msg.content) {
105
152
  if (part.type === 'tool-result') {
106
- const result = part.result ?? part.output
107
153
  messages.push({
108
154
  role: 'tool',
109
155
  tool_call_id: part.toolCallId,
110
- content: typeof result === 'string' ? result : JSON.stringify(result),
156
+ content: stringifyIfNeeded(part.result ?? part.output),
111
157
  })
112
158
  }
113
159
  }
@@ -118,6 +164,58 @@ function convertVercelPromptToMessages (prompt) {
118
164
  return messages
119
165
  }
120
166
 
167
+ /**
168
+ * Converts OpenAI chat-completions messages to the message format expected by AI Guard.
169
+ *
170
+ * Modern `tool_calls` messages already match the expected shape. Deprecated chat
171
+ * completions `function_call` and `function` role messages are normalized to the
172
+ * equivalent tool-call shape so AI Guard can classify them as tool interactions.
173
+ *
174
+ * @param {Array<object>} messages
175
+ * @returns {Array<object>|undefined}
176
+ */
177
+ function normalizeOpenAIChatMessages (messages) {
178
+ if (!Array.isArray(messages) || messages.length === 0) return
179
+
180
+ const normalizedMessages = []
181
+ for (const message of messages) {
182
+ const normalized = normalizeOpenAIChatMessage(message)
183
+ if (normalized) normalizedMessages.push(normalized)
184
+ }
185
+ return normalizedMessages.length ? normalizedMessages : undefined
186
+ }
187
+
188
+ /**
189
+ * Converts one OpenAI chat-completions message to AI Guard's expected shape.
190
+ *
191
+ * @param {object} message
192
+ * @returns {object|undefined}
193
+ */
194
+ function normalizeOpenAIChatMessage (message) {
195
+ if (!message || typeof message !== 'object') return
196
+
197
+ if (message.role === 'function') {
198
+ return {
199
+ role: 'tool',
200
+ tool_call_id: message.tool_call_id ?? message.name,
201
+ content: stringifyOrEmpty(message.content),
202
+ }
203
+ }
204
+
205
+ if (!message.function_call) return message
206
+
207
+ const { function_call: functionCall, ...normalized } = message
208
+ const name = functionCall.name
209
+ normalized.tool_calls ??= [{
210
+ id: message.tool_call_id ?? name,
211
+ function: {
212
+ name,
213
+ arguments: stringifyOrEmpty(functionCall.arguments),
214
+ },
215
+ }]
216
+ return normalized
217
+ }
218
+
121
219
  /**
122
220
  * Converts LLM output tool calls to AI guard style message format.
123
221
  *
@@ -130,16 +228,13 @@ function buildToolCallOutputMessages (inputMessages, toolCalls) {
130
228
  ...inputMessages,
131
229
  {
132
230
  role: 'assistant',
133
- tool_calls: toolCalls.map(tc => {
134
- const args = tc.args ?? tc.input
135
- return {
136
- id: tc.toolCallId,
137
- function: {
138
- name: tc.toolName,
139
- arguments: typeof args === 'string' ? args : JSON.stringify(args),
140
- },
141
- }
142
- }),
231
+ tool_calls: toolCalls.map(tc => ({
232
+ id: tc.toolCallId,
233
+ function: {
234
+ name: tc.toolName,
235
+ arguments: stringifyIfNeeded(tc.args ?? tc.input),
236
+ },
237
+ })),
143
238
  },
144
239
  ]
145
240
  }
@@ -173,10 +268,223 @@ function buildOutputMessages (inputMessages, content) {
173
268
  return inputMessages
174
269
  }
175
270
 
271
+ /**
272
+ * Converts OpenAI Responses API input/output items to OpenAI chat-style messages.
273
+ *
274
+ * @param {string|Array<object>|undefined} items
275
+ * @param {string} defaultRole
276
+ * @returns {Array<object>}
277
+ */
278
+ function convertOpenAIResponseItemsToMessages (items, defaultRole) {
279
+ if (typeof items === 'string') return [{ role: defaultRole, content: items }]
280
+ if (!Array.isArray(items)) return []
281
+
282
+ const messages = []
283
+ for (const item of items) {
284
+ const converted = openAIResponseItemToMessage(item, defaultRole)
285
+ if (Array.isArray(converted)) {
286
+ for (const message of converted) messages.push(message)
287
+ } else if (converted) {
288
+ messages.push(converted)
289
+ }
290
+ }
291
+ return messages
292
+ }
293
+
294
+ /**
295
+ * Converts OpenAI reusable prompt variables to user messages for AI Guard.
296
+ *
297
+ * The reusable prompt template body is not available on the request, but its
298
+ * variables are user/application-provided content that OpenAI substitutes into
299
+ * the prompt. Screening them closes prompt-only `responses.create({ prompt })`
300
+ * calls and prompt variables used alongside `input`.
301
+ *
302
+ * @param {{variables?: Record<string, string|object>|null}|undefined|null} prompt
303
+ * @returns {Array<object>}
304
+ */
305
+ function convertOpenAIResponsePromptToMessages (prompt) {
306
+ const variables = prompt?.variables
307
+ if (!variables || typeof variables !== 'object') return []
308
+
309
+ const messages = []
310
+ for (const value of Object.values(variables)) {
311
+ const content = openAIResponsePromptVariableToMessageContent(value)
312
+ if (content != null) messages.push({ role: 'user', content })
313
+ }
314
+ return messages
315
+ }
316
+
317
+ /**
318
+ * Converts one OpenAI reusable prompt variable value to message content.
319
+ *
320
+ * Routes every variable through `openAIResponseContentToMessageContent` so the
321
+ * result follows the same string-when-text-only / array-when-multimodal shape
322
+ * convention used elsewhere in this file. Media variables that produce no
323
+ * usable content (e.g. an `input_image` with no URL or `file_id`) fall back to
324
+ * a stable text marker so AI Guard still observes that a media variable was
325
+ * attached.
326
+ *
327
+ * @param {string|object} value
328
+ * @returns {string|Array<{type: string, text?: string, image_url?: {url: string}}>|undefined}
329
+ */
330
+ function openAIResponsePromptVariableToMessageContent (value) {
331
+ let part
332
+ if (typeof value === 'string') {
333
+ part = { type: 'input_text', text: value }
334
+ } else if (value && typeof value === 'object') {
335
+ part = value
336
+ } else {
337
+ return
338
+ }
339
+
340
+ const content = openAIResponseContentToMessageContent([part])
341
+ if (content != null) return content
342
+ if (part.type === 'input_image') return IMAGE_FALLBACK
343
+ }
344
+
345
+ /**
346
+ * Converts one OpenAI Responses API item to an OpenAI chat-style message.
347
+ *
348
+ * @param {object} item
349
+ * @param {string} defaultRole
350
+ * @returns {object|Array<object>|undefined}
351
+ */
352
+ function openAIResponseItemToMessage (item, defaultRole) {
353
+ if (!item || typeof item !== 'object') return
354
+ const type = item.type ?? 'message'
355
+
356
+ if (type === 'message') {
357
+ const content = openAIResponseContentToMessageContent(item.content)
358
+ if (content != null) return { role: item.role || defaultRole, content }
359
+ } else if (OPENAI_RESPONSE_TOOL_CALL_TYPES.has(type)) {
360
+ return openAIResponseToolCallToMessages(item)
361
+ } else if (OPENAI_RESPONSE_TOOL_OUTPUT_TYPES.has(type)) {
362
+ return openAIResponseToolOutputToMessage(item)
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Converts a Responses API tool-call item to one or more chat-style messages.
368
+ *
369
+ * Most tool-call items represent only the assistant's tool request. MCP and
370
+ * image-generation items can also carry tool output on the same item, so include
371
+ * a linked tool message when output-like fields are present.
372
+ *
373
+ * @param {object} item
374
+ * @returns {object|Array<object>}
375
+ */
376
+ function openAIResponseToolCallToMessages (item) {
377
+ const toolCallId = item.call_id ?? item.id ?? item.name ?? item.type
378
+ const message = {
379
+ role: 'assistant',
380
+ tool_calls: [{
381
+ id: toolCallId,
382
+ function: {
383
+ name: item.name ?? item.server_label ?? item.type,
384
+ arguments: stringifyOrEmpty(item.arguments ?? item.input ?? item.action),
385
+ },
386
+ }],
387
+ }
388
+
389
+ if (item.output == null && item.result == null && item.error == null) return message
390
+ return [message, openAIResponseToolOutputToMessage(item)]
391
+ }
392
+
393
+ /**
394
+ * Converts a Responses API tool-output item to a chat-style tool message.
395
+ *
396
+ * @param {object} item
397
+ * @returns {object}
398
+ */
399
+ function openAIResponseToolOutputToMessage (item) {
400
+ return {
401
+ role: 'tool',
402
+ tool_call_id: item.call_id ?? item.id,
403
+ content: openAIResponseOutputValueToMessageContent(item.output ?? item.result ?? item.error),
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Converts Responses API tool output to message content.
409
+ *
410
+ * @param {unknown} output
411
+ * @returns {string|Array<{type: string, text?: string, image_url?: {url: string}}>}
412
+ */
413
+ function openAIResponseOutputValueToMessageContent (output) {
414
+ const content = openAIResponseContentToMessageContent(output)
415
+ return content ?? stringifyOrEmpty(output)
416
+ }
417
+
418
+ /**
419
+ * Converts OpenAI Responses API content to OpenAI chat-style message content.
420
+ *
421
+ * @param {string|Array<string|{type?: string, text?: string, refusal?: string,
422
+ * image_url?: string|{url?: string}, file_id?: string, file_url?: string,
423
+ * filename?: string}>|undefined} content
424
+ * @returns {string|Array<{type: string, text?: string, image_url?: {url: string}}>|undefined}
425
+ */
426
+ function openAIResponseContentToMessageContent (content) {
427
+ if (typeof content === 'string') return content
428
+ if (!Array.isArray(content)) return
429
+
430
+ const parts = []
431
+ let hasImages = false
432
+
433
+ for (const part of content) {
434
+ if (!part) continue
435
+ if (typeof part === 'string') {
436
+ parts.push({ type: 'text', text: part })
437
+ } else if ((part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') &&
438
+ typeof part.text === 'string') {
439
+ parts.push({ type: 'text', text: part.text })
440
+ } else if (part.type === 'refusal' && typeof part.refusal === 'string') {
441
+ parts.push({ type: 'text', text: part.refusal })
442
+ } else if (part.type === 'input_image' || part.type === 'image_url') {
443
+ const image = openAIResponseImageContentPart(part)
444
+ if (image) {
445
+ hasImages = true
446
+ parts.push(image)
447
+ }
448
+ } else if (part.type === 'input_file') {
449
+ parts.push({ type: 'text', text: openAIResponseFileContentPart(part) })
450
+ }
451
+ }
452
+
453
+ if (!parts.length) return
454
+ if (hasImages) return parts
455
+ return parts.map(part => part.text).join('\n')
456
+ }
457
+
458
+ /**
459
+ * Converts an OpenAI image content part to AI Guard image_url content.
460
+ *
461
+ * @param {{image_url?: string|{url?: string}, file_id?: string, url?: string}} part
462
+ * @returns {{type: 'image_url', image_url: {url: string}}|undefined}
463
+ */
464
+ function openAIResponseImageContentPart (part) {
465
+ const url = typeof part.image_url === 'string' ? part.image_url : part.image_url?.url ?? part.url
466
+ if (url) return { type: 'image_url', image_url: { url } }
467
+ if (part.file_id) return { type: 'image_url', image_url: { url: part.file_id } }
468
+ }
469
+
470
+ /**
471
+ * Extracts a stable text marker from an OpenAI file content part.
472
+ *
473
+ * @param {{file_id?: string|null, file_url?: string, filename?: string, file_data?: string}} part
474
+ * @returns {string}
475
+ */
476
+ function openAIResponseFileContentPart (part) {
477
+ return part.file_id ?? part.file_url ?? part.filename ?? FILE_FALLBACK
478
+ }
479
+
176
480
  module.exports = {
177
481
  convertVercelPromptToMessages,
178
482
  convertFilePartToImageUrl,
483
+ normalizeOpenAIChatMessages,
179
484
  buildToolCallOutputMessages,
180
485
  buildTextOutputMessages,
181
486
  buildOutputMessages,
487
+ convertOpenAIResponseItemsToMessages,
488
+ convertOpenAIResponsePromptToMessages,
489
+ openAIResponseContentToMessageContent,
182
490
  }
@@ -5,6 +5,7 @@ module.exports = {
5
5
  child_process: () => require('../child_process'),
6
6
  crypto: () => require('../crypto'),
7
7
  dns: () => require('../dns'),
8
+ 'dns/promises': () => require('../dns'),
8
9
  fs: { serverless: false, fn: () => require('../fs') },
9
10
  http: () => require('../http'),
10
11
  http2: () => require('../http2'),
@@ -21,6 +22,7 @@ module.exports = {
21
22
  '@modelcontextprotocol/sdk': () => require('../modelcontextprotocol-sdk'),
22
23
  'apollo-server-core': () => require('../apollo-server-core'),
23
24
  '@aws-sdk/smithy-client': () => require('../aws-sdk'),
25
+ '@azure/cosmos': { esmFirst: true, fn: () => require('../azure-cosmos') },
24
26
  '@azure/event-hubs': () => require('../azure-event-hubs'),
25
27
  '@azure/functions': () => require('../azure-functions'),
26
28
  'durable-functions': () => require('../azure-durable-functions'),
@@ -48,6 +50,7 @@ module.exports = {
48
50
  '@prisma/client': { esmFirst: true, fn: () => require('../prisma') },
49
51
  './runtime/library.js': () => require('../prisma'),
50
52
  '@redis/client': () => require('../redis'),
53
+ '@smithy/core': () => require('../aws-sdk'),
51
54
  '@smithy/smithy-client': () => require('../aws-sdk'),
52
55
  '@vitest/runner': { esmFirst: true, fn: () => require('../vitest') },
53
56
  aerospike: () => require('../aerospike'),
@@ -111,6 +114,7 @@ module.exports = {
111
114
  multer: () => require('../multer'),
112
115
  mysql: () => require('../mysql'),
113
116
  mysql2: () => require('../mysql2'),
117
+ '@nats-io/nats-core': () => require('../nats'),
114
118
  next: () => require('../next'),
115
119
  'node-serialize': () => require('../node-serialize'),
116
120
  nyc: () => require('../nyc'),
@@ -58,7 +58,8 @@ exports.getHooks = function getHooks (names) {
58
58
  * @param {string} [args.file] path to file within package to instrument. Defaults to 'index.js'.
59
59
  * @param {string} [args.filePattern] pattern to match files within package to instrument
60
60
  * @param {boolean} [args.patchDefault] whether to patch the default export. Defaults to true.
61
- * @param {(moduleExports: unknown, version: string, isIitm?: boolean) => unknown} [hook] Patches module exports
61
+ * @param {(moduleExports: unknown, version: string, isIitm?: boolean, hookMeta?: object) => unknown} [hook]
62
+ * Patches module exports
62
63
  */
63
64
  exports.addHook = function addHook ({ name, versions, file, filePattern, patchDefault }, hook) {
64
65
  if (!instrumentations[name]) {
@@ -0,0 +1,269 @@
1
+ 'use strict'
2
+
3
+ const dc = require('dc-polyfill')
4
+ const shimmer = require('../../../datadog-shimmer')
5
+ const {
6
+ convertOpenAIResponseItemsToMessages,
7
+ convertOpenAIResponsePromptToMessages,
8
+ normalizeOpenAIChatMessages,
9
+ } = require('./ai-messages')
10
+
11
+ // TODO: this channel name is incorrect, instrumentations publish with THEIR name, not with their subscribers names.
12
+ const aiguardChannel = dc.channel('dd-trace:ai:aiguard')
13
+
14
+ /**
15
+ * @typedef {object} ResourceHandler
16
+ * @property {(callArgs: object) => (Array<object>|undefined)} getInputMessages
17
+ * @property {(body: object) => Array<object>} getOutputMessages
18
+ * @property {(inputMessages: Array<object>, outputMessages: Array<object>) => Promise<unknown>}
19
+ * publishOutputEvaluation
20
+ */
21
+
22
+ /**
23
+ * @typedef {object} Guard
24
+ * @property {ResourceHandler} handler
25
+ * @property {Array<object>} inputMessages
26
+ * @property {() => Promise<void>} getInputEval
27
+ */
28
+
29
+ /**
30
+ * Publishes already-converted AI-style messages to the AI Guard evaluation channel.
31
+ *
32
+ * @param {Array<object>} messages - AI-style messages to evaluate.
33
+ * @returns {Promise<void>}
34
+ */
35
+ function publishEvaluation (messages) {
36
+ return new Promise((resolve, reject) => {
37
+ aiguardChannel.publish({ messages, integration: 'openai', resolve, reject })
38
+ })
39
+ }
40
+
41
+ /**
42
+ * Extracts OpenAI input messages from a `chat.completions.create` call.
43
+ *
44
+ * @param {object} callArgs - First argument passed to the wrapped method
45
+ * @returns {Array<object>|undefined}
46
+ */
47
+ function getChatCompletionsInputMessages (callArgs) {
48
+ return normalizeOpenAIChatMessages(callArgs?.messages)
49
+ }
50
+
51
+ /**
52
+ * Extracts OpenAI output messages from a `chat.completions.create` parsed body.
53
+ * Includes any choice whose message carries content (including empty string),
54
+ * `tool_calls`, a `refusal` field, or the deprecated `function_call` field. GPT-4o
55
+ * emits `{content: null, refusal: "..."}` on policy refusals, and pre-tool-call
56
+ * SDK paths still produce `function_call`-only output — AI Guard must still see them.
57
+ *
58
+ * @param {object} body - Parsed response body
59
+ * @returns {Array<object>}
60
+ */
61
+ function getChatCompletionsOutputMessages (body) {
62
+ const eligible = []
63
+ const choices = Array.isArray(body?.choices) ? body.choices : []
64
+ for (const choice of choices) {
65
+ const message = choice?.message
66
+ if (
67
+ message?.content != null ||
68
+ message?.tool_calls?.length ||
69
+ message?.refusal != null ||
70
+ message?.function_call != null
71
+ ) {
72
+ eligible.push(message)
73
+ }
74
+ }
75
+ return normalizeOpenAIChatMessages(eligible) ?? []
76
+ }
77
+
78
+ /**
79
+ * Publishes AI Guard After Model evaluation for `chat.completions` output.
80
+ *
81
+ * Chat completions may return multiple choices when `n > 1`. Screen every choice
82
+ * concurrently so any unsafe assistant output rejects `.parse()`, regardless of
83
+ * which choice the caller ends up using.
84
+ *
85
+ * @param {Array<object>} inputMessages
86
+ * @param {Array<object>} outputMessages - One entry per choice
87
+ * @returns {Promise<Array<void>>}
88
+ */
89
+ function publishChatCompletionsOutputEvaluation (inputMessages, outputMessages) {
90
+ const evals = []
91
+ for (const message of outputMessages) {
92
+ evals.push(publishEvaluation([...inputMessages, message]))
93
+ }
94
+ return Promise.all(evals)
95
+ }
96
+
97
+ /**
98
+ * Extracts OpenAI input messages from a `responses.create` call. The `instructions`
99
+ * field is treated as a developer prompt — it directly steers model behavior and the
100
+ * LLMObs OpenAI plugin already surfaces it as one — so AI Guard must screen it too.
101
+ *
102
+ * AI Guard `/evaluate` accepts a single leading system/developer message; if the
103
+ * caller's `input` already begins with one, prepend the `instructions` text to its
104
+ * content rather than emit a second developer turn.
105
+ *
106
+ * @param {object} callArgs - First argument passed to the wrapped method
107
+ * @returns {Array<object>|undefined}
108
+ */
109
+ function getResponsesInputMessages (callArgs) {
110
+ const messages = [
111
+ ...convertOpenAIResponseItemsToMessages(callArgs?.input, 'user'),
112
+ ...convertOpenAIResponsePromptToMessages(callArgs?.prompt),
113
+ ]
114
+
115
+ const instructions = typeof callArgs?.instructions === 'string' && callArgs.instructions.length
116
+ ? callArgs.instructions
117
+ : null
118
+ if (!instructions) return messages.length ? messages : undefined
119
+
120
+ const first = messages[0]
121
+ if (first && (first.role === 'developer' || first.role === 'system')) {
122
+ const merged = { role: 'developer', content: mergeInstructionsWithContent(instructions, first.content) }
123
+ return [merged, ...messages.slice(1)]
124
+ }
125
+ return [{ role: 'developer', content: instructions }, ...messages]
126
+ }
127
+
128
+ /**
129
+ * Merges Responses API instructions with an existing leading developer/system content value.
130
+ *
131
+ * @param {string} instructions
132
+ * @param {string|Array<object>|undefined} content
133
+ * @returns {string|Array<object>}
134
+ */
135
+ function mergeInstructionsWithContent (instructions, content) {
136
+ if (Array.isArray(content)) return [{ type: 'text', text: instructions }, ...content]
137
+ if (typeof content === 'string' && content.length) return `${instructions}\n\n${content}`
138
+ return instructions
139
+ }
140
+
141
+ /**
142
+ * Extracts OpenAI output messages from a `responses.create` parsed body.
143
+ *
144
+ * @param {object} body - Parsed response body
145
+ * @returns {Array<object>}
146
+ */
147
+ function getResponsesOutputMessages (body) {
148
+ return convertOpenAIResponseItemsToMessages(body?.output, 'assistant')
149
+ }
150
+
151
+ /**
152
+ * Publishes AI Guard After Model evaluation for `responses` output.
153
+ *
154
+ * The Responses API returns a single conversation turn whose `output` items form one
155
+ * coherent message (reasoning steps + final assistant message + tool calls + ...);
156
+ * they are screened together as a single evaluation.
157
+ *
158
+ * @param {Array<object>} inputMessages
159
+ * @param {Array<object>} outputMessages
160
+ * @returns {Promise<void>}
161
+ */
162
+ function publishResponsesOutputEvaluation (inputMessages, outputMessages) {
163
+ return publishEvaluation([...inputMessages, ...outputMessages])
164
+ }
165
+
166
+ /**
167
+ * Per-resource handlers describing how AI Guard reads inputs and screens outputs for
168
+ * each LLM-prompt-accepting OpenAI endpoint. The keys also serve as the set of
169
+ * resources eligible for AI Guard evaluation.
170
+ *
171
+ * @type {Record<string, ResourceHandler>}
172
+ */
173
+ const RESOURCE_HANDLERS = {
174
+ 'chat.completions': {
175
+ getInputMessages: getChatCompletionsInputMessages,
176
+ getOutputMessages: getChatCompletionsOutputMessages,
177
+ publishOutputEvaluation: publishChatCompletionsOutputEvaluation,
178
+ },
179
+ responses: {
180
+ getInputMessages: getResponsesInputMessages,
181
+ getOutputMessages: getResponsesOutputMessages,
182
+ publishOutputEvaluation: publishResponsesOutputEvaluation,
183
+ },
184
+ }
185
+
186
+ /**
187
+ * Reports whether the AI Guard channel has subscribers. The OpenAI instrumentation
188
+ * uses this to decide whether to take the AI Guard path at all.
189
+ *
190
+ * @returns {boolean}
191
+ */
192
+ function hasSubscribers () {
193
+ return aiguardChannel.hasSubscribers
194
+ }
195
+
196
+ /**
197
+ * Builds a guard handle when AI Guard is enabled and applicable to this call. The
198
+ * handle binds the per-resource handler so downstream functions never re-dispatch
199
+ * on `baseResource`. Returns null when AI Guard does not apply (no subscribers,
200
+ * non-eligible resource, streaming, or no input messages).
201
+ *
202
+ * @param {string} baseResource - e.g. `'chat.completions'` or `'responses'`
203
+ * @param {object} callArgs - First argument passed to the wrapped OpenAI method
204
+ * @param {boolean} stream - Whether the caller asked for a streamed response
205
+ * @returns {Guard|null}
206
+ */
207
+ function createGuard (baseResource, callArgs, stream) {
208
+ // Streaming AI Guard support lands in a follow-up PR. For now, provider-level AI
209
+ // Guard only evaluates non-streaming responses.
210
+ if (stream || !aiguardChannel.hasSubscribers) return null
211
+ const handler = RESOURCE_HANDLERS[baseResource]
212
+ if (!handler) return null
213
+
214
+ const inputMessages = handler.getInputMessages(callArgs)
215
+ if (!inputMessages) return null
216
+
217
+ let inputEvalPromise
218
+ const getInputEval = () => (inputEvalPromise ??= publishEvaluation(inputMessages))
219
+ return { handler, inputMessages, getInputEval }
220
+ }
221
+
222
+ /**
223
+ * Wraps `apiProm.asResponse` so callers that consume the raw `Response` object still
224
+ * receive the Before Model verdict. After Model evaluation is not performed on this
225
+ * path because the response body has not been parsed.
226
+ *
227
+ * @param {object} apiProm - APIPromise returned from the OpenAI SDK method
228
+ * @param {Guard} guard
229
+ */
230
+ function wrapAsResponse (apiProm, guard) {
231
+ if (typeof apiProm.asResponse !== 'function') return
232
+ shimmer.wrap(apiProm, 'asResponse', origAsResponse => function (...args) {
233
+ const responsePromise = origAsResponse.apply(this, args)
234
+ return Promise.all([guard.getInputEval(), responsePromise]).then(([, response]) => response)
235
+ })
236
+ }
237
+
238
+ /**
239
+ * Gates the parsed-body promise on Before Model evaluation. Resolves to the SDK's
240
+ * result only once the Before Model verdict is in.
241
+ *
242
+ * @param {Promise<unknown>} parsedPromise
243
+ * @param {Guard} guard
244
+ * @returns {Promise<unknown>}
245
+ */
246
+ function gateParse (parsedPromise, guard) {
247
+ return Promise.all([guard.getInputEval(), parsedPromise]).then(([, result]) => result)
248
+ }
249
+
250
+ /**
251
+ * Runs After Model evaluation against the response body.
252
+ *
253
+ * @param {Guard} guard
254
+ * @param {object} body - Parsed OpenAI response body
255
+ * @returns {Promise<unknown>}
256
+ */
257
+ function evaluateOutput (guard, body) {
258
+ const outputMessages = guard.handler.getOutputMessages(body)
259
+ if (!outputMessages.length) return Promise.resolve()
260
+ return guard.handler.publishOutputEvaluation(guard.inputMessages, outputMessages)
261
+ }
262
+
263
+ module.exports = {
264
+ hasSubscribers,
265
+ createGuard,
266
+ wrapAsResponse,
267
+ gateParse,
268
+ evaluateOutput,
269
+ }