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
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { clearTimeout, setTimeout } = require('node:timers')
4
+
3
5
  const {
4
6
  createWebdriverioWorkerMessage,
5
7
  WEBDRIVERIO_WORKER_ENV,
@@ -11,6 +13,9 @@ const { FINAL_FLUSH_TIMEOUT } = require('../../../dd-trace/src/ci-visibility/fin
11
13
  const SCREENSHOT_UPLOAD = 'dd:test-optimization:webdriverio:screenshot:upload'
12
14
  const SCREENSHOT_UPLOAD_RESPONSE = 'dd:test-optimization:webdriverio:screenshot:upload:response'
13
15
  const SCREENSHOT_UPLOAD_TIMEOUT_MS = FINAL_FLUSH_TIMEOUT + 5000
16
+ const VIDEO_UPLOAD = 'dd:test-optimization:webdriverio:video:upload'
17
+ const VIDEO_UPLOAD_RESPONSE = 'dd:test-optimization:webdriverio:video:upload:response'
18
+ const VIDEO_UPLOAD_TIMEOUT_MS = 5 * FINAL_FLUSH_TIMEOUT + 5000
14
19
 
15
20
  /**
16
21
  * Sends a message over WebdriverIO's worker IPC envelope.
@@ -34,95 +39,113 @@ function sendWebdriverioWorkerMessage (message, onError, onDone) {
34
39
  })
35
40
  }
36
41
 
37
- let screenshotUploadRequestId = 0
38
- const screenshotUploadRequests = new Map()
42
+ let mediaUploadRequestId = 0
43
+ const mediaUploadRequests = new Map()
39
44
 
40
45
  /**
41
- * Removes shared screenshot response listeners when there are no pending requests.
46
+ * Removes shared media response listeners when there are no pending requests.
42
47
  *
43
48
  */
44
- function removeScreenshotUploadListeners () {
45
- if (screenshotUploadRequests.size !== 0) return
49
+ function removeMediaUploadListeners () {
50
+ if (mediaUploadRequests.size !== 0) return
46
51
 
47
- process.off('message', onScreenshotUploadResponse)
48
- process.off('disconnect', onScreenshotUploadDisconnect)
52
+ process.off('message', onMediaUploadResponse)
53
+ process.off('disconnect', onMediaUploadDisconnect)
49
54
  }
50
55
 
51
56
  /**
52
- * Completes one pending screenshot upload request.
57
+ * Completes one pending media upload request.
53
58
  *
54
59
  * @param {string} requestId
55
60
  * @param {Error} [error]
56
61
  */
57
- function finishScreenshotUploadRequest (requestId, error) {
58
- const request = screenshotUploadRequests.get(requestId)
62
+ function finishMediaUploadRequest (requestId, error) {
63
+ const request = mediaUploadRequests.get(requestId)
59
64
  if (!request) return
60
65
 
61
- screenshotUploadRequests.delete(requestId)
66
+ mediaUploadRequests.delete(requestId)
62
67
  clearTimeout(request.timeout)
63
- removeScreenshotUploadListeners()
68
+ removeMediaUploadListeners()
64
69
  request.onDone(error)
65
70
  }
66
71
 
67
72
  /**
68
- * Dispatches one coordinator screenshot response to its pending request.
73
+ * Dispatches one coordinator media response to its pending request.
69
74
  *
70
75
  * @param {object} message
71
76
  */
72
- function onScreenshotUploadResponse (message) {
73
- if (message?.name !== SCREENSHOT_UPLOAD_RESPONSE) return
77
+ function onMediaUploadResponse (message) {
78
+ if (message?.name !== SCREENSHOT_UPLOAD_RESPONSE && message?.name !== VIDEO_UPLOAD_RESPONSE) return
74
79
 
75
80
  const { error: errorMessage, requestId } = message.content || {}
76
81
  if (!requestId) return
77
82
 
78
- finishScreenshotUploadRequest(requestId, errorMessage ? new Error(errorMessage) : undefined)
83
+ finishMediaUploadRequest(requestId, errorMessage ? new Error(errorMessage) : undefined)
79
84
  }
80
85
 
81
86
  /**
82
- * Fails every pending screenshot upload after coordinator disconnect.
87
+ * Fails every pending media upload after coordinator disconnect.
83
88
  *
84
89
  */
85
- function onScreenshotUploadDisconnect () {
86
- for (const requestId of screenshotUploadRequests.keys()) {
87
- finishScreenshotUploadRequest(
90
+ function onMediaUploadDisconnect () {
91
+ for (const requestId of mediaUploadRequests.keys()) {
92
+ finishMediaUploadRequest(
88
93
  requestId,
89
- new Error('WebdriverIO coordinator disconnected during screenshot upload')
94
+ new Error('WebdriverIO coordinator disconnected during media upload')
90
95
  )
91
96
  }
92
97
  }
93
98
 
94
99
  /**
95
- * Requests one screenshot upload from the WebdriverIO coordinator.
100
+ * Requests one media upload from the WebdriverIO coordinator.
96
101
  *
97
- * @param {object} content - Screenshot upload metadata
102
+ * @param {object} content - Upload metadata
103
+ * @param {'screenshot'|'video'} kind
98
104
  * @param {(error?: Error) => void} onDone - Upload completion callback
99
105
  */
100
- function requestWebdriverioScreenshotUpload (content, onDone) {
101
- const requestId = `${process.pid}-${++screenshotUploadRequestId}`
106
+ function requestWebdriverioMediaUpload (content, kind, onDone) {
107
+ const requestId = `${process.pid}-${++mediaUploadRequestId}`
102
108
  const timeout = setTimeout(() => {
103
- finishScreenshotUploadRequest(requestId, new Error('WebdriverIO screenshot upload timed out'))
104
- }, SCREENSHOT_UPLOAD_TIMEOUT_MS)
109
+ finishMediaUploadRequest(requestId, new Error(`WebdriverIO ${kind} upload timed out`))
110
+ }, kind === 'video' ? VIDEO_UPLOAD_TIMEOUT_MS : SCREENSHOT_UPLOAD_TIMEOUT_MS)
105
111
  timeout.unref?.()
106
- if (screenshotUploadRequests.size === 0) {
107
- process.on('message', onScreenshotUploadResponse)
108
- process.once('disconnect', onScreenshotUploadDisconnect)
112
+ if (mediaUploadRequests.size === 0) {
113
+ process.on('message', onMediaUploadResponse)
114
+ process.once('disconnect', onMediaUploadDisconnect)
109
115
  }
110
- screenshotUploadRequests.set(requestId, { onDone, timeout })
116
+ mediaUploadRequests.set(requestId, { onDone, timeout })
111
117
  sendWebdriverioWorkerMessage({
112
118
  origin: 'datadog',
113
- name: SCREENSHOT_UPLOAD,
119
+ name: kind === 'video' ? VIDEO_UPLOAD : SCREENSHOT_UPLOAD,
114
120
  content: { ...content, requestId },
115
- }, error => finishScreenshotUploadRequest(
121
+ }, error => finishMediaUploadRequest(
116
122
  requestId,
117
- error || new Error('WebdriverIO screenshot upload IPC failed')
123
+ error || new Error(`WebdriverIO ${kind} upload IPC failed`)
118
124
  ))
119
125
  }
120
126
 
127
+ /**
128
+ * @param {object} content
129
+ * @param {(error?: Error) => void} onDone
130
+ */
131
+ function requestWebdriverioScreenshotUpload (content, onDone) {
132
+ requestWebdriverioMediaUpload(content, 'screenshot', onDone)
133
+ }
134
+
135
+ /**
136
+ * @param {object} content
137
+ * @param {(error?: Error) => void} onDone
138
+ */
139
+ function requestWebdriverioVideoUpload (content, onDone) {
140
+ requestWebdriverioMediaUpload(content, 'video', onDone)
141
+ }
142
+
121
143
  module.exports = {
122
144
  CONFIGURATION_REQUEST: 'dd:test-optimization:webdriverio:configuration:request',
123
145
  CONFIGURATION_RESPONSE: 'dd:test-optimization:webdriverio:configuration:response',
124
146
  createWebdriverioWorkerMessage,
125
147
  requestWebdriverioScreenshotUpload,
148
+ requestWebdriverioVideoUpload,
126
149
  SCREENSHOT_UPLOAD,
127
150
  SCREENSHOT_UPLOAD_RESPONSE,
128
151
  SCREENSHOT_UPLOAD_TIMEOUT_MS,
@@ -130,6 +153,10 @@ module.exports = {
130
153
  SUITE_FINISH: 'dd:test-optimization:webdriverio:test-suite:finish',
131
154
  WORKER_READY: 'dd:test-optimization:webdriverio:worker:ready',
132
155
  WORKER_READY_RESPONSE: 'dd:test-optimization:webdriverio:worker:ready:response',
156
+ VIDEO_UPLOAD_FLUSH: 'dd:test-optimization:webdriverio:video:flush',
157
+ VIDEO_UPLOAD,
158
+ VIDEO_UPLOAD_RESPONSE,
159
+ VIDEO_UPLOAD_TIMEOUT_MS,
133
160
  WEBDRIVERIO_WORKER_ENV,
134
161
  WEBDRIVERIO_WORKER_EVENT,
135
162
  WEBDRIVERIO_WORKER_ORIGIN,
@@ -416,7 +416,7 @@ function wrapMochaRun (Mocha, frameworkVersion) {
416
416
  })
417
417
  }
418
418
  filterSkippedFiles(runner, skippedFiles)
419
- if (isFailedTestReplayEnabled() || config.isFlakyTestRetriesEnabled) {
419
+ if (isFailedTestReplayEnabled() || config.isFlakyTestRetriesEnabled || config.isTestFailureVideosEnabled) {
420
420
  patchFailedTestReplayHookUp(runner.constructor)
421
421
  }
422
422
  configurationReady = true
@@ -443,8 +443,7 @@ addHook({
443
443
  filePattern: String.raw`lib/runner\.(?:c?js)$`,
444
444
  }, function (runnerPackage) {
445
445
  const Runner = runnerPackage.Runner ?? runnerPackage.default ?? runnerPackage
446
- if (typeof Runner !== 'function') return
447
- if (patchedRunners.has(Runner)) return
446
+ if (typeof Runner !== 'function' || patchedRunners.has(Runner)) return
448
447
 
449
448
  patchedRunners.add(Runner)
450
449
  shimmer.wrap(Runner.prototype, 'runTests', runTests => getRunTestsWrapper(runTests, config))
@@ -453,7 +452,7 @@ addHook({
453
452
  if (!workerFinishCh.hasSubscribers) {
454
453
  return run.apply(this, args)
455
454
  }
456
- if (isFailedTestReplayEnabled() || config.isFlakyTestRetriesEnabled) {
455
+ if (isFailedTestReplayEnabled() || config.isFlakyTestRetriesEnabled || config.isTestFailureVideosEnabled) {
457
456
  patchFailedTestReplayHookUp(Runner)
458
457
  }
459
458
  if (isWebdriverioWorker) {
@@ -469,7 +468,10 @@ addHook({
469
468
  const onRunDone = args[0]
470
469
  if (isWebdriverioWorker && typeof onRunDone === 'function') {
471
470
  args[0] = (...onRunDoneArgs) => {
472
- finishWebdriverioWorker(this, () => onRunDone(...onRunDoneArgs))
471
+ const adjustedFailures = Number.isFinite(this.failures)
472
+ ? Math.max(0, this.failures)
473
+ : onRunDoneArgs[0]
474
+ finishWebdriverioWorker(this, () => onRunDone(adjustedFailures, ...onRunDoneArgs.slice(1)))
473
475
  }
474
476
  } else {
475
477
  // Flush after the worker finishes its Mocha run, including grouped spec files.
@@ -45,7 +45,7 @@ addHook({ name: 'mongodb', versions: ['>=4.6.0 <6.4.0'], file: 'lib/cmap/connect
45
45
 
46
46
  addHook({ name: 'mongodb', versions: ['>=6.4.0'], file: 'lib/cmap/connection.js' }, Connection => {
47
47
  const proto = Connection.Connection.prototype
48
- shimmer.wrap(proto, 'command', command => wrapConnectionCommand(command, 'command', undefined, instrumentPromise))
48
+ shimmer.wrap(proto, 'command', command => wrapConnectionCommand(command, 'command', instrumentPromise))
49
49
  })
50
50
 
51
51
  addHook({ name: 'mongodb', versions: ['>=3.3 <4'], file: 'lib/core/wireprotocol/index.js' }, wp => wrapWp(wp))
@@ -95,8 +95,8 @@ function wrapUnifiedCommand (command, operation, name) {
95
95
  }
96
96
  }
97
97
 
98
- function wrapConnectionCommand (command, operation, name, instrumentFn = instrument) {
99
- const opts = { name }
98
+ function wrapConnectionCommand (command, operation, instrumentFn = instrument) {
99
+ const opts = { name: undefined }
100
100
  return function (ns, ops) {
101
101
  if (!startCh.hasSubscribers) {
102
102
  return command.apply(this, arguments)
@@ -83,7 +83,7 @@ addHook({
83
83
 
84
84
  let callbackWrapped = false
85
85
 
86
- const wrapCallbackIfExist = (args, ctx) => {
86
+ const wrapCallbackIfExist = (args) => {
87
87
  const lastArgumentIndex = args.length - 1
88
88
 
89
89
  if (typeof args[lastArgumentIndex] === 'function') {
@@ -106,7 +106,7 @@ addHook({
106
106
  }
107
107
 
108
108
  return startCh.runStores(ctx, () => {
109
- wrapCallbackIfExist(args, ctx)
109
+ wrapCallbackIfExist(args)
110
110
 
111
111
  // A callback query executes synchronously inside `method.apply`, so the
112
112
  // exec scope (and the deferred driver call it spawns) must be entered
@@ -121,7 +121,7 @@ addHook({
121
121
  shimmer.wrap(res, 'exec', originalExec => {
122
122
  return function wrappedExec (...args) {
123
123
  if (!callbackWrapped) {
124
- wrapCallbackIfExist(args, ctx)
124
+ wrapCallbackIfExist(args)
125
125
  }
126
126
 
127
127
  const execResult = execCh.runStores(ctx, () => originalExec.apply(this, args))
@@ -0,0 +1,173 @@
1
+ 'use strict'
2
+
3
+ const { LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES } = require('../../../dd-trace/src/llmobs/constants/audio')
4
+
5
+ // Leave room for the WAV header when the retained PCM is encoded.
6
+ const MAX_AUDIO = LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES - 128
7
+ const RATES = new Set([8000, 16_000, 24_000, 48_000])
8
+ const INVALID_BASE64 = /[^A-Za-z0-9+/]/
9
+
10
+ /** @param {object} configuration */
11
+ function pcmRate (configuration = {}) {
12
+ return configuration.mediaType === 'audio/lpcm' && configuration.sampleSizeBits === 16 &&
13
+ configuration.channelCount === 1 && configuration.encoding === 'base64' &&
14
+ RATES.has(configuration.sampleRateHertz)
15
+ ? configuration.sampleRateHertz
16
+ : 0
17
+ }
18
+
19
+ /** @param {string} encoded */
20
+ function decodedLength (encoded) {
21
+ if (typeof encoded !== 'string' || encoded.length % 4) return
22
+ const padding = encoded.endsWith('==') ? 2 : encoded.endsWith('=') ? 1 : 0
23
+ if (INVALID_BASE64.test(encoded.slice(0, encoded.length - padding))) return
24
+ return encoded.length / 4 * 3 - padding
25
+ }
26
+
27
+ /** A bounded, connection-wide sample timeline. Content-name changes do not reset its origin. */
28
+ class InputAudio {
29
+ rate = 0
30
+ valid = true
31
+ total = 0
32
+ anchor
33
+ #chunks = []
34
+ #bytes = 0
35
+
36
+ /** @param {object} configuration */
37
+ configure (configuration) {
38
+ const rate = pcmRate(configuration)
39
+ if (!rate || (this.total && rate !== this.rate)) this.valid = false
40
+ this.rate = rate
41
+ }
42
+
43
+ /**
44
+ * @param {string} encoded
45
+ * @param {number} now Milliseconds at SDK consumption, not an acknowledgement from the provider.
46
+ */
47
+ append (encoded, now) {
48
+ const length = decodedLength(encoded)
49
+ if (length === undefined) {
50
+ this.valid = false
51
+ this.clear()
52
+ return
53
+ }
54
+ if (length % 2) this.valid = false
55
+ this.total += length
56
+ if (this.rate) this.anchor = now - this.total * 1000 / (2 * this.rate)
57
+ if (!this.valid || !length) return
58
+ // Decode from a base64 quartet boundary, retaining at most two alignment bytes
59
+ // beyond the cap instead of allocating the entire incoming frame.
60
+ const start = Math.max(0, length - MAX_AUDIO)
61
+ const retained = Buffer.from(encoded.slice(Math.floor(start / 3) * 4), 'base64').subarray(start % 3)
62
+ this.#chunks.push(retained)
63
+ this.#bytes += retained.length
64
+ while (this.#bytes > MAX_AUDIO) {
65
+ const first = this.#chunks[0]
66
+ const excess = this.#bytes - MAX_AUDIO
67
+ if (first.length <= excess) {
68
+ this.#chunks.shift()
69
+ this.#bytes -= first.length
70
+ } else {
71
+ this.#chunks[0] = Buffer.from(first.subarray(excess))
72
+ this.#bytes -= excess
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @param {number} startMs
79
+ * @param {number} endMs
80
+ */
81
+ clip (startMs, endMs) {
82
+ if (!this.valid || !this.rate || !Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) return
83
+ const start = Math.floor(startMs * this.rate / 1000) * 2
84
+ const end = Math.floor(endMs * this.rate / 1000) * 2
85
+ const base = this.total - this.#bytes
86
+ if (start < base || end > this.total) return
87
+ const clip = Buffer.allocUnsafe(end - start)
88
+ let offset = base
89
+ let written = 0
90
+ for (const chunk of this.#chunks) {
91
+ const chunkEnd = offset + chunk.length
92
+ if (chunkEnd > start) {
93
+ written += chunk.copy(clip, written, Math.max(start - offset, 0), Math.min(end - offset, chunk.length))
94
+ }
95
+ offset = chunkEnd
96
+ if (offset >= end) break
97
+ }
98
+ return clip
99
+ }
100
+
101
+ clear () {
102
+ this.#chunks = []
103
+ this.#bytes = 0
104
+ }
105
+ }
106
+
107
+ /** Ordered PCM playback projection for one response, including genuine underrun silence. */
108
+ class OutputAudio {
109
+ rate = 0
110
+ start
111
+ end
112
+ interrupted
113
+ generatedBytes = 0
114
+ timingValid = true
115
+ omittedReason
116
+ #chunks = []
117
+ #bytes = 0
118
+
119
+ /**
120
+ * @param {string} encoded
121
+ * @param {object} configuration
122
+ * @param {number} now
123
+ */
124
+ append (encoded, configuration, now) {
125
+ const length = decodedLength(encoded)
126
+ if (length !== undefined) this.generatedBytes += length
127
+ // Late chunks cannot resurrect playback or invalidate the clip already cut at interruption.
128
+ if (this.interrupted !== undefined) return
129
+ const rate = pcmRate(configuration)
130
+ if (length === undefined || !rate || length % 2 || (this.rate && this.rate !== rate)) {
131
+ this.timingValid = false
132
+ this.omittedReason = 'invalid_audio'
133
+ this.#chunks = []
134
+ this.#bytes = 0
135
+ return
136
+ }
137
+ if (!length) return
138
+ this.rate ||= rate
139
+ this.start ??= now
140
+ const previousEnd = this.end ?? now
141
+ const gap = Math.floor(Math.max(0, now - previousEnd) * rate / 1000) * 2
142
+ this.end = previousEnd + (gap + length) * 1000 / (2 * rate)
143
+ if (!this.omittedReason && this.#bytes + gap + length <= MAX_AUDIO) {
144
+ if (gap) this.#chunks.push(Buffer.alloc(gap))
145
+ this.#chunks.push(Buffer.from(encoded, 'base64'))
146
+ this.#bytes += gap + length
147
+ } else {
148
+ this.omittedReason ||= 'retention_limit'
149
+ this.#chunks = []
150
+ this.#bytes = 0
151
+ }
152
+ }
153
+
154
+ /** @param {number} now */
155
+ interrupt (now) {
156
+ if (this.interrupted !== undefined) return
157
+ this.interrupted = now
158
+ if (this.start === undefined || this.end === undefined || !this.rate) return
159
+ const frames = Math.floor(Math.max(0, now - this.start) * this.rate / 1000)
160
+ this.end = Math.min(this.end, this.start + frames * 1000 / this.rate)
161
+ if (this.#bytes > frames * 2) {
162
+ const clip = Buffer.concat(this.#chunks, this.#bytes).subarray(0, frames * 2)
163
+ this.#chunks = [Buffer.from(clip)]
164
+ this.#bytes = clip.length
165
+ }
166
+ }
167
+
168
+ pcm () {
169
+ if (!this.omittedReason && this.#bytes) return Buffer.concat(this.#chunks, this.#bytes)
170
+ }
171
+ }
172
+
173
+ module.exports = { InputAudio, OutputAudio }
@@ -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