dd-trace 5.132.0 → 5.134.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 (231) hide show
  1. package/ci/diagnose.js +9 -14
  2. package/ci/init.js +1 -2
  3. package/ci/test-optimization-validation/ci-discovery.js +9 -4
  4. package/ci/test-optimization-validation/executable.js +1 -5
  5. package/ci/test-optimization-validation/framework-adapters/cypress.js +1 -4
  6. package/ci/test-optimization-validation/framework-adapters/vitest.js +1 -1
  7. package/ci/test-optimization-validation/generated-files.js +6 -4
  8. package/ci/test-optimization-validation/generated-test-contract.js +7 -3
  9. package/ci/test-optimization-validation/manifest-scaffold.js +7 -2
  10. package/ci/test-optimization-validation/manifest-schema.js +6 -7
  11. package/ci/test-optimization-validation/report-writer.js +6 -2
  12. package/ci/test-optimization-validation/runner-command.js +2 -4
  13. package/ci/test-optimization-validation/runner-contract.js +1 -2
  14. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +1 -15
  15. package/ci/vitest-no-worker-init-runner-setup.mjs +27 -0
  16. package/ci/vitest-no-worker-init-setup.mjs +10 -7
  17. package/index.d.ts +41 -19
  18. package/initialize.mjs +1 -3
  19. package/openfeature/rules-based.d.ts +11 -0
  20. package/openfeature/rules-based.js +7 -0
  21. package/package.json +25 -22
  22. package/packages/datadog-core/src/storage.js +16 -0
  23. package/packages/datadog-esbuild/index.js +18 -12
  24. package/packages/datadog-instrumentations/index.js +2 -1
  25. package/packages/datadog-instrumentations/src/aerospike.js +3 -3
  26. package/packages/datadog-instrumentations/src/ai.js +3 -3
  27. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
  28. package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
  29. package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
  30. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
  31. package/packages/datadog-instrumentations/src/cucumber.js +65 -41
  32. package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
  33. package/packages/datadog-instrumentations/src/electron.js +6 -6
  34. package/packages/datadog-instrumentations/src/fastify.js +13 -4
  35. package/packages/datadog-instrumentations/src/fs.js +2 -2
  36. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
  37. package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
  38. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +28 -9
  39. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
  40. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  41. package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
  42. package/packages/datadog-instrumentations/src/helpers/register.js +45 -1
  43. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
  44. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +89 -6
  45. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/jest.js +12 -0
  47. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
  48. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/vitest.js +8 -0
  49. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +7 -7
  50. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +5 -0
  51. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
  52. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
  53. package/packages/datadog-instrumentations/src/ioredis.js +7 -5
  54. package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
  55. package/packages/datadog-instrumentations/src/jest.js +72 -67
  56. package/packages/datadog-instrumentations/src/knex.js +9 -5
  57. package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
  58. package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
  59. package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
  60. package/packages/datadog-instrumentations/src/mocha/utils.js +38 -4
  61. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -2
  62. package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
  63. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  64. package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
  65. package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
  66. package/packages/datadog-instrumentations/src/nova-sonic/session.js +492 -0
  67. package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
  68. package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
  69. package/packages/datadog-instrumentations/src/openai.js +7 -0
  70. package/packages/datadog-instrumentations/src/pino.js +5 -6
  71. package/packages/datadog-instrumentations/src/playwright.js +2 -0
  72. package/packages/datadog-instrumentations/src/redis.js +27 -13
  73. package/packages/datadog-instrumentations/src/router.js +30 -0
  74. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
  75. package/packages/datadog-instrumentations/src/stripe.js +23 -12
  76. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +8 -5
  77. package/packages/datadog-instrumentations/src/vitest-main.js +16 -18
  78. package/packages/datadog-instrumentations/src/vitest-util.js +7 -4
  79. package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
  80. package/packages/datadog-instrumentations/src/vitest.js +9 -1
  81. package/packages/datadog-instrumentations/src/webdriverio.js +5 -8
  82. package/packages/datadog-instrumentations/src/ws.js +1 -4
  83. package/packages/datadog-plugin-amqplib/src/client.js +1 -2
  84. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
  85. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
  86. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
  87. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
  88. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
  89. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  90. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
  91. package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
  92. package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
  93. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
  94. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +4 -5
  95. package/packages/datadog-plugin-cypress/src/index.js +6 -6
  96. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
  97. package/packages/datadog-plugin-cypress/src/support.js +3 -4
  98. package/packages/datadog-plugin-express/src/code_origin.js +1 -2
  99. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
  100. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
  101. package/packages/datadog-plugin-graphql/src/execute.js +5 -5
  102. package/packages/datadog-plugin-grpc/src/util.js +1 -2
  103. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
  104. package/packages/datadog-plugin-mocha/src/index.js +7 -10
  105. package/packages/datadog-plugin-mocha/src/webdriverio-video.js +8 -4
  106. package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
  107. package/packages/datadog-plugin-next/src/index.js +1 -2
  108. package/packages/datadog-plugin-openai/src/tracing.js +5 -2
  109. package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
  110. package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
  111. package/packages/datadog-plugin-postgres/src/index.js +24 -0
  112. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
  113. package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
  114. package/packages/datadog-plugin-vitest/src/index.js +5 -2
  115. package/packages/datadog-shimmer/src/shimmer.js +5 -4
  116. package/packages/datadog-webpack/index.js +55 -43
  117. package/packages/datadog-webpack/src/loader.js +56 -8
  118. package/packages/dd-trace/src/aiguard/evaluation.js +143 -1
  119. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
  120. package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
  121. package/packages/dd-trace/src/aiguard/redaction.js +8 -2
  122. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
  123. package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
  124. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
  125. package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
  126. package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
  127. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
  128. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
  129. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -1
  130. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
  131. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
  132. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
  133. package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
  134. package/packages/dd-trace/src/appsec/lambda.js +1 -3
  135. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
  136. package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
  137. package/packages/dd-trace/src/appsec/reporter.js +21 -1
  138. package/packages/dd-trace/src/appsec/stack_trace.js +4 -4
  139. package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
  140. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +3 -2
  141. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
  142. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
  143. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
  144. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
  145. package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
  146. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
  147. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
  148. package/packages/dd-trace/src/config/config-types.d.ts +2 -2
  149. package/packages/dd-trace/src/config/defaults.js +5 -5
  150. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  151. package/packages/dd-trace/src/config/helper.js +34 -3
  152. package/packages/dd-trace/src/config/index.js +55 -9
  153. package/packages/dd-trace/src/config/normalize-service.js +1 -1
  154. package/packages/dd-trace/src/config/parsers.js +2 -2
  155. package/packages/dd-trace/src/config/supported-configurations.json +44 -0
  156. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  157. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
  158. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +6 -5
  159. package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -5
  160. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -0
  161. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +4 -6
  162. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
  163. package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
  164. package/packages/dd-trace/src/debugger/unsupported-runtime.js +16 -0
  165. package/packages/dd-trace/src/dogstatsd.js +1 -1
  166. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  167. package/packages/dd-trace/src/exporters/common/request.js +1 -5
  168. package/packages/dd-trace/src/id.js +14 -1
  169. package/packages/dd-trace/src/lambda/handler.js +179 -19
  170. package/packages/dd-trace/src/lambda/index.js +15 -1
  171. package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
  172. package/packages/dd-trace/src/llmobs/agent-manifest.js +19 -2
  173. package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
  174. package/packages/dd-trace/src/llmobs/constants/tags.js +15 -0
  175. package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
  176. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
  177. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
  178. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
  179. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  180. package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
  181. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +5 -0
  182. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +64 -2
  183. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
  184. package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
  185. package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
  186. package/packages/dd-trace/src/llmobs/sdk.js +15 -4
  187. package/packages/dd-trace/src/llmobs/span_processor.js +13 -2
  188. package/packages/dd-trace/src/llmobs/tagger.js +20 -10
  189. package/packages/dd-trace/src/llmobs/util.js +11 -6
  190. package/packages/dd-trace/src/llmobs/writers/base.js +4 -5
  191. package/packages/dd-trace/src/log/log.js +1 -5
  192. package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
  193. package/packages/dd-trace/src/openfeature/flagging_provider.js +48 -21
  194. package/packages/dd-trace/src/openfeature/index.js +6 -0
  195. package/packages/dd-trace/src/openfeature/writers/util.js +4 -4
  196. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
  197. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  198. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
  199. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
  200. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +24 -24
  201. package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
  202. package/packages/dd-trace/src/otel-sampling.js +4 -5
  203. package/packages/dd-trace/src/otel-thread-ctx.js +79 -13
  204. package/packages/dd-trace/src/plugin_manager.js +1 -2
  205. package/packages/dd-trace/src/plugins/database.js +7 -16
  206. package/packages/dd-trace/src/plugins/tracing.js +0 -1
  207. package/packages/dd-trace/src/plugins/util/git.js +99 -59
  208. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
  209. package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -1
  210. package/packages/dd-trace/src/plugins/util/test.js +50 -4
  211. package/packages/dd-trace/src/plugins/util/web.js +90 -20
  212. package/packages/dd-trace/src/priority_sampler.js +1 -2
  213. package/packages/dd-trace/src/profiling/profiler.js +4 -6
  214. package/packages/dd-trace/src/proxy.js +1 -2
  215. package/packages/dd-trace/src/ritm.js +4 -2
  216. package/packages/dd-trace/src/serverless.js +5 -0
  217. package/packages/dd-trace/src/span_processor.js +13 -17
  218. package/packages/dd-trace/src/span_stats.js +4 -1
  219. package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
  220. package/packages/dd-trace/src/telemetry/logs/log-collector.js +1 -1
  221. package/packages/dd-trace/src/util.js +37 -4
  222. package/packages/dd-trace/src/web-tags-cache.js +6 -2
  223. package/vendor/dist/@apm-js-collab/code-transformer/index.js +8 -8
  224. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  225. package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
  226. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
  227. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
  228. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  229. package/vendor/dist/protobufjs/index.js +1 -1
  230. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  231. package/vendor/dist/shell-quote/index.js +1 -1
