dd-trace 6.20.1 → 6.22.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 (225) 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-setup.mjs +10 -7
  16. package/index.d.ts +116 -17
  17. package/initialize.mjs +1 -3
  18. package/openfeature/rules-based.d.ts +11 -0
  19. package/openfeature/rules-based.js +7 -0
  20. package/package.json +13 -11
  21. package/packages/datadog-core/src/storage.js +16 -0
  22. package/packages/datadog-esbuild/index.js +18 -12
  23. package/packages/datadog-instrumentations/src/aerospike.js +3 -3
  24. package/packages/datadog-instrumentations/src/ai.js +3 -3
  25. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
  26. package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
  27. package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
  28. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
  29. package/packages/datadog-instrumentations/src/cucumber.js +48 -33
  30. package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
  31. package/packages/datadog-instrumentations/src/electron.js +6 -6
  32. package/packages/datadog-instrumentations/src/fastify.js +13 -4
  33. package/packages/datadog-instrumentations/src/fs.js +2 -2
  34. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
  35. package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
  36. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +2 -2
  37. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
  38. package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
  39. package/packages/datadog-instrumentations/src/helpers/register.js +36 -1
  40. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
  41. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +87 -6
  42. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
  43. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
  44. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +64 -11
  45. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
  47. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
  48. package/packages/datadog-instrumentations/src/ioredis.js +7 -5
  49. package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
  50. package/packages/datadog-instrumentations/src/jest.js +122 -11
  51. package/packages/datadog-instrumentations/src/knex.js +9 -5
  52. package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
  53. package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
  54. package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
  55. package/packages/datadog-instrumentations/src/mocha/utils.js +127 -38
  56. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +61 -34
  57. package/packages/datadog-instrumentations/src/mocha/worker.js +7 -5
  58. package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
  59. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  60. package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
  61. package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
  62. package/packages/datadog-instrumentations/src/nova-sonic/session.js +483 -0
  63. package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
  64. package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
  65. package/packages/datadog-instrumentations/src/openai.js +7 -0
  66. package/packages/datadog-instrumentations/src/pino.js +5 -6
  67. package/packages/datadog-instrumentations/src/playwright.js +3 -0
  68. package/packages/datadog-instrumentations/src/redis.js +27 -13
  69. package/packages/datadog-instrumentations/src/router.js +30 -0
  70. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
  71. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -5
  72. package/packages/datadog-instrumentations/src/vitest-main.js +9 -17
  73. package/packages/datadog-instrumentations/src/vitest-util.js +2 -2
  74. package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
  75. package/packages/datadog-instrumentations/src/vitest.js +2 -2
  76. package/packages/datadog-instrumentations/src/webdriverio.js +118 -21
  77. package/packages/datadog-instrumentations/src/ws.js +1 -4
  78. package/packages/datadog-plugin-amqplib/src/client.js +1 -2
  79. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
  80. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
  81. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
  82. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
  83. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
  84. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  85. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
  86. package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
  87. package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
  88. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
  89. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +18 -5
  90. package/packages/datadog-plugin-cypress/src/index.js +6 -6
  91. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
  92. package/packages/datadog-plugin-cypress/src/support.js +1 -4
  93. package/packages/datadog-plugin-express/src/code_origin.js +1 -2
  94. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
  95. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
  96. package/packages/datadog-plugin-graphql/src/execute.js +5 -5
  97. package/packages/datadog-plugin-grpc/src/util.js +1 -2
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
  99. package/packages/datadog-plugin-mocha/src/index.js +223 -39
  100. package/packages/datadog-plugin-mocha/src/webdriverio-video.js +453 -0
  101. package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
  102. package/packages/datadog-plugin-next/src/index.js +1 -2
  103. package/packages/datadog-plugin-openai/src/tracing.js +5 -2
  104. package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
  105. package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
  106. package/packages/datadog-plugin-postgres/src/index.js +24 -0
  107. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
  108. package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
  109. package/packages/datadog-plugin-vitest/src/index.js +5 -2
  110. package/packages/datadog-shimmer/src/shimmer.js +5 -4
  111. package/packages/datadog-webpack/index.js +55 -43
  112. package/packages/datadog-webpack/src/loader.js +56 -8
  113. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
  114. package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
  115. package/packages/dd-trace/src/aiguard/redaction.js +8 -2
  116. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
  117. package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
  118. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
  119. package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
  120. package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
  121. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
  122. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
  123. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
  125. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
  126. package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
  127. package/packages/dd-trace/src/appsec/lambda.js +1 -3
  128. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
  129. package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
  130. package/packages/dd-trace/src/appsec/reporter.js +21 -1
  131. package/packages/dd-trace/src/appsec/stack_trace.js +1 -2
  132. package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
  135. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
  137. package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
  138. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
  139. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
  140. package/packages/dd-trace/src/config/config-types.d.ts +2 -2
  141. package/packages/dd-trace/src/config/defaults.js +5 -5
  142. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -0
  143. package/packages/dd-trace/src/config/helper.js +34 -3
  144. package/packages/dd-trace/src/config/index.js +10 -9
  145. package/packages/dd-trace/src/config/normalize-service.js +1 -1
  146. package/packages/dd-trace/src/config/parsers.js +2 -2
  147. package/packages/dd-trace/src/config/supported-configurations.json +35 -0
  148. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  149. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
  150. package/packages/dd-trace/src/debugger/constants.js +1 -0
  151. package/packages/dd-trace/src/debugger/devtools_client/index.js +5 -3
  152. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +17 -12
  153. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -2
  154. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
  155. package/packages/dd-trace/src/debugger/devtools_client/worker-error.js +21 -0
  156. package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
  157. package/packages/dd-trace/src/debugger/index.js +24 -2
  158. package/packages/dd-trace/src/debugger/probe_sampler.js +108 -11
  159. package/packages/dd-trace/src/dogstatsd.js +1 -1
  160. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  161. package/packages/dd-trace/src/exporters/common/request.js +2 -5
  162. package/packages/dd-trace/src/exporters/common/url.js +1 -1
  163. package/packages/dd-trace/src/id.js +15 -5
  164. package/packages/dd-trace/src/lambda/handler.js +179 -19
  165. package/packages/dd-trace/src/lambda/index.js +15 -1
  166. package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
  167. package/packages/dd-trace/src/llmobs/agent-manifest.js +296 -0
  168. package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
  169. package/packages/dd-trace/src/llmobs/constants/tags.js +13 -0
  170. package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
  171. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
  172. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
  173. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
  174. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  175. package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
  176. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +56 -2
  177. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
  178. package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
  179. package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
  180. package/packages/dd-trace/src/llmobs/sdk.js +23 -6
  181. package/packages/dd-trace/src/llmobs/span_processor.js +18 -8
  182. package/packages/dd-trace/src/llmobs/tagger.js +56 -3
  183. package/packages/dd-trace/src/llmobs/util.js +1 -2
  184. package/packages/dd-trace/src/llmobs/writers/base.js +7 -6
  185. package/packages/dd-trace/src/llmobs/writers/util.js +3 -1
  186. package/packages/dd-trace/src/log/log.js +1 -5
  187. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +30 -1
  188. package/packages/dd-trace/src/openfeature/configuration_source.js +17 -2
  189. package/packages/dd-trace/src/openfeature/debug-logging-hook.js +25 -0
  190. package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
  191. package/packages/dd-trace/src/openfeature/flagging_provider.js +59 -3
  192. package/packages/dd-trace/src/openfeature/remote_config.js +9 -0
  193. package/packages/dd-trace/src/openfeature/writers/util.js +43 -4
  194. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
  195. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  196. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
  197. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
  198. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +1 -1
  199. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -23
  200. package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
  201. package/packages/dd-trace/src/otel-sampling.js +4 -5
  202. package/packages/dd-trace/src/otel-thread-ctx.js +81 -15
  203. package/packages/dd-trace/src/plugins/database.js +7 -16
  204. package/packages/dd-trace/src/plugins/util/git.js +9 -25
  205. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
  206. package/packages/dd-trace/src/plugins/util/test.js +48 -4
  207. package/packages/dd-trace/src/priority_sampler.js +1 -2
  208. package/packages/dd-trace/src/profiling/profiler.js +4 -6
  209. package/packages/dd-trace/src/profiling/profilers/wall.js +4 -10
  210. package/packages/dd-trace/src/proxy.js +3 -2
  211. package/packages/dd-trace/src/ritm.js +4 -2
  212. package/packages/dd-trace/src/serverless.js +10 -0
  213. package/packages/dd-trace/src/span_processor.js +13 -17
  214. package/packages/dd-trace/src/storage-channels.js +112 -46
  215. package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
  216. package/packages/dd-trace/src/util.js +29 -0
  217. package/packages/dd-trace/src/web-tags-cache.js +6 -2
  218. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  219. package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
  220. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
  221. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
  222. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  223. package/vendor/dist/protobufjs/index.js +1 -1
  224. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  225. package/vendor/dist/shell-quote/index.js +1 -1
