dd-trace 6.18.0 → 6.20.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 (123) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/vitest-no-worker-init-setup.mjs +22 -12
  3. package/index.d.ts +166 -24
  4. package/package.json +7 -7
  5. package/packages/datadog-esbuild/index.js +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +26 -0
  7. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  9. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +40 -4
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
  16. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  17. package/packages/datadog-instrumentations/src/jest.js +3 -10
  18. package/packages/datadog-instrumentations/src/playwright.js +26 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  21. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  22. package/packages/datadog-plugin-cucumber/src/index.js +19 -4
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
  24. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  25. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  26. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  27. package/packages/datadog-plugin-undici/src/index.js +5 -0
  28. package/packages/dd-trace/src/aiguard/client.js +38 -19
  29. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  30. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  31. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  32. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  33. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  34. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
  35. package/packages/dd-trace/src/config/remote_config.js +12 -0
  36. package/packages/dd-trace/src/config/supported-configurations.json +26 -5
  37. package/packages/dd-trace/src/constants.js +2 -0
  38. package/packages/dd-trace/src/debugger/constants.js +13 -4
  39. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  41. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  42. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  43. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  44. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  45. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  46. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  47. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  48. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  49. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  50. package/packages/dd-trace/src/debugger/index.js +60 -28
  51. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  52. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  53. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  54. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  55. package/packages/dd-trace/src/knuth-hash.js +17 -0
  56. package/packages/dd-trace/src/llmobs/constants/tags.js +33 -8
  57. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  58. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  59. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  60. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  61. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  62. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  63. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  64. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  65. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  66. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  67. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  68. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  69. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  71. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  72. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  73. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  74. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  75. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  76. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
  77. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  78. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  79. package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
  80. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  81. package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
  82. package/packages/dd-trace/src/llmobs/tagger.js +37 -36
  83. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  84. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  85. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  86. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  87. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  88. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  89. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  90. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  91. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  92. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  93. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  94. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  95. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  96. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  97. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  98. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  99. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  100. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  101. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  102. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  103. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  104. package/packages/dd-trace/src/opentracing/span.js +14 -9
  105. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  106. package/packages/dd-trace/src/otel-sampling.js +171 -0
  107. package/packages/dd-trace/src/plugins/index.js +0 -1
  108. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  109. package/packages/dd-trace/src/priority_sampler.js +155 -33
  110. package/packages/dd-trace/src/profiler.js +51 -5
  111. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  112. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  113. package/packages/dd-trace/src/ritm.js +12 -9
  114. package/packages/dd-trace/src/sampler.js +2 -5
  115. package/packages/dd-trace/src/sampling_rule.js +4 -8
  116. package/packages/dd-trace/src/span_format.js +6 -0
  117. package/packages/dd-trace/src/span_processor.js +32 -3
  118. package/packages/dd-trace/src/standalone/index.js +5 -4
  119. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  120. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  121. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  122. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  123. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -23,6 +23,15 @@ const PROVIDER = {
23
23
  MISTRAL: 'MISTRAL',
24
24
  }
25
25
 