@@ -0,0 +1,209 @@
1
+ 'use strict'
2
+
3
+ const dc = require('dc-polyfill')
4
+
5
+ const log = require('../../../dd-trace/src/log')
6
+ const { storage } = require('../../../datadog-core')
7
+ const SonicSession = require('./session')
8
+
9
+ const captureChannel = dc.channel('dd-trace:aws:bedrockruntime:sonic:capture-context')
10
+ const spanChannel = dc.tracingChannel('apm:aws:bedrockruntime:sonic:span')
11
+ const MODEL = 'amazon.nova-2-sonic-v1:0'
12
+ const FOUNDATION_MODEL_ARN = /^arn:aws(?:-[a-z0-9]+)*:bedrock:[a-z0-9-]{1,20}::foundation-model\/([^/]+)$/
13
+ const sessionStorage = storage('nova-sonic')
14
+ const instrumentedClients = new WeakSet()
15
+
16
+ function noop () {}
17
+
18
+ /** @param {unknown} identifier */
19
+ function resolveModel (identifier) {
20
+ if (identifier === MODEL) return identifier
21
+ if (typeof identifier !== 'string' || identifier.length > 2048) return
22
+ // Only foundation-model ARNs directly identify a model. Profile IDs need a service lookup,
23
+ // and the bidirectional API currently rejects them. Never infer a model from an ARN suffix.
24
+ const model = FOUNDATION_MODEL_ARN.exec(identifier)?.[1]
25
+ if (model === MODEL) return model
26
+ }
27
+
28
+ /**
29
+ * @param {object} descriptor
30
+ * @param {(fn: () => void) => void} runInContext
31
+ * @param {string} model
32
+ */
33
+ function emitTurn (descriptor, runInContext, model) {
34
+ const { turn } = descriptor
35
+ const common = { descriptor, model }
36
+ runInContext(() => {
37
+ spanChannel.traceSync(() => {
38
+ if (turn.inputStart !== undefined && turn.inputEnd !== undefined && turn.inputEnd >= turn.inputStart) {
39
+ spanChannel.traceSync(noop, {
40
+ ...common, name: 'user speech', kind: 'workflow', startTime: turn.inputStart, finishTime: turn.inputEnd,
41
+ })
42
+ }
43
+ spanChannel.traceSync(noop, {
44
+ ...common,
45
+ name: 'nova sonic response',
46
+ kind: 'llm',
47
+ startTime: descriptor.llmStart,
48
+ finishTime: descriptor.responseEnd,
49
+ })
50
+ const output = turn.output
51
+ if (output.timingValid && output.start !== undefined && output.end > output.start) {
52
+ spanChannel.traceSync(noop, {
53
+ ...common, name: 'agent speech', kind: 'workflow', startTime: output.start, finishTime: output.end,
54
+ })
55
+ }
56
+ }, {
57
+ ...common,
58
+ name: 'nova sonic audio turn',
59
+ kind: 'workflow',
60
+ startTime: descriptor.startTime,
61
+ finishTime: descriptor.finishTime,
62
+ })
63
+ })
64
+ }
65
+
66
+ /**
67
+ * These iterators are application/factory-created objects with no matchable source function.
68
+ * Wrap the concrete iterator, preserving next/return/throw and the SDK's own backpressure/cleanup.
69
+ * No producer is started here; each next delegates exactly once to the original iterator.
70
+ * @param {object} source SDK async iterable.
71
+ * @param {SonicSession} session
72
+ * @param {boolean} outbound
73
+ * @param {(error?: Error) => void} finish
74
+ */
75
+ function observeIterable (source, session, outbound, finish) {
76
+ if (typeof source?.[Symbol.asyncIterator] !== 'function') return source
77
+ const rejected = error => {
78
+ finish(error)
79
+ throw error
80
+ }
81
+ return {
82
+ [Symbol.asyncIterator] () {
83
+ let iterator
84
+ try {
85
+ iterator = source[Symbol.asyncIterator]()
86
+ } catch (error) {
87
+ finish(error)
88
+ throw error
89
+ }
90
+ const observed = { [Symbol.asyncIterator] () { return this } }
91
+ for (const method of ['next', 'return', 'throw']) {
92
+ if (typeof iterator[method] !== 'function') continue
93
+ const fulfilled = result => {
94
+ if (result.done || method === 'return') {
95
+ if (!outbound) finish()
96
+ } else {
97
+ session.observe(result.value, outbound)
98
+ }
99
+ return result
100
+ }
101
+ observed[method] = function (...args) {
102
+ try {
103
+ const result = iterator[method](...args)
104
+ return typeof result?.then === 'function' ? result.then(fulfilled, rejected) : fulfilled(result)
105
+ } catch (error) {
106
+ return rejected(error)
107
+ }
108
+ }
109
+ }
110
+ return observed
111
+ },
112
+ }
113
+ }
114
+
115
+ /** @param {object} client */
116
+ function instrumentInput (client) {
117
+ if (instrumentedClients.has(client)) return true
118
+ try {
119
+ // Install before even an uncaptured send can cache the SDK handler. Cached handlers must
120
+ // look up the invocation's session at execution time, not retain the first session.
121
+ client.middlewareStack.add((next, context) => args => {
122
+ const capture = sessionStorage.getStore()
123
+ if (capture?.client !== client || context.commandName !== 'InvokeModelWithBidirectionalStreamCommand') {
124
+ return next(args)
125
+ }
126
+ const { session, finish } = capture
127
+ const input = { ...args.input, body: observeIterable(args.input.body, session, true, finish) }
128
+ // Smithy passes the shared Command as args. Copy it before asynchronous middleware can
129
+ // read input, so overlapping sends never modify the command or observe another session.
130
+ return next({ ...args, input })
131
+ }, { name: 'datadogSonicInput', step: 'initialize', priority: 'high' })
132
+ instrumentedClients.add(client)
133
+ return true
134
+ } catch {
135
+ log.debug('Cannot observe Nova Sonic input')
136
+ return false
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Called from the existing Smithy send seam. The bidi operation bypasses the ordinary AWS response
142
+ * accumulator: retaining every chunk there would retain a whole conversation twice.
143
+ * @param {Function} send
144
+ * @param {object} client
145
+ * @param {object} command
146
+ * @param {Array<object | Function>} args
147
+ */
148
+ function sendSonic (send, client, command, args) {
149
+ const instrumented = instrumentInput(client)
150
+ const passthrough = () => sessionStorage.run(undefined, () => send.call(client, command, ...args))
151
+ if (!instrumented || !captureChannel.hasSubscribers) return passthrough()
152
+ const model = resolveModel(command.input?.modelId)
153
+ if (!model) return passthrough()
154
+ const context = {}
155
+ captureChannel.publish(context)
156
+ // Only the LLMObs plugin enables capture; the tracing plugin alone does not buffer conversations.
157
+ if (!context.enabled || !context.runInContext) return passthrough()
158
+ const session = new SonicSession(descriptor => emitTurn(descriptor, context.runInContext, model))
159
+ const signal = args[0] !== null && typeof args[0] === 'object' ? args[0]?.abortSignal : undefined
160
+ // Smithy's legacy AbortSignal only exposes onabort; leave that slot to the SDK's transport.
161
+ const hasListeners = typeof signal?.addEventListener === 'function' &&
162
+ typeof signal?.removeEventListener === 'function'
163
+ const finish = error => {
164
+ if (hasListeners) signal.removeEventListener('abort', onAbort)
165
+ // Aborting an already delivered HTTP/2 response can surface as clean iterator EOF.
166
+ if (!error && signal?.aborted) {
167
+ error = new Error('Nova Sonic request aborted')
168
+ error.name = 'AbortError'
169
+ }
170
+ session.finish(error)
171
+ }
172
+ const onAbort = () => finish()
173
+ if (signal?.aborted) onAbort()
174
+ else if (hasListeners) signal.addEventListener('abort', onAbort, { once: true })
175
+
176
+ const completed = result => {
177
+ try {
178
+ if (typeof result?.body?.[Symbol.asyncIterator] === 'function') {
179
+ result.body = observeIterable(result.body, session, false, finish)
180
+ } else {
181
+ finish()
182
+ }
183
+ } catch {
184
+ finish()
185
+ log.debug('Cannot observe Nova Sonic output')
186
+ }
187
+ return result
188
+ }
189
+ const failed = error => {
190
+ finish(error)
191
+ throw error
192
+ }
193
+ const callback = args.at(-1)
194
+ if (typeof callback === 'function') {
195
+ args[args.length - 1] = function (error, result) {
196
+ if (error) finish(error)
197
+ else completed(result)
198
+ return callback.apply(this, arguments)
199
+ }
200
+ }
201
+ try {
202
+ const result = sessionStorage.run({ client, session, finish }, () => send.call(client, command, ...args))
203
+ return typeof callback === 'function' ? result : result.then(completed, failed)
204
+ } catch (error) {
205
+ return failed(error)
206
+ }
207
+ }
208
+
209
+ module.exports = sendSonic
@@ -0,0 +1,492 @@
1
+ 'use strict'
2
+
3
+ const { randomUUID } = require('node:crypto')
4
+
5
+ const log = require('../../../dd-trace/src/log')
6
+ const { InputAudio, OutputAudio } = require('./audio')
7
+
8
+ const MAX_TEXT = 65_536
9
+ // Replayed history can exceed 16 messages after only eight exchanges. Allow longer resumed
10
+ // conversations while bounding tiny/empty message overhead independently of the total text budget.
11
+ const MAX_HISTORY_MESSAGES = 128
12
+ const MAX_BLOCKS = 256
13
+ const MAX_TOOLS = 16
14
+ const MAX_TOOL_TEXT = 4096
15
+
16
+ class Turn {
17
+ windows = []
18
+ userText = ''
19
+ finalText = ''
20
+ speculativeText = ''
21
+ tools = []
22
+ toolResults = []
23
+ inputPcm
24
+ inputRate = 0
25
+ inputStart
26
+ inputEnd
27
+ inputAudioObserved = false
28
+ outputAudioObserved = false
29
+ audioStarted = false
30
+ output = new OutputAudio()
31
+ started
32
+ generationEnd
33
+ textEnd
34
+ completionId
35
+ metrics = { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
36
+ missingModalityMetrics = new Set()
37
+ emitted = false
38
+ }
39
+
40
+ /** @param {Turn} turn */
41
+ function hasInput (turn) {
42
+ return turn.windows.length || turn.userText || turn.toolResults.length
43
+ }
44
+
45
+ /** @param {Turn} turn */
46
+ function completionTime (turn) {
47
+ // Text can finish before audio starts. Keep its boundary as a fallback, without completing
48
+ // an unfinished or interrupted audio response when its final transcript arrives.
49
+ return turn.generationEnd ?? (!turn.audioStarted && turn.output.interrupted === undefined ? turn.textEnd : undefined)
50
+ }
51
+
52
+ /** @param {unknown} value */
53
+ function object (value) {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
55
+ }
56
+
57
+ /**
58
+ * @param {string} text
59
+ */
60
+ function parse (text) {
61
+ try {
62
+ return JSON.parse(text)
63
+ } catch {
64
+ return {}
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Nova's completion ID can cover the entire connection. Local response ownership is driven by
70
+ * pending input and new generation, with FINAL text staying on the preceding response.
71
+ */
72
+ class SonicSession {
73
+ #emit
74
+ #audio = new InputAudio()
75
+ #pending = new Turn()
76
+ #current
77
+ #outbound = new Map()
78
+ #blocks = new Map()
79
+ #completed = new Set()
80
+ #prompt
81
+ #configuration = {}
82
+ #outputConfiguration = {}
83
+ #history = []
84
+ #closed = false
85
+ #turnIndex = 0
86
+ #sessionId = randomUUID()
87
+ #providerSessionId
88
+ #totals = { input_tokens: 0, output_tokens: 0 }
89
+ #modalityTotals = {
90
+ input_audio_tokens: 0, output_audio_tokens: 0, input_text_tokens: 0, output_text_tokens: 0,
91
+ }
92
+
93
+ /** @param {(descriptor: object) => void} emit */
94
+ constructor (emit) {
95
+ this.#emit = emit
96
+ }
97
+
98
+ /**
99
+ * Observe decoded SDK union members without consuming either iterable ahead of its owner.
100
+ * @param {{ chunk?: { bytes?: Uint8Array } }} event
101
+ * @param {boolean} outbound
102
+ * @param {number} [now]
103
+ */
104
+ observe (event, outbound, now = Date.now()) {
105
+ if (this.#closed) return
106
+ try {
107
+ const bytes = event?.chunk?.bytes
108
+ if (!ArrayBuffer.isView(bytes)) return
109
+ const payload = parse(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('utf8'))
110
+ if (!object(payload?.event)) return
111
+ for (const [name, data] of Object.entries(payload.event)) {
112
+ if (!object(data)) continue
113
+ if (outbound) {
114
+ this.#sent(name, data, now)
115
+ } else {
116
+ this.#received(name, data, now)
117
+ }
118
+ if (this.#closed) break
119
+ }
120
+ } catch {
121
+ // Malformed telemetry never changes transport behavior or logs captured conversation data.
122
+ log.debug('Cannot process Nova Sonic telemetry')
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @param {string} name
128
+ * @param {object} data
129
+ * @param {number} now
130
+ */
131
+ #sent (name, data, now) {
132
+ const key = data.contentName ?? ''
133
+ switch (name) {
134
+ case 'sessionStart':
135
+ for (const name of ['inferenceConfiguration', 'turnDetectionConfiguration']) {
136
+ const config = this.#configuration[name] = {}
137
+ if (!object(data[name])) continue
138
+ for (const key of ['maxTokens', 'topP', 'temperature', 'endpointingSensitivity']) {
139
+ const value = data[name][key]
140
+ if ((typeof value === 'number' || typeof value === 'string') && String(value).length < 128) {
141
+ config[key] = value
142
+ }
143
+ }
144
+ }
145
+ break
146
+ case 'promptStart':
147
+ if (this.#prompt !== undefined && this.#prompt !== data.promptName) {
148
+ this.finish(undefined, now)
149
+ return
150
+ }
151
+ this.#prompt = data.promptName
152
+ this.#outputConfiguration = data.audioOutputConfiguration ?? {}
153
+ break
154
+ case 'contentStart':
155
+ this.#outbound.set(key, { ...data, text: '' })
156
+ if (data.type === 'AUDIO' && data.role === 'USER') this.#audio.configure(data.audioInputConfiguration)
157
+ this.#bound(this.#outbound)
158
+ break
159
+ case 'audioInput': {
160
+ const block = this.#outbound.get(key)
161
+ if (block?.type === 'AUDIO' && block.role === 'USER') {
162
+ if (typeof data.content === 'string' && data.content.length) this.#pending.inputAudioObserved = true
163
+ this.#audio.append(data.content, now)
164
+ }
165
+ break
166
+ }
167
+ case 'textInput': {
168
+ const block = this.#outbound.get(key)
169
+ if (block && typeof data.content === 'string') block.text = (block.text + data.content).slice(0, MAX_TEXT)
170
+ break
171
+ }
172
+ case 'toolResult': {
173
+ const block = this.#outbound.get(key)
174
+ const target = this.#current ?? this.#pending
175
+ if (target.toolResults.length < MAX_TOOLS && typeof data.content === 'string') {
176
+ target.toolResults.push({
177
+ role: 'tool',
178
+ content: '',
179
+ toolResults: [{
180
+ result: data.content.slice(0, MAX_TOOL_TEXT),
181
+ toolId: block?.toolResultInputConfiguration?.toolUseId ?? '',
182
+ type: 'tool_result',
183
+ }],
184
+ })
185
+ }
186
+ break
187
+ }
188
+ case 'contentEnd': {
189
+ const block = this.#outbound.get(key)
190
+ this.#outbound.delete(key)
191
+ if (block?.type !== 'TEXT') break
192
+ if (block.role === 'USER' && block.interactive) {
193
+ this.#pending.userText = (this.#pending.userText + block.text).slice(0, MAX_TEXT)
194
+ } else if (this.#history.length < MAX_HISTORY_MESSAGES) {
195
+ const remaining = MAX_TEXT - this.#history.reduce((n, message) => n + message.content.length, 0)
196
+ this.#history.push({
197
+ role: String(block.role ?? 'USER').toLowerCase(), content: block.text.slice(0, remaining),
198
+ })
199
+ }
200
+ break
201
+ }
202
+ }
203
+ }
204
+
205
+ /**
206
+ * @param {Map<string, object>} map
207
+ */
208
+ #bound (map) {
209
+ while (map.size > MAX_BLOCKS) map.delete(map.keys().next().value)
210
+ }
211
+
212
+ #hasPendingInput () {
213
+ if (hasInput(this.#pending)) return true
214
+ // ASR text may precede contentEnd, even when speech boundary events are absent.
215
+ for (const block of this.#blocks.values()) {
216
+ if (block.turn === this.#pending && block.data.role === 'USER' && block.text) return true
217
+ }
218
+ return false
219
+ }
220
+
221
+ /**
222
+ * @param {object} data
223
+ * @param {number} now
224
+ */
225
+ #startResponse (data, now) {
226
+ if (!this.#current || this.#hasPendingInput()) {
227
+ if (this.#current) {
228
+ // A new response can arrive before the previous response's text blocks close.
229
+ for (const block of this.#blocks.values()) {
230
+ if (block.turn === this.#current) this.#text(block)
231
+ }
232
+ this.#emitTurn(this.#current, now)
233
+ }
234
+ const turn = this.#pending
235
+ this.#pending = new Turn()
236
+ turn.started = now
237
+ turn.completionId = data.completionId
238
+ this.#snapshotInput(turn)
239
+ this.#current = turn
240
+ for (const [key, block] of this.#blocks) {
241
+ if (block.turn !== turn) this.#blocks.delete(key)
242
+ }
243
+ }
244
+ return this.#current
245
+ }
246
+
247
+ /** @param {Turn} turn */
248
+ #snapshotInput (turn) {
249
+ if (turn.windows.length && !turn.inputPcm) {
250
+ turn.inputRate = this.#audio.rate
251
+ turn.inputPcm = this.#audio.clip(turn.windows[0].start_ms, turn.windows.at(-1).end_ms)
252
+ }
253
+ }
254
+
255
+ /**
256
+ * @param {string} name
257
+ * @param {object} data
258
+ * @param {number} now
259
+ */
260
+ #received (name, data, now) {
261
+ // Once any turn has been emitted, its conversation identity must never change.
262
+ if (!this.#providerSessionId && typeof data.sessionId === 'string' && data.sessionId) {
263
+ this.#providerSessionId = data.sessionId
264
+ if (!this.#turnIndex) this.#sessionId = data.sessionId
265
+ }
266
+ switch (name) {
267
+ case 'userSpeechStart': {
268
+ const offset = data.inputAudioOffsetMs
269
+ if (!Number.isFinite(offset) || offset < 0 || this.#pending.windows.length >= MAX_BLOCKS) return
270
+ this.#pending.windows.push({ start_ms: offset })
271
+ this.#pending.inputAudioObserved = true
272
+ this.#pending.inputEnd = undefined
273
+ this.#pending.inputStart ??= this.#audio.anchor === undefined
274
+ ? now
275
+ : Math.min(now, this.#audio.anchor + offset)
276
+ break
277
+ }
278
+ case 'userSpeechEnd': {
279
+ const window = this.#pending.windows.at(-1)
280
+ const end = data.inputAudioOffsetMs
281
+ if (!window || !Number.isFinite(end) || end <= window.start_ms) return
282
+ window.end_ms = end
283
+ if (Number.isFinite(data.inputAudioDetectionOffsetMs)) window.detection_ms = data.inputAudioDetectionOffsetMs
284
+ this.#pending.inputEnd = now
285
+ break
286
+ }
287
+ case 'contentStart': {
288
+ const key = data.contentId ?? ''
289
+ if (this.#blocks.has(key) || this.#completed.has(key)) return
290
+ const fields = object(data.additionalModelFields)
291
+ ? data.additionalModelFields
292
+ : parse(data.additionalModelFields)
293
+ const stage = object(fields) ? fields.generationStage : undefined
294
+ let turn
295
+ if (data.role === 'ASSISTANT' || data.type === 'TOOL') {
296
+ const newTextResponse = this.#current && !this.#current.audioStarted &&
297
+ completionTime(this.#current) !== undefined && this.#hasPendingInput()
298
+ turn = stage === 'FINAL' && this.#current && !newTextResponse
299
+ ? this.#current
300
+ : this.#startResponse(data, now)
301
+ } else {
302
+ turn = this.#hasPendingInput() || !this.#current || completionTime(this.#current) !== undefined
303
+ ? this.#pending
304
+ : this.#current
305
+ }
306
+ if (data.role === 'ASSISTANT' && data.type === 'AUDIO') turn.audioStarted = true
307
+ this.#blocks.set(key, { data, stage, turn, text: '', control: false })
308
+ this.#bound(this.#blocks)
309
+ break
310
+ }
311
+ case 'textOutput':
312
+ case 'audioOutput':
313
+ case 'toolUse':
314
+ case 'contentEnd':
315
+ this.#content(name, data, now)
316
+ break
317
+ case 'usageEvent':
318
+ this.#usage(data)
319
+ break
320
+ case 'completionEnd':
321
+ this.finish(undefined, now)
322
+ break
323
+ }
324
+ }
325
+
326
+ /**
327
+ * @param {string} name
328
+ * @param {object} data
329
+ * @param {number} now
330
+ */
331
+ #content (name, data, now) {
332
+ const key = data.contentId ?? ''
333
+ const block = this.#blocks.get(key)
334
+ if (!block) return
335
+ const { turn } = block
336
+ if (name === 'textOutput' && typeof data.content === 'string') {
337
+ block.text = (block.text + data.content).slice(0, MAX_TEXT)
338
+ if (block.data.role === 'ASSISTANT' && parse(block.text)?.interrupted === true) {
339
+ // Keep literal JSON until contentEnd confirms protocol control. Retain its
340
+ // receipt time so confirmation does not move the projected playback cut.
341
+ block.interruptionTime ??= now
342
+ } else {
343
+ block.interruptionTime = undefined
344
+ }
345
+ } else if (name === 'audioOutput' && block.data.role === 'ASSISTANT') {
346
+ if (typeof data.content === 'string' && data.content.length) turn.outputAudioObserved = true
347
+ turn.output.append(data.content, block.data.audioOutputConfiguration ?? this.#outputConfiguration, now)
348
+ } else if (name === 'toolUse' && turn.tools.length < MAX_TOOLS) {
349
+ const args = typeof data.content === 'string' && data.content.length <= MAX_TOOL_TEXT ? parse(data.content) : {}
350
+ turn.tools.push({
351
+ name: data.toolName ?? '',
352
+ arguments: object(args) ? args : {},
353
+ toolId: data.toolUseId ?? '',
354
+ type: 'function',
355
+ })
356
+ } else if (name === 'contentEnd') {
357
+ if (block.data.role === 'ASSISTANT' && data.stopReason === 'INTERRUPTED') {
358
+ block.control = block.interruptionTime !== undefined
359
+ turn.output.interrupt(block.interruptionTime ?? now)
360
+ }
361
+ this.#text(block)
362
+ if (block.data.type === 'AUDIO' && data.stopReason === 'END_TURN') turn.generationEnd = now
363
+ if (block.data.role === 'ASSISTANT' && block.data.type === 'TEXT' &&
364
+ !block.control && data.stopReason === 'END_TURN') turn.textEnd = now
365
+ this.#blocks.delete(key)
366
+ this.#completed.add(key)
367
+ if (this.#completed.size > MAX_BLOCKS) this.#completed.delete(this.#completed.values().next().value)
368
+ }
369
+ }
370
+
371
+ /** @param {{ control: boolean, data: object, stage?: string, text: string, turn: object }} block */
372
+ #text (block) {
373
+ if (block.control) return
374
+ const field = block.data.role === 'USER'
375
+ ? 'userText'
376
+ : block.stage === 'FINAL' ? 'finalText' : 'speculativeText'
377
+ block.turn[field] = (block.turn[field] + block.text).slice(0, MAX_TEXT)
378
+ }
379
+
380
+ /** @param {object} data */
381
+ #usage (data) {
382
+ for (const direction of ['input', 'output']) {
383
+ const key = `${direction}_tokens`
384
+ const total = data[direction === 'input' ? 'totalInputTokens' : 'totalOutputTokens']
385
+ if (!Number.isSafeInteger(total) || total < this.#totals[key]) continue
386
+ const delta = total - this.#totals[key]
387
+ this.#totals[key] = total
388
+ const target = !this.#current || (direction === 'input' && this.#hasPendingInput())
389
+ ? this.#pending
390
+ : this.#current
391
+ target.metrics[key] += delta
392
+ target.metrics.total_tokens += delta
393
+
394
+ // Each modality is a subset of the aggregate and has its own cumulative baseline.
395
+ // Missing text must not discard valid speech or charge a previous turn's gap.
396
+ let remaining = delta
397
+ for (const [modality, field] of [['audio', 'speechTokens'], ['text', 'textTokens']]) {
398
+ const modalityKey = `${direction}_${modality}_tokens`
399
+ const modalityTotal = data.details?.total?.[direction]?.[field]
400
+ const previous = this.#modalityTotals[modalityKey]
401
+ const valid = Number.isSafeInteger(modalityTotal) && modalityTotal >= 0 && modalityTotal <= total &&
402
+ (previous === undefined || modalityTotal >= previous)
403
+ const modalityDelta = valid && previous !== undefined ? modalityTotal - previous : undefined
404
+ this.#modalityTotals[modalityKey] = valid ? modalityTotal : delta ? undefined : previous
405
+ if (modalityDelta === undefined || modalityDelta > remaining) {
406
+ if (delta || modalityDelta > remaining) {
407
+ target.missingModalityMetrics.add(modalityKey)
408
+ delete target.metrics[modalityKey]
409
+ }
410
+ } else {
411
+ remaining -= modalityDelta
412
+ if (!target.missingModalityMetrics.has(modalityKey)) {
413
+ target.metrics[modalityKey] = (target.metrics[modalityKey] ?? 0) + modalityDelta
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ /**
421
+ * @param {Turn} turn
422
+ * @param {number} now
423
+ * @param {Error} [error]
424
+ */
425
+ #emitTurn (turn, now, error) {
426
+ if (turn.emitted) return
427
+ turn.emitted = true
428
+ this.#turnIndex++
429
+ try {
430
+ this.#snapshotInput(turn)
431
+ const start = turn.inputStart ?? turn.started ?? now
432
+ const llmStart = turn.inputEnd ?? turn.started ?? start
433
+ const completed = completionTime(turn)
434
+ const responseEnd = Math.max(llmStart, completed ?? turn.output.interrupted ?? now)
435
+ this.#emit({
436
+ turn,
437
+ error,
438
+ sessionId: this.#sessionId,
439
+ history: this.#history,
440
+ startTime: start,
441
+ finishTime: Math.max(responseEnd, turn.output.end ?? 0, turn.inputEnd ?? 0),
442
+ llmStart,
443
+ responseEnd,
444
+ metadata: {
445
+ turn_index: this.#turnIndex,
446
+ completion_id: turn.completionId,
447
+ speech_windows: turn.windows,
448
+ ttfa_boundary: 'speech_end_event_receipt',
449
+ output_timing: 'projected_playback',
450
+ interrupted: turn.output.interrupted !== undefined,
451
+ partial: completed === undefined,
452
+ transcript_stage: turn.finalText ? 'FINAL' : 'SPECULATIVE',
453
+ usage_attribution: 'event_receipt',
454
+ generated_output_bytes: turn.output.generatedBytes,
455
+ session_configuration: this.#configuration,
456
+ },
457
+ })
458
+ } catch {
459
+ log.debug('Cannot emit Nova Sonic turn')
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Input EOF is deliberately not a finish. Only output completion, cancellation or failure flushes.
465
+ * @param {Error} [error]
466
+ * @param {number} [now]
467
+ */
468
+ finish (error, now = Date.now()) {
469
+ if (this.#closed) return
470
+ this.#closed = true
471
+ for (const block of this.#blocks.values()) this.#text(block)
472
+ if (this.#current) {
473
+ // A failure before the next response starts belongs to its pending utterance,
474
+ // not to the response that has already completed.
475
+ const laterTurnFailed = completionTime(this.#current) !== undefined && this.#hasPendingInput()
476
+ this.#emitTurn(this.#current, now, laterTurnFailed ? undefined : error)
477
+ }
478
+ if (hasInput(this.#pending) || this.#pending.metrics.total_tokens || (error && !this.#current)) {
479
+ this.#emitTurn(this.#pending, now, error)
480
+ }
481
+ this.#current = undefined
482
+ this.#pending = new Turn()
483
+ this.#blocks.clear()
484
+ this.#outbound.clear()
485
+ this.#completed.clear()
486
+ this.#history = []
487
+ this.#audio.clear()
488
+ this.#emit = undefined
489
+ }
490
+ }
491
+
492
+ module.exports = SonicSession