@@ -0,0 +1,483 @@
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
+ missingAudioMetrics = 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
+ #audioTotals = { input: 0, output: 0 }
90
+
91
+ /** @param {(descriptor: object) => void} emit */
92
+ constructor (emit) {
93
+ this.#emit = emit
94
+ }
95
+
96
+ /**
97
+ * Observe decoded SDK union members without consuming either iterable ahead of its owner.
98
+ * @param {{ chunk?: { bytes?: Uint8Array } }} event
99
+ * @param {boolean} outbound
100
+ * @param {number} [now]
101
+ */
102
+ observe (event, outbound, now = Date.now()) {
103
+ if (this.#closed) return
104
+ try {
105
+ const bytes = event?.chunk?.bytes
106
+ if (!ArrayBuffer.isView(bytes)) return
107
+ const payload = parse(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('utf8'))
108
+ if (!object(payload?.event)) return
109
+ for (const [name, data] of Object.entries(payload.event)) {
110
+ if (!object(data)) continue
111
+ if (outbound) {
112
+ this.#sent(name, data, now)
113
+ } else {
114
+ this.#received(name, data, now)
115
+ }
116
+ if (this.#closed) break
117
+ }
118
+ } catch {
119
+ // Malformed telemetry never changes transport behavior or logs captured conversation data.
120
+ log.debug('Cannot process Nova Sonic telemetry')
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @param {string} name
126
+ * @param {object} data
127
+ * @param {number} now
128
+ */
129
+ #sent (name, data, now) {
130
+ const key = data.contentName ?? ''
131
+ switch (name) {
132
+ case 'sessionStart':
133
+ for (const name of ['inferenceConfiguration', 'turnDetectionConfiguration']) {
134
+ const config = this.#configuration[name] = {}
135
+ if (!object(data[name])) continue
136
+ for (const key of ['maxTokens', 'topP', 'temperature', 'endpointingSensitivity']) {
137
+ const value = data[name][key]
138
+ if ((typeof value === 'number' || typeof value === 'string') && String(value).length < 128) {
139
+ config[key] = value
140
+ }
141
+ }
142
+ }
143
+ break
144
+ case 'promptStart':
145
+ if (this.#prompt !== undefined && this.#prompt !== data.promptName) {
146
+ this.finish(undefined, now)
147
+ return
148
+ }
149
+ this.#prompt = data.promptName
150
+ this.#outputConfiguration = data.audioOutputConfiguration ?? {}
151
+ break
152
+ case 'contentStart':
153
+ this.#outbound.set(key, { ...data, text: '' })
154
+ if (data.type === 'AUDIO' && data.role === 'USER') this.#audio.configure(data.audioInputConfiguration)
155
+ this.#bound(this.#outbound)
156
+ break
157
+ case 'audioInput': {
158
+ const block = this.#outbound.get(key)
159
+ if (block?.type === 'AUDIO' && block.role === 'USER') {
160
+ if (typeof data.content === 'string' && data.content.length) this.#pending.inputAudioObserved = true
161
+ this.#audio.append(data.content, now)
162
+ }
163
+ break
164
+ }
165
+ case 'textInput': {
166
+ const block = this.#outbound.get(key)
167
+ if (block && typeof data.content === 'string') block.text = (block.text + data.content).slice(0, MAX_TEXT)
168
+ break
169
+ }
170
+ case 'toolResult': {
171
+ const block = this.#outbound.get(key)
172
+ const target = this.#current ?? this.#pending
173
+ if (target.toolResults.length < MAX_TOOLS && typeof data.content === 'string') {
174
+ target.toolResults.push({
175
+ role: 'tool',
176
+ content: '',
177
+ toolResults: [{
178
+ result: data.content.slice(0, MAX_TOOL_TEXT),
179
+ toolId: block?.toolResultInputConfiguration?.toolUseId ?? '',
180
+ type: 'tool_result',
181
+ }],
182
+ })
183
+ }
184
+ break
185
+ }
186
+ case 'contentEnd': {
187
+ const block = this.#outbound.get(key)
188
+ this.#outbound.delete(key)
189
+ if (block?.type !== 'TEXT') break
190
+ if (block.role === 'USER' && block.interactive) {
191
+ this.#pending.userText = (this.#pending.userText + block.text).slice(0, MAX_TEXT)
192
+ } else if (this.#history.length < MAX_HISTORY_MESSAGES) {
193
+ const remaining = MAX_TEXT - this.#history.reduce((n, message) => n + message.content.length, 0)
194
+ this.#history.push({
195
+ role: String(block.role ?? 'USER').toLowerCase(), content: block.text.slice(0, remaining),
196
+ })
197
+ }
198
+ break
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * @param {Map<string, object>} map
205
+ */
206
+ #bound (map) {
207
+ while (map.size > MAX_BLOCKS) map.delete(map.keys().next().value)
208
+ }
209
+
210
+ #hasPendingInput () {
211
+ if (hasInput(this.#pending)) return true
212
+ // ASR text may precede contentEnd, even when speech boundary events are absent.
213
+ for (const block of this.#blocks.values()) {
214
+ if (block.turn === this.#pending && block.data.role === 'USER' && block.text) return true
215
+ }
216
+ return false
217
+ }
218
+
219
+ /**
220
+ * @param {object} data
221
+ * @param {number} now
222
+ */
223
+ #startResponse (data, now) {
224
+ if (!this.#current || this.#hasPendingInput()) {
225
+ if (this.#current) {
226
+ // A new response can arrive before the previous response's text blocks close.
227
+ for (const block of this.#blocks.values()) {
228
+ if (block.turn === this.#current) this.#text(block)
229
+ }
230
+ this.#emitTurn(this.#current, now)
231
+ }
232
+ const turn = this.#pending
233
+ this.#pending = new Turn()
234
+ turn.started = now
235
+ turn.completionId = data.completionId
236
+ this.#snapshotInput(turn)
237
+ this.#current = turn
238
+ for (const [key, block] of this.#blocks) {
239
+ if (block.turn !== turn) this.#blocks.delete(key)
240
+ }
241
+ }
242
+ return this.#current
243
+ }
244
+
245
+ /** @param {Turn} turn */
246
+ #snapshotInput (turn) {
247
+ if (turn.windows.length && !turn.inputPcm) {
248
+ turn.inputRate = this.#audio.rate
249
+ turn.inputPcm = this.#audio.clip(turn.windows[0].start_ms, turn.windows.at(-1).end_ms)
250
+ }
251
+ }
252
+
253
+ /**
254
+ * @param {string} name
255
+ * @param {object} data
256
+ * @param {number} now
257
+ */
258
+ #received (name, data, now) {
259
+ // Once any turn has been emitted, its conversation identity must never change.
260
+ if (!this.#providerSessionId && typeof data.sessionId === 'string' && data.sessionId) {
261
+ this.#providerSessionId = data.sessionId
262
+ if (!this.#turnIndex) this.#sessionId = data.sessionId
263
+ }
264
+ switch (name) {
265
+ case 'userSpeechStart': {
266
+ const offset = data.inputAudioOffsetMs
267
+ if (!Number.isFinite(offset) || offset < 0 || this.#pending.windows.length >= MAX_BLOCKS) return
268
+ this.#pending.windows.push({ start_ms: offset })
269
+ this.#pending.inputAudioObserved = true
270
+ this.#pending.inputEnd = undefined
271
+ this.#pending.inputStart ??= this.#audio.anchor === undefined
272
+ ? now
273
+ : Math.min(now, this.#audio.anchor + offset)
274
+ break
275
+ }
276
+ case 'userSpeechEnd': {
277
+ const window = this.#pending.windows.at(-1)
278
+ const end = data.inputAudioOffsetMs
279
+ if (!window || !Number.isFinite(end) || end <= window.start_ms) return
280
+ window.end_ms = end
281
+ if (Number.isFinite(data.inputAudioDetectionOffsetMs)) window.detection_ms = data.inputAudioDetectionOffsetMs
282
+ this.#pending.inputEnd = now
283
+ break
284
+ }
285
+ case 'contentStart': {
286
+ const key = data.contentId ?? ''
287
+ if (this.#blocks.has(key) || this.#completed.has(key)) return
288
+ const fields = object(data.additionalModelFields)
289
+ ? data.additionalModelFields
290
+ : parse(data.additionalModelFields)
291
+ const stage = object(fields) ? fields.generationStage : undefined
292
+ let turn
293
+ if (data.role === 'ASSISTANT' || data.type === 'TOOL') {
294
+ const newTextResponse = this.#current && !this.#current.audioStarted &&
295
+ completionTime(this.#current) !== undefined && this.#hasPendingInput()
296
+ turn = stage === 'FINAL' && this.#current && !newTextResponse
297
+ ? this.#current
298
+ : this.#startResponse(data, now)
299
+ } else {
300
+ turn = this.#hasPendingInput() || !this.#current || completionTime(this.#current) !== undefined
301
+ ? this.#pending
302
+ : this.#current
303
+ }
304
+ if (data.role === 'ASSISTANT' && data.type === 'AUDIO') turn.audioStarted = true
305
+ this.#blocks.set(key, { data, stage, turn, text: '', control: false })
306
+ this.#bound(this.#blocks)
307
+ break
308
+ }
309
+ case 'textOutput':
310
+ case 'audioOutput':
311
+ case 'toolUse':
312
+ case 'contentEnd':
313
+ this.#content(name, data, now)
314
+ break
315
+ case 'usageEvent':
316
+ this.#usage(data)
317
+ break
318
+ case 'completionEnd':
319
+ this.finish(undefined, now)
320
+ break
321
+ }
322
+ }
323
+
324
+ /**
325
+ * @param {string} name
326
+ * @param {object} data
327
+ * @param {number} now
328
+ */
329
+ #content (name, data, now) {
330
+ const key = data.contentId ?? ''
331
+ const block = this.#blocks.get(key)
332
+ if (!block) return
333
+ const { turn } = block
334
+ if (name === 'textOutput' && typeof data.content === 'string') {
335
+ block.text = (block.text + data.content).slice(0, MAX_TEXT)
336
+ if (block.data.role === 'ASSISTANT' && parse(block.text)?.interrupted === true) {
337
+ // Keep literal JSON until contentEnd confirms protocol control. Retain its
338
+ // receipt time so confirmation does not move the projected playback cut.
339
+ block.interruptionTime ??= now
340
+ } else {
341
+ block.interruptionTime = undefined
342
+ }
343
+ } else if (name === 'audioOutput' && block.data.role === 'ASSISTANT') {
344
+ if (typeof data.content === 'string' && data.content.length) turn.outputAudioObserved = true
345
+ turn.output.append(data.content, block.data.audioOutputConfiguration ?? this.#outputConfiguration, now)
346
+ } else if (name === 'toolUse' && turn.tools.length < MAX_TOOLS) {
347
+ const args = typeof data.content === 'string' && data.content.length <= MAX_TOOL_TEXT ? parse(data.content) : {}
348
+ turn.tools.push({
349
+ name: data.toolName ?? '',
350
+ arguments: object(args) ? args : {},
351
+ toolId: data.toolUseId ?? '',
352
+ type: 'function',
353
+ })
354
+ } else if (name === 'contentEnd') {
355
+ if (block.data.role === 'ASSISTANT' && data.stopReason === 'INTERRUPTED') {
356
+ block.control = block.interruptionTime !== undefined
357
+ turn.output.interrupt(block.interruptionTime ?? now)
358
+ }
359
+ this.#text(block)
360
+ if (block.data.type === 'AUDIO' && data.stopReason === 'END_TURN') turn.generationEnd = now
361
+ if (block.data.role === 'ASSISTANT' && block.data.type === 'TEXT' &&
362
+ !block.control && data.stopReason === 'END_TURN') turn.textEnd = now
363
+ this.#blocks.delete(key)
364
+ this.#completed.add(key)
365
+ if (this.#completed.size > MAX_BLOCKS) this.#completed.delete(this.#completed.values().next().value)
366
+ }
367
+ }
368
+
369
+ /** @param {{ control: boolean, data: object, stage?: string, text: string, turn: object }} block */
370
+ #text (block) {
371
+ if (block.control) return
372
+ const field = block.data.role === 'USER'
373
+ ? 'userText'
374
+ : block.stage === 'FINAL' ? 'finalText' : 'speculativeText'
375
+ block.turn[field] = (block.turn[field] + block.text).slice(0, MAX_TEXT)
376
+ }
377
+
378
+ /** @param {object} data */
379
+ #usage (data) {
380
+ for (const direction of ['input', 'output']) {
381
+ const key = `${direction}_tokens`
382
+ const total = data[direction === 'input' ? 'totalInputTokens' : 'totalOutputTokens']
383
+ if (!Number.isSafeInteger(total) || total < this.#totals[key]) continue
384
+ const delta = total - this.#totals[key]
385
+ this.#totals[key] = total
386
+ const target = !this.#current || (direction === 'input' && this.#hasPendingInput())
387
+ ? this.#pending
388
+ : this.#current
389
+ target.metrics[key] += delta
390
+ target.metrics.total_tokens += delta
391
+
392
+ // Speech usage is a subset of these totals, never another charge to total_tokens.
393
+ const audioKey = `${direction}_audio_tokens`
394
+ const audioTotal = data.details?.total?.[direction]?.speechTokens
395
+ const previous = this.#audioTotals[direction]
396
+ const valid = Number.isSafeInteger(audioTotal) && audioTotal >= 0 && audioTotal <= total &&
397
+ (previous === undefined || audioTotal >= previous)
398
+ const audioDelta = valid && previous !== undefined ? audioTotal - previous : undefined
399
+ this.#audioTotals[direction] = valid ? audioTotal : delta ? undefined : previous
400
+ if (audioDelta === undefined || audioDelta > delta) {
401
+ if (delta || audioDelta > delta) {
402
+ target.missingAudioMetrics.add(audioKey)
403
+ delete target.metrics[audioKey]
404
+ }
405
+ } else if (!target.missingAudioMetrics.has(audioKey)) {
406
+ target.metrics[audioKey] = (target.metrics[audioKey] ?? 0) + audioDelta
407
+ }
408
+ }
409
+ }
410
+
411
+ /**
412
+ * @param {Turn} turn
413
+ * @param {number} now
414
+ * @param {Error} [error]
415
+ */
416
+ #emitTurn (turn, now, error) {
417
+ if (turn.emitted) return
418
+ turn.emitted = true
419
+ this.#turnIndex++
420
+ try {
421
+ this.#snapshotInput(turn)
422
+ const start = turn.inputStart ?? turn.started ?? now
423
+ const llmStart = turn.inputEnd ?? turn.started ?? start
424
+ const completed = completionTime(turn)
425
+ const responseEnd = Math.max(llmStart, completed ?? turn.output.interrupted ?? now)
426
+ this.#emit({
427
+ turn,
428
+ error,
429
+ sessionId: this.#sessionId,
430
+ history: this.#history,
431
+ startTime: start,
432
+ finishTime: Math.max(responseEnd, turn.output.end ?? 0, turn.inputEnd ?? 0),
433
+ llmStart,
434
+ responseEnd,
435
+ metadata: {
436
+ turn_index: this.#turnIndex,
437
+ completion_id: turn.completionId,
438
+ speech_windows: turn.windows,
439
+ ttfa_boundary: 'speech_end_event_receipt',
440
+ output_timing: 'projected_playback',
441
+ interrupted: turn.output.interrupted !== undefined,
442
+ partial: completed === undefined,
443
+ transcript_stage: turn.finalText ? 'FINAL' : 'SPECULATIVE',
444
+ usage_attribution: 'event_receipt',
445
+ generated_output_bytes: turn.output.generatedBytes,
446
+ session_configuration: this.#configuration,
447
+ },
448
+ })
449
+ } catch {
450
+ log.debug('Cannot emit Nova Sonic turn')
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Input EOF is deliberately not a finish. Only output completion, cancellation or failure flushes.
456
+ * @param {Error} [error]
457
+ * @param {number} [now]
458
+ */
459
+ finish (error, now = Date.now()) {
460
+ if (this.#closed) return
461
+ this.#closed = true
462
+ for (const block of this.#blocks.values()) this.#text(block)
463
+ if (this.#current) {
464
+ // A failure before the next response starts belongs to its pending utterance,
465
+ // not to the response that has already completed.
466
+ const laterTurnFailed = completionTime(this.#current) !== undefined && this.#hasPendingInput()
467
+ this.#emitTurn(this.#current, now, laterTurnFailed ? undefined : error)
468
+ }
469
+ if (hasInput(this.#pending) || this.#pending.metrics.total_tokens || (error && !this.#current)) {
470
+ this.#emitTurn(this.#pending, now, error)
471
+ }
472
+ this.#current = undefined
473
+ this.#pending = new Turn()
474
+ this.#blocks.clear()
475
+ this.#outbound.clear()
476
+ this.#completed.clear()
477
+ this.#history = []
478
+ this.#audio.clear()
479
+ this.#emit = undefined
480
+ }
481
+ }
482
+
483
+ module.exports = SonicSession
@@ -10,6 +10,11 @@ const RealtimeSession = require('./session')
10
10
  /**
11
11
  * @typedef {import('./session').TurnDescriptor} TurnDescriptor
12
12
  * @typedef {import('./turns').ResponseTurn} ResponseTurn
13
+ * @typedef {{
14
+ * session: RealtimeSession,
15
+ * socket: object | undefined,
16
+ * finalize: (failed?: boolean) => void,
17
+ * }} Connection
13
18
  */
14
19
 
15
20
  // One span per phase of a turn. `user speech` and `agent speech` share a channel: they differ only
@@ -36,7 +41,7 @@ const audioChannel = dc.channel('dd-trace:openai:realtime:audio')
36
41
  const USER_SPEECH = { operation: 'createRealtimeUserSpeech', llmobsName: 'user speech' }
37
42
  const AGENT_SPEECH = { operation: 'createRealtimeAgentSpeech', llmobsName: 'agent speech' }
38
43
 
39
- /** @type {WeakMap<object, { session: RealtimeSession, finalize: () => void }>} */
44
+ /** @type {WeakMap<object, Connection>} */
40
45
  const connections = new WeakMap()
41
46
 
42
47
  /**
@@ -92,7 +97,7 @@ function captureContext (turn) {
92
97
  * Start tracking a connection, reading the model and base path off the URL the SDK already built.
93
98
  *
94
99
  * @param {object} emitter
95
- * @returns {{ session: RealtimeSession, finalize: () => void }}
100
+ * @returns {Connection}
96
101
  */
97
102
  function createConnection (emitter) {
98
103
  let model
@@ -135,7 +140,7 @@ function createConnection (emitter) {
135
140
  session.finishSession(Date.now(), failed)
136
141
  }
137
142
 
138
- const connection = { session, finalize }
143
+ const connection = { session, socket: emitter.socket, finalize }
139
144
  attachSocketClose(emitter, finalize)
140
145
  return connection
141
146
  }
@@ -181,14 +186,21 @@ function attachSocketClose (emitter, finalize) {
181
186
 
182
187
  /**
183
188
  * @param {object} emitter
184
- * @returns {{ session: RealtimeSession, finalize: () => void } | undefined}
189
+ * @returns {Connection | undefined}
185
190
  */
186
191
  function getConnection (emitter) {
187
192
  const connection = connections.get(emitter)
188
- if (connection !== undefined) return connection
193
+ if (connection !== undefined) {
194
+ if (connection.socket === emitter.socket) return connection
195
+
196
+ // Opt-in SDK recovery keeps the emitter but replaces its socket with a new server session.
197
+ // Retire the old state before attaching close and exit cleanup to the replacement.
198
+ connection.finalize(true)
199
+ connections.delete(emitter)
200
+ }
189
201
 
190
202
  // Nothing is listening, so never allocate. Checked only on the miss path, so an established
191
- // connection pays one WeakMap lookup per event.
203
+ // connection reuses its state while the socket is unchanged.
192
204
  if (!turnChannel.start.hasSubscribers) return
193
205
 
194
206
  const created = createConnection(emitter)
@@ -829,7 +829,7 @@ class RealtimeSession {
829
829
  input.audio.totalDecodedBytes, inputFormat.mimeType, inputFormat.sampleRate
830
830
  )
831
831
  const userEnd = input.speechEndTime ??
832
- (userStart !== undefined && inputDurationMs !== undefined ? userStart + inputDurationMs : undefined)
832
+ (userStart === undefined || inputDurationMs === undefined ? undefined : userStart + inputDurationMs)
833
833
 
834
834
  const agentStart = turn.audio.startTime
835
835
  const playbackMs = segmentDurationMs(
@@ -58,6 +58,13 @@ const V4_PACKAGE_SHIMS = [
58
58
  versions: ['>=4.87.0'],
59
59
  interceptChannel: responsesInterceptChannel,
60
60
  },
61
+ {
62
+ file: 'resources/decisions',
63
+ targetClass: 'Decisions',
64
+ baseResource: 'decisions',
65
+ methods: ['create'],
66
+ versions: ['>=7.30.0'],
67
+ },
61
68
  {
62
69
  file: 'resources/embeddings',
63
70
  targetClass: 'Embeddings',
@@ -10,10 +10,9 @@ const logSubmissionCh = channel('ci:log-submission:log')
10
10
 
11
11
  /**
12
12
  * @param {string} symbol
13
- * @param {(original: Function) => Function} wrapper
14
13
  * @param {Function} pino
15
14
  */
16
- function wrapPino (symbol, wrapper, pino) {
15
+ function wrapPino (symbol, pino) {
17
16
  /**
18
17
  * @param {unknown[]} args
19
18
  * @returns {unknown}
@@ -25,7 +24,7 @@ function wrapPino (symbol, wrapper, pino) {
25
24
  configurable: true,
26
25
  enumerable: true,
27
26
  writable: true,
28
- value: wrapper(instance[symbol]),
27
+ value: wrapAsJson(instance[symbol]),
29
28
  })
30
29
 
31
30
  return instance
@@ -83,19 +82,19 @@ function wrapPrettyFactory (prettyFactory) {
83
82
  addHook({ name: 'pino', versions: ['2 - 3', '4'], patchDefault: true }, (pino) => {
84
83
  const asJsonSym = (pino.symbols && pino.symbols.asJsonSym) || 'asJson'
85
84
 
86
- return shimmer.wrapFunction(pino, pino => wrapPino(asJsonSym, wrapAsJson, pino))
85
+ return shimmer.wrapFunction(pino, pino => wrapPino(asJsonSym, pino))
87
86
  })
88
87
 
89
88
  addHook({ name: 'pino', versions: ['>=5 <6.8.0'], patchDefault: true }, (pino) => {
90
89
  const asJsonSym = ((pino.default || pino)?.symbols.asJsonSym) || 'asJson'
91
90
 
92
- return shimmer.wrapFunction(pino, pino => wrapPino(asJsonSym, wrapAsJson, pino.default || pino))
91
+ return shimmer.wrapFunction(pino, pino => wrapPino(asJsonSym, pino.default || pino))
93
92
  })
94
93
 
95
94
  addHook({ name: 'pino', versions: ['>=6.8.0'], patchDefault: false }, (pino) => {
96
95
  const asJsonSym = pino.symbols.asJsonSym
97
96
 
98
- const wrapped = shimmer.wrapFunction(pino, pino => wrapPino(asJsonSym, wrapAsJson, pino))
97
+ const wrapped = shimmer.wrapFunction(pino, pino => wrapPino(asJsonSym, pino))
99
98
  wrapped.pino = wrapped
100
99
  wrapped.default = wrapped
101
100
 
@@ -2657,11 +2657,14 @@ function instrumentWorkerMainMethods (workerMain) {
2657
2657
  }
2658
2658
 
2659
2659
  // In cases where there is no afterEach hook with _ddHook, we need to add one
2660
+ // Playwright 1.64 builds the hook step stack from hook.location, so an undefined
2661
+ // location ends up as [undefined] and breaks trace writing
2660
2662
  if (!existAfterEachHook) {
2661
2663
  test.parent._hooks.push({
2662
2664
  type: 'afterEach',
2663
2665
  fn: cleanupRumPages,
2664
2666
  title: 'afterEach hook',
2667
+ location: test.location,
2665
2668
  _ddHook: true,
2666
2669
  })
2667
2670
  }