26
+ /**
27
+ * @typedef {{
28
+ * inputTokens?: number,
29
+ * outputTokens?: number,
30
+ * cacheReadTokens?: number,
31
+ * cacheWriteTokens?: number,
32
+ * }} StreamedUsage
33
+ */
34
+
26
35
  /**
27
36
  * Coerce the chunks into a single response body.
28
37
  *
@@ -44,10 +53,7 @@ function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelNam
44
53
  }
45
54
 
46
55
  let message = ''
47
- let inputTokens = 0
48
- let outputTokens = 0
49
- let cacheReadTokens = 0
50
- let cacheWriteTokens = 0
56
+ let usage
51
57
 
52
58
  for (const { chunk: { bytes } } of chunks) {
53
59
  const body = JSON.parse(Buffer.from(bytes).toString('utf8'))
@@ -56,8 +62,6 @@ function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelNam
56
62
  case PROVIDER.AMAZON: {
57
63
  if (body?.outputText) {
58
64
  message += body?.outputText
59
- inputTokens = body?.inputTextTokenCount
60
- outputTokens = body?.totalOutputTextTokenCount
61
65
  } else if (body?.contentBlockDelta?.delta?.text) {
62
66
  message += body.contentBlockDelta.delta.text
63
67
  }
@@ -79,9 +83,6 @@ function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelNam
79
83
  message += body.delta.text
80
84
  }
81
85
 
82
- if (body.message?.usage?.input_tokens) inputTokens = body.message.usage.input_tokens
83
- if (body.message?.usage?.output_tokens) outputTokens = body.message.usage.output_tokens
84
-
85
86
  break
86
87
  }
87
88
  case PROVIDER.COHERE: {
@@ -101,24 +102,61 @@ function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelNam
101
102
  }
102
103
  }
103
104
 
104
- // by default, it seems newer versions of the AWS SDK include the input/output token counts in the response body
105
- const invocationMetrics = body['amazon-bedrock-invocationMetrics']
106
- if (invocationMetrics) {
107
- inputTokens = invocationMetrics.inputTokenCount
108
- outputTokens = invocationMetrics.outputTokenCount
109
- cacheReadTokens = invocationMetrics.cacheReadInputTokenCount
110
- cacheWriteTokens = invocationMetrics.cacheWriteInputTokenCount
105
+ usage = mergeStreamedUsage(usage, body, modelProviderUpper)
106
+ }
107
+
108
+ return new Generation({ message, role: 'assistant', ...usage })
109
+ }
110
+
111
+ /**
112
+ * Merge the token counts one streamed `invokeModel` frame reports into the running totals. Each
113
+ * provider spells them differently, and most report none until the trailing invocation metrics,
114
+ * so a frame carrying no counts leaves the totals as they were.
115
+ *
116
+ * @param {StreamedUsage} [usage]
117
+ * @param {object} body parsed chunk body
118
+ * @param {string} modelProviderUpper
119
+ * @returns {StreamedUsage | undefined}
120
+ */
121
+ function mergeStreamedUsage (usage, body, modelProviderUpper) {
122
+ // by default, it seems newer versions of the AWS SDK include the input/output token counts in
123
+ // the response body; any provider can send them, and they supersede what the frames reported
124
+ const invocationMetrics = body['amazon-bedrock-invocationMetrics']
125
+ if (invocationMetrics) {
126
+ return {
127
+ inputTokens: invocationMetrics.inputTokenCount,
128
+ outputTokens: invocationMetrics.outputTokenCount,
129
+ cacheReadTokens: invocationMetrics.cacheReadInputTokenCount,
130
+ cacheWriteTokens: invocationMetrics.cacheWriteInputTokenCount,
111
131
  }
112
132
  }
113
133
 
114
- return new Generation({
115
- message,
116
- role: 'assistant',
117
- inputTokens,
118
- outputTokens,
119
- cacheReadTokens,
120
- cacheWriteTokens,
121
- })
134
+ switch (modelProviderUpper) {
135
+ case PROVIDER.AMAZON: {
136
+ const { inputTextTokenCount, totalOutputTextTokenCount } = body
137
+ if (inputTextTokenCount === undefined && totalOutputTextTokenCount === undefined) return usage
138
+
139
+ return {
140
+ ...usage,
141
+ inputTokens: inputTextTokenCount ?? usage?.inputTokens,
142
+ outputTokens: totalOutputTextTokenCount ?? usage?.outputTokens,
143
+ }
144
+ }
145
+ case PROVIDER.ANTHROPIC: {
146
+ // `message_start` reports the initial counts under `message.usage`; the closing
147
+ // `message_delta` reports the final output count at the top level
148
+ const chunkUsage = body.message?.usage ?? body.usage
149
+ if (!chunkUsage) return usage
150
+
151
+ return {
152
+ ...usage,
153
+ inputTokens: chunkUsage.input_tokens ?? usage?.inputTokens,
154
+ outputTokens: chunkUsage.output_tokens ?? usage?.outputTokens,
155
+ }
156
+ }
157
+ }
158
+
159
+ return usage
122
160
  }
123
161
 
124
162
  class Generation {
@@ -568,13 +606,27 @@ function resolveToolResultItem (item) {
568
606
  return `[Unsupported content type(s): ${getContentBlockType(item)}]`
569
607
  }
570
608
 
609
+ /**
610
+ * Normalize a Converse usage object onto the LLMObs metric names. Returns undefined when the
611
+ * response reported no counts at all, so a caller cannot mistake an empty record for a measurement.
612
+ *
613
+ * @param {object} [usage]
614
+ * @returns {{
615
+ * inputTokens?: number, outputTokens?: number, cacheReadTokens?: number, cacheWriteTokens?: number
616
+ * } | undefined}
617
+ */
571
618
  function buildUsage (usage = {}) {
572
- return {
573
- inputTokens: usage.inputTokens,
574
- outputTokens: usage.outputTokens,
575
- cacheReadTokens: usage.cacheReadInputTokens ?? usage.cacheReadInputTokenCount,
576
- cacheWriteTokens: usage.cacheWriteInputTokens ?? usage.cacheWriteInputTokenCount,
619
+ const inputTokens = usage.inputTokens
620
+ const outputTokens = usage.outputTokens
621
+ const cacheReadTokens = usage.cacheReadInputTokens ?? usage.cacheReadInputTokenCount
622
+ const cacheWriteTokens = usage.cacheWriteInputTokens ?? usage.cacheWriteInputTokenCount
623
+
624
+ if (inputTokens === undefined && outputTokens === undefined &&
625
+ cacheReadTokens === undefined && cacheWriteTokens === undefined) {
626
+ return
577
627
  }
628
+
629
+ return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens }
578
630
  }
579
631
 
580
632
  /**
@@ -714,6 +766,8 @@ function extractTextAndResponseReasonConverseFromStream (chunks) {
714
766
 
715
767
  module.exports = {
716
768
  Generation,
769
+ buildUsage,
770
+ mergeStreamedUsage,
717
771
  RequestParams,
718
772
  extractTextAndResponseReasonFromStream,
719
773
  parseModelId,
@@ -449,7 +449,8 @@ class CucumberPlugin extends CiPlugin {
449
449
  })
450
450
 
451
451
  this.addSub('ci:cucumber:is-modified-test', ({
452
- scenarios,
452
+ gherkinNodes,
453
+ gherkinScopeRanges,
453
454
  testFileAbsolutePath,
454
455
  modifiedFiles,
455
456
  stepIds,
@@ -457,11 +458,25 @@ class CucumberPlugin extends CiPlugin {
457
458
  setIsModified,
458
459
  }) => {
459
460
  const testScenarioPath = getTestSuitePath(testFileAbsolutePath, this.repositoryRoot || process.cwd())
460
- for (const scenario of scenarios) {
461
+ for (const [startLine, endLine] of gherkinScopeRanges) {
462
+ if (isModifiedTest(testScenarioPath, startLine, endLine, modifiedFiles, 'cucumber')) {
463
+ setIsModified(true)
464
+ return
465
+ }
466
+ }
467
+ for (const gherkinNode of gherkinNodes) {
468
+ const lastStep = gherkinNode.steps.at(-1)
469
+ let endLine = lastStep?.location.line ?? gherkinNode.location.line
470
+ if (lastStep?.dataTable?.rows.length) {
471
+ endLine = lastStep.dataTable.rows.at(-1).location.line
472
+ } else if (lastStep?.docString) {
473
+ const { content, location } = lastStep.docString
474
+ endLine = location.line + (content ? content.split('\n').length + 1 : 1)
475
+ }
461
476
  const isModified = isModifiedTest(
462
477
  testScenarioPath,
463
- scenario.location.line,
464
- scenario.steps.at(-1).location.line,
478
+ gherkinNode.location.line,
479
+ endLine,
465
480
  modifiedFiles,
466
481
  'cucumber'
467
482
  )
@@ -94,6 +94,7 @@ const {
94
94
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
95
95
  const { RESOURCE_NAME } = require('../../../ext/tags')
96
96
  const getConfig = require('../../dd-trace/src/config')
97
+ const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
97
98
  const {
98
99
  SCREENSHOT_UPLOAD_RESULT_ERROR,
99
100
  SCREENSHOT_UPLOAD_RESULT_UPLOADED,
@@ -857,6 +858,12 @@ class CypressPlugin {
857
858
 
858
859
  const testOptimizationConfig = getConfig().testOptimization
859
860
  this.rumFlushWaitMillis = testOptimizationConfig.DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS
861
+ if (getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED', true) === true) {
862
+ cypressConfig.screenshotOnRunFailure = true
863
+ }
864
+ if (getValueFromEnvSources('DD_TEST_FAILURE_VIDEOS_ENABLED', true) === true) {
865
+ cypressConfig.video = true
866
+ }
860
867
  this.warnIfMisconfiguredTestFailureScreenshots(cypressConfig, tracer, testOptimizationConfig)
861
868
  this.warnIfMisconfiguredTestFailureVideos(cypressConfig, tracer, testOptimizationConfig)
862
869
 
@@ -17,6 +17,9 @@ class FetchPlugin extends HttpClientPlugin {
17
17
 
18
18
  const store = super.bindStart(ctx)
19
19
 
20
+ // Native undici diagnostics also fire for requests already traced by this wrapper.
21
+ store.skipUndici = true
22
+
20
23
  for (const name in options.headers) {
21
24
  if (!req.headers.has(name)) {
22
25
  req.headers.set(name, options.headers[name])
@@ -17,7 +17,13 @@
17
17
  * @typedef {JsonObject & { choices: Array<StreamChoice | undefined>, usage?: unknown }} StreamResponseBody
18
18
  *
19
19
  * @typedef {JsonObject & { status?: string }} ResponsesApiResponse
20
- * @typedef {JsonObject & { response?: ResponsesApiResponse }} ResponsesApiChunk
20
+ * @typedef {JsonObject & {
21
+ * type?: string,
22
+ * response?: ResponsesApiResponse,
23
+ * output_index?: number,
24
+ * item?: JsonObject,
25
+ * delta?: string
26
+ * }} ResponsesApiChunk
21
27
  */
22
28
 
23
29
  /**
@@ -143,12 +149,55 @@ function constructResponseResponseFromStreamedChunks (chunks) {
143
149
  // Find the last chunk with a complete response object (status: done, incomplete, or completed)
144
150
  const responseStatusSet = new Set(['done', 'incomplete', 'completed'])
145
151
 
152
+ let latestResponse
146
153
  for (let i = chunks.length - 1; i >= 0; i--) {
147
154
  const chunk = chunks[i]
148
155
  if (chunk.response && responseStatusSet.has(chunk.response.status)) {
149
156
  return chunk.response
150
157
  }
158
+ latestResponse ??= chunk.response
151
159
  }
160
+
161
+ // A stream that ends early (e.g. aborted by the caller) never sends a final response,
162
+ // so rebuild a partial one from the output streamed so far, as the chat helpers do.
163
+ if (latestResponse) {
164
+ return { ...latestResponse, output: constructPartialResponseOutput(chunks) }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Rebuilds the output items of a Responses API stream that ended before its final response.
170
+ * Items are shallow-copied so the chunks handed to the application are not mutated.
171
+ * @param {ResponsesApiChunk[]} chunks
172
+ * @returns {JsonObject[]}
173
+ */
174
+ function constructPartialResponseOutput (chunks) {
175
+ const output = []
176
+
177
+ for (const chunk of chunks) {
178
+ const index = chunk.output_index
179
+ if (index === undefined) continue
180
+
181
+ const item = output[index]
182
+
183
+ switch (chunk.type) {
184
+ case 'response.output_item.added':
185
+ case 'response.output_item.done':
186
+ output[index] = { ...chunk.item }
187
+ break
188
+ case 'response.output_text.delta':
189
+ if (item) {
190
+ const text = (item.content?.[0]?.text ?? '') + (chunk.delta ?? '')
191
+ item.content = [{ type: 'output_text', text }]
192
+ }
193
+ break
194
+ case 'response.function_call_arguments.delta':
195
+ if (item) item.arguments = (item.arguments ?? '') + (chunk.delta ?? '')
196
+ break
197
+ }
198
+ }
199
+
200
+ return output.filter(Boolean)
152
201
  }
153
202
 
154
203
  module.exports = {
@@ -3,6 +3,8 @@
3
3
  const { URL } = require('url')
4
4
  const log = require('../../dd-trace/src/log')
5
5
 
6
+ const easyConnectProtocol = /^tcps?:\/\//i
7
+
6
8
  function parseOracleDescriptor (descriptor) {
7
9
  const hostnameMatch = descriptor.match(/HOST\s*=\s*([^)]+)/i)
8
10
  const hostname = hostnameMatch?.[1] || 'localhost' // Default Oracle hostname
@@ -24,7 +26,7 @@ module.exports = function getDBInformation (connAttrs) {
24
26
  return parseOracleDescriptor(connectString)
25
27
  }
26
28
  try {
27
- const url = new URL(`oracle://${connectString}`)
29
+ const url = new URL(easyConnectProtocol.test(connectString) ? connectString : `oracle://${connectString}`)
28
30
  return {
29
31
  hostname: url.hostname || 'localhost', // Default Oracle hostname
30
32
  port: url.port || '1521', // Default Oracle port
@@ -47,6 +47,8 @@ class UndiciPlugin extends HttpClientPlugin {
47
47
  if (!request) return
48
48
 
49
49
  const store = storage('legacy').getStore()
50
+ if (store?.skipUndici) return
51
+
50
52
  const { origin = '', path = '/' } = request
51
53
  const method = request.method?.toUpperCase() ?? 'GET'
52
54
 
@@ -207,6 +209,9 @@ class UndiciPlugin extends HttpClientPlugin {
207
209
 
208
210
  const store = super.bindStart(ctx)
209
211
 
212
+ // Undici 5.0 emits native diagnostics even though its fetch needs the legacy wrapper.
213
+ store.skipUndici = true
214
+
210
215
  // Inject trace headers back into the request
211
216
  for (const name of Object.keys(options.headers)) {
212
217
  if (!req.headers.has(name)) {
@@ -1,10 +1,15 @@
1
1
  'use strict'
2
2
 
3
3
  const tracerVersion = require('../../../../package.json').version
4
+ const { createAgents } = require('../exporters/common/agents')
5
+ const request = require('../exporters/common/request')
4
6
  const { AIGuardClientError } = require('./errors')
5
7
  const { parseEvaluationResponse } = require('./evaluation')
6
8
  const TAGS = require('./tags')
7
9
 
10
+ // Evaluations can run concurrently on the application's request path.
11
+ const { httpAgent, httpsAgent } = createAgents(16)
12
+
8
13
  /**
9
14
  * Resolves the AI Guard host for a Datadog site.
10
15
  *
@@ -18,29 +23,43 @@ function aiGuardHost (site) {
18
23
  * Sends a request to the AI Guard service.
19
24
  *
20
25
  * @param {object} body
21
- * @param {{ url: string, headers: Record<string, string>, timeout: number }} opts
26
+ * @param {{ url: string, headers: Record<string, string|undefined>, timeout: number }} opts
22
27
  * @returns {Promise<{ status: number, body: unknown }>}
23
28
  */
24
- async function executeRequest (body, opts) {
25
- const postData = JSON.stringify(body)
26
- const headers = {
27
- 'Content-Type': 'application/json',
28
- 'Content-Length': Buffer.byteLength(postData),
29
- ...opts.headers,
30
- }
29
+ function executeRequest (body, opts) {
30
+ return new Promise((resolve, reject) => {
31
+ const postData = JSON.stringify(body)
32
+ const url = new URL(opts.url)
33
+ request(postData, {
34
+ url,
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ ...opts.headers,
39
+ },
40
+ agent: url.protocol === 'https:' ? httpsAgent : httpAgent,
41
+ timeout: opts.timeout,
42
+ signal: AbortSignal.timeout(opts.timeout),
43
+ retry: false,
44
+ includeErrorResponseBody: true,
45
+ }, (error, result, status) => {
46
+ if (status === undefined) {
47
+ reject(error || new Error('AI Guard request completed without a status'))
48
+ return
49
+ }
31
50
 
32
- const response = await fetch(opts.url, {
33
- method: 'POST',
34
- headers,
35
- body: postData,
36
- signal: AbortSignal.timeout(opts.timeout),
51
+ let responseBody
52
+ try {
53
+ const responseText = error ? /** @type {Error & { responseBody?: string }} */ (error).responseBody : result
54
+ if (typeof responseText !== 'string') throw new TypeError('AI Guard response body is missing')
55
+ responseBody = JSON.parse(responseText)
56
+ } catch (cause) {
57
+ reject(cause)
58
+ return
59
+ }
60
+ resolve({ status, body: responseBody })
61
+ })
37
62
  })
38
-
39
- const responseBody = await response.json()
40
- return {
41
- status: response.status,
42
- body: responseBody,
43
- }
44
63
  }
45
64
 
46
65
  class AIGuardClient {
@@ -2,10 +2,15 @@
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
- const { getMessagesInputMessages, getMessagesOutputMessages } = require('../messages/anthropic')
5
+ const {
6
+ getMessagesInputMessages,
7
+ getMessagesOutputMessages,
8
+ getStreamedMessagesOutputMessages,
9
+ } = require('../messages/anthropic')
6
10
  const { decode } = require('../messages/utils')
7
11
  const { SOURCE_AUTO } = require('../tags')
8
12
  const { evaluate } = require('./evaluate')
13
+ const { interceptStream } = require('./stream')
9
14
 
10
15
  const messagesPrepareChannel = channel('dd-trace:anthropic:messages:prepare')
11
16
  const messagesInterceptChannel = channel('dd-trace:anthropic:messages:intercept')
@@ -13,18 +18,21 @@ const messagesInterceptChannel = channel('dd-trace:anthropic:messages:intercept'
13
18
  let isEnabled = false
14
19
  let aiguard
15
20
  let opts
21
+ let analyzeStreamResponses
16
22
 
17
23
  /**
18
24
  * Subscribes AI Guard to the Anthropic interception channels.
19
25
  *
20
26
  * @param {object} aiguardInstance
21
27
  * @param {boolean} block
28
+ * @param {boolean} analyzeStreams
22
29
  */
23
- function enable (aiguardInstance, block) {
30
+ function enable (aiguardInstance, block, analyzeStreams) {
24
31
  if (isEnabled) return
25
32
 
26
33
  aiguard = aiguardInstance
27
34
  opts = { block, source: SOURCE_AUTO, integration: 'anthropic' }
35
+ analyzeStreamResponses = analyzeStreams
28
36
 
29
37
  messagesPrepareChannel.subscribe(onMessagesPrepare)
30
38
  messagesInterceptChannel.subscribe(onMessagesIntercept)
@@ -40,6 +48,7 @@ function disable () {
40
48
 
41
49
  aiguard = undefined
42
50
  opts = undefined
51
+ analyzeStreamResponses = undefined
43
52
  isEnabled = false
44
53
  }
45
54
 
@@ -76,19 +85,35 @@ function onMessagesIntercept (ctx) {
76
85
  return inputEvaluation
77
86
  }
78
87
 
88
+ const isStream = ctx.arguments[0].stream
89
+ if (isStream && !analyzeStreamResponses) return
90
+
79
91
  // One model call has one output however many readers observe it: `parse`, `json()`, `text()`
80
92
  // and every `clone()` of the raw response share this callback.
81
93
  let outputEvaluation
82
94
  ctx.onResult = body => {
83
95
  if (!isEnabled) return body
84
96
 
97
+ if (isStream) {
98
+ outputEvaluation ??= interceptStream(body, chunks => {
99
+ if (!isEnabled) return
100
+ const outputMessages = decode(
101
+ () => getStreamedMessagesOutputMessages(chunks),
102
+ null,
103
+ 'AIGuard: unable to decode the streamed Anthropic response: %s'
104
+ )
105
+ if (!outputMessages?.length) return
106
+ return evaluate(ctx, aiguard, [[...inputMessages, ...outputMessages]], opts)
107
+ })
108
+ return outputEvaluation
109
+ }
110
+
85
111
  const outputMessages = decode(
86
112
  () => getMessagesOutputMessages(body),
87
113
  null,
88
114
  'AIGuard: unable to decode Anthropic response body: %s'
89
115
  )
90
- if (outputMessages === null) return body
91
- if (!outputMessages.length) return body
116
+ if (!outputMessages?.length) return body
92
117
 
93
118
  outputEvaluation ??= evaluate(ctx, aiguard, [[...inputMessages, ...outputMessages]], opts)
94
119
  return outputEvaluation.then(() => body)
@@ -16,7 +16,7 @@ let isEnabled = false
16
16
  function enable (aiguard, block, analyzeStreamResponses) {
17
17
  if (isEnabled) return
18
18
 
19
- anthropic.enable(aiguard, block)
19
+ anthropic.enable(aiguard, block, analyzeStreamResponses)
20
20
  openai.enable(aiguard, block, analyzeStreamResponses)
21
21
  vercelAi.enable(aiguard, block, analyzeStreamResponses)
22
22
 
@@ -13,6 +13,7 @@ const {
13
13
  const { decode } = require('../messages/utils')
14
14
  const { SOURCE_AUTO } = require('../tags')
15
15
  const { evaluate } = require('./evaluate')
16
+ const { interceptStream } = require('./stream')
16
17
 
17
18
  const chatCompletionsInterceptChannel = channel('dd-trace:openai:chat.completions:intercept')
18
19
  const responsesInterceptChannel = channel('dd-trace:openai:responses:intercept')
@@ -74,9 +75,16 @@ function onChatCompletions (ctx) {
74
75
 
75
76
  if (isStream) {
76
77
  outputEvaluation ??= interceptStream(body, chunks => {
77
- return getStreamedChatCompletionsOutputMessages(chunks)
78
- .map(message => [...inputMessages, message])
79
- }, ctx)
78
+ if (!isEnabled) return
79
+ const conversations = decode(
80
+ () => getStreamedChatCompletionsOutputMessages(chunks)
81
+ .map(message => [...inputMessages, message]),
82
+ null,
83
+ 'AIGuard: unable to decode the streamed OpenAI response: %s'
84
+ )
85
+ if (!conversations?.length) return
86
+ return evaluate(ctx, aiguard, conversations, opts)
87
+ })
80
88
  return outputEvaluation
81
89
  }
82
90
 
@@ -112,9 +120,15 @@ function onResponses (ctx) {
112
120
 
113
121
  if (isStream) {
114
122
  outputEvaluation ??= interceptStream(body, chunks => {
115
- const outputMessages = getStreamedResponsesOutputMessages(chunks)
116
- return outputMessages.length ? [[...inputMessages, ...outputMessages]] : []
117
- }, ctx)
123
+ if (!isEnabled) return
124
+ const outputMessages = decode(
125
+ () => getStreamedResponsesOutputMessages(chunks),
126
+ null,
127
+ 'AIGuard: unable to decode the streamed OpenAI response: %s'
128
+ )
129
+ if (!outputMessages?.length) return
130
+ return evaluate(ctx, aiguard, [[...inputMessages, ...outputMessages]], opts)
131
+ })
118
132
  return outputEvaluation
119
133
  }
120
134
 
@@ -130,54 +144,4 @@ function onResponses (ctx) {
130
144
  }
131
145
  }
132
146
 
133
- /**
134
- * Uses the OpenAI SDK's stream splitting support to evaluate one branch and return the other.
135
- *
136
- * @param {object} stream
137
- * @param {(chunks: Array<object>) => Array<Array<object>>} getConversations
138
- * @param {object} ctx
139
- * @returns {object|Promise<object>}
140
- */
141
- function interceptStream (stream, getConversations, ctx) {
142
- if (typeof stream?.tee !== 'function') return stream
143
-
144
- let branches
145
- try {
146
- branches = stream.tee()
147
- } catch {
148
- return stream
149
- }
150
-
151
- const [evaluationStream, resultStream] = branches
152
- return drainStream(evaluationStream).then(chunks => {
153
- if (!isEnabled) return resultStream
154
- const conversations = decode(
155
- () => getConversations(chunks),
156
- null,
157
- 'AIGuard: unable to decode the streamed OpenAI response: %s'
158
- )
159
- if (!conversations?.length) return resultStream
160
- return evaluate(ctx, aiguard, conversations, opts).then(() => resultStream)
161
- }, () => resultStream)
162
- }
163
-
164
- /**
165
- * @param {object} stream
166
- * @returns {Promise<Array<object>>}
167
- */
168
- function drainStream (stream) {
169
- const chunks = []
170
- const iterator = stream[Symbol.asyncIterator]()
171
-
172
- function readAll () {
173
- return iterator.next().then(({ done, value }) => {
174
- if (done) return chunks
175
- chunks.push(value)
176
- return readAll()
177
- })
178
- }
179
-
180
- return readAll()
181
- }
182
-
183
147
  module.exports = { enable, disable }
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../log')
4
+
5
+ /**
6
+ * Splits an SDK stream, consumes one branch, and returns the other after inspection.
7
+ *
8
+ * @param {object} stream
9
+ * @param {(chunks: Array<object>) => void|Promise<void>} inspect
10
+ * @returns {object|Promise<object>}
11
+ */
12
+ function interceptStream (stream, inspect) {
13
+ if (typeof stream?.tee !== 'function') return stream
14
+
15
+ try {
16
+ const [inspectionStream, resultStream] = stream.tee()
17
+ return drainStream(inspectionStream).then(chunks => {
18
+ return Promise.resolve(inspect(chunks)).then(() => resultStream)
19
+ })
20
+ } catch {
21
+ return stream
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Buffers the whole stream. A stream that ends early still yields what it delivered: truncated
27
+ * output can carry the very violation the evaluation is looking for, so it is judged rather than
28
+ * skipped, and the read failure is logged because nothing downstream reports it.
29
+ *
30
+ * @param {object} stream
31
+ * @returns {Promise<Array<object>>}
32
+ */
33
+ function drainStream (stream) {
34
+ const chunks = []
35
+ const iterator = stream[Symbol.asyncIterator]()
36
+
37
+ function onReadError (error) {
38
+ log.error('AIGuard: the streamed response ended after %s chunks: %s', chunks.length, error)
39
+ return chunks
40
+ }
41
+
42
+ function readAll () {
43
+ let next
44
+ try {
45
+ next = iterator.next()
46
+ } catch (error) {
47
+ return Promise.resolve(onReadError(error))
48
+ }
49
+
50
+ return next.then(({ done, value }) => {
51
+ if (done) return chunks
52
+ chunks.push(value)
53
+ return readAll()
54
+ }, onReadError)
55
+ }
56
+
57
+ return readAll()
58
+ }
59
+
60
+ module.exports = { interceptStream }