dd-trace 6.13.0 → 6.15.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 (208) hide show
  1. package/LICENSE-3rdparty.csv +3 -1
  2. package/ci/init.js +8 -3
  3. package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
  4. package/ci/test-optimization-validation/ci-remediation.js +6 -6
  5. package/ci/test-optimization-validation/command-blocker.js +7 -5
  6. package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
  7. package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
  8. package/ci/test-optimization-validation/package-check.js +1 -1
  9. package/ci/test-optimization-validation/report-writer.js +7 -5
  10. package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
  11. package/ci/vitest-no-worker-init-setup.mjs +13 -13
  12. package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
  13. package/index.d.ts +8 -0
  14. package/package.json +13 -10
  15. package/packages/datadog-esbuild/index.js +3 -1
  16. package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
  17. package/packages/datadog-instrumentations/src/fs.js +24 -20
  18. package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
  19. package/packages/datadog-instrumentations/src/graphql.js +16 -16
  20. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
  21. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  22. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +265 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +14 -5
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +251 -0
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +11 -1
  33. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
  34. package/packages/datadog-instrumentations/src/jest.js +166 -45
  35. package/packages/datadog-instrumentations/src/knex.js +76 -0
  36. package/packages/datadog-instrumentations/src/mariadb.js +6 -0
  37. package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
  38. package/packages/datadog-instrumentations/src/mocha/main.js +66 -20
  39. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  40. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
  41. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
  42. package/packages/datadog-instrumentations/src/net.js +14 -17
  43. package/packages/datadog-instrumentations/src/next.js +212 -62
  44. package/packages/datadog-instrumentations/src/openai.js +42 -40
  45. package/packages/datadog-instrumentations/src/pg.js +15 -0
  46. package/packages/datadog-instrumentations/src/playwright.js +113 -13
  47. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
  48. package/packages/datadog-instrumentations/src/sequelize.js +94 -2
  49. package/packages/datadog-instrumentations/src/undici.js +53 -4
  50. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +37 -3
  51. package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
  52. package/packages/datadog-instrumentations/src/vitest-worker.js +71 -14
  53. package/packages/datadog-instrumentations/src/webdriverio.js +965 -6
  54. package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
  55. package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
  56. package/packages/datadog-plugin-amqp10/src/util.js +2 -1
  57. package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
  58. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
  59. package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
  60. package/packages/datadog-plugin-cucumber/src/index.js +0 -2
  61. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
  62. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
  63. package/packages/datadog-plugin-graphql/src/execute.js +998 -196
  64. package/packages/datadog-plugin-graphql/src/index.js +27 -7
  65. package/packages/datadog-plugin-graphql/src/jit.js +136 -0
  66. package/packages/datadog-plugin-graphql/src/request.js +18 -27
  67. package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
  68. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
  69. package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
  70. package/packages/datadog-plugin-graphql/src/utils.js +208 -120
  71. package/packages/datadog-plugin-graphql/src/validate.js +10 -23
  72. package/packages/datadog-plugin-jest/src/index.js +0 -6
  73. package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
  74. package/packages/datadog-plugin-mocha/src/index.js +403 -75
  75. package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
  76. package/packages/datadog-plugin-mysql/src/index.js +41 -15
  77. package/packages/datadog-plugin-net/src/tcp.js +3 -1
  78. package/packages/datadog-plugin-next/src/index.js +63 -29
  79. package/packages/datadog-plugin-playwright/src/index.js +167 -27
  80. package/packages/datadog-plugin-undici/src/index.js +27 -41
  81. package/packages/datadog-plugin-vitest/src/index.js +6 -12
  82. package/packages/dd-trace/src/aiguard/evaluation.js +2 -1
  83. package/packages/dd-trace/src/aiguard/messages/anthropic.js +8 -1
  84. package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
  85. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
  86. package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
  87. package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
  88. package/packages/dd-trace/src/appsec/graphql.js +6 -2
  89. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  90. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
  91. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
  92. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +3 -1
  93. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +531 -95
  94. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
  95. package/packages/dd-trace/src/appsec/lambda.js +44 -26
  96. package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
  97. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
  100. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +5 -173
  101. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
  102. package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
  103. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
  104. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +394 -225
  105. package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
  106. package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
  107. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
  108. package/packages/dd-trace/src/ci-visibility/final-flush.js +2 -1
  109. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
  110. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
  111. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -0
  112. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
  113. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +180 -0
  114. package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
  115. package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
  116. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  117. package/packages/dd-trace/src/config/index.js +73 -13
  118. package/packages/dd-trace/src/config/parsers.js +11 -1
  119. package/packages/dd-trace/src/config/stable.js +3 -3
  120. package/packages/dd-trace/src/config/supported-configurations.json +22 -2
  121. package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
  122. package/packages/dd-trace/src/debugger/config.js +12 -1
  123. package/packages/dd-trace/src/debugger/constants.js +1 -0
  124. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
  125. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +28 -0
  126. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -40
  127. package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
  128. package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
  129. package/packages/dd-trace/src/debugger/index.js +23 -3
  130. package/packages/dd-trace/src/dogstatsd.js +510 -76
  131. package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
  132. package/packages/dd-trace/src/encode/0.4.js +5 -5
  133. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
  134. package/packages/dd-trace/src/evp_proxy/direct.js +3 -25
  135. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  136. package/packages/dd-trace/src/exporters/agent/writer.js +24 -7
  137. package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
  138. package/packages/dd-trace/src/exporters/agentless/writer.js +103 -119
  139. package/packages/dd-trace/src/exporters/common/agents.js +28 -6
  140. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
  141. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
  142. package/packages/dd-trace/src/exporters/common/proxy.js +52 -0
  143. package/packages/dd-trace/src/exporters/common/request.js +42 -14
  144. package/packages/dd-trace/src/exporters/common/url.js +40 -1
  145. package/packages/dd-trace/src/exporters/common/writer.js +9 -7
  146. package/packages/dd-trace/src/flush.js +6 -3
  147. package/packages/dd-trace/src/id.js +14 -0
  148. package/packages/dd-trace/src/index.js +5 -2
  149. package/packages/dd-trace/src/llmobs/experiments/dataset.js +2 -1
  150. package/packages/dd-trace/src/llmobs/index.js +32 -32
  151. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
  152. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
  153. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
  154. package/packages/dd-trace/src/llmobs/util.js +0 -33
  155. package/packages/dd-trace/src/llmobs/writers/base.js +49 -32
  156. package/packages/dd-trace/src/log/index.js +7 -1
  157. package/packages/dd-trace/src/{ci-visibility/log-submission → log-submission}/log-submission-plugin.js +48 -19
  158. package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
  159. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  160. package/packages/dd-trace/src/opentelemetry/logs/index.js +5 -24
  161. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
  162. package/packages/dd-trace/src/opentelemetry/metrics/index.js +15 -22
  163. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +14 -4
  164. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +10 -0
  165. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +5 -0
  166. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
  167. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
  168. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  169. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
  170. package/packages/dd-trace/src/opentracing/propagation/text_map.js +101 -20
  171. package/packages/dd-trace/src/payload-tagging/index.js +2 -1
  172. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
  173. package/packages/dd-trace/src/plugins/tracing.js +12 -10
  174. package/packages/dd-trace/src/plugins/util/ci.js +3 -1
  175. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
  176. package/packages/dd-trace/src/plugins/util/test.js +83 -20
  177. package/packages/dd-trace/src/plugins/util/url.js +2 -2
  178. package/packages/dd-trace/src/plugins/util/web.js +4 -9
  179. package/packages/dd-trace/src/priority_sampler.js +4 -0
  180. package/packages/dd-trace/src/profiling/config.js +1 -0
  181. package/packages/dd-trace/src/profiling/oom.js +7 -2
  182. package/packages/dd-trace/src/profiling/profiler.js +23 -13
  183. package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
  184. package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
  185. package/packages/dd-trace/src/proxy.js +75 -5
  186. package/packages/dd-trace/src/remote_config/index.js +319 -101
  187. package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
  188. package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
  189. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -1
  190. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -1
  191. package/packages/dd-trace/src/sampling_rule.js +39 -10
  192. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +4 -8
  193. package/packages/dd-trace/src/serverless/vercel.js +53 -20
  194. package/packages/dd-trace/src/serverless.js +10 -3
  195. package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
  196. package/packages/dd-trace/src/span_processor.js +18 -3
  197. package/packages/dd-trace/src/span_stats.js +24 -28
  198. package/packages/dd-trace/src/startup-log.js +3 -2
  199. package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
  200. package/packages/dd-trace/src/telemetry/send-data.js +23 -20
  201. package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
  202. package/packages/dd-trace/src/tracer.js +4 -5
  203. package/packages/dd-trace/src/tracer_metadata.js +11 -3
  204. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  205. package/vendor/dist/protobufjs/index.js +1 -1
  206. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
  208. package/packages/dd-trace/src/encode/agentless-json.js +0 -210
@@ -1,21 +1,28 @@
1
1
  'use strict'
2
2
 
3
3
  const { performance } = require('node:perf_hooks')
4
- const dateNow = Date.now
5
4
  const { fileURLToPath } = require('node:url')
6
5
 
7
6
  const { channel } = require('dc-polyfill')
8
7
 
9
8
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
10
9
  const { storage } = require('../../datadog-core')
10
+ const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
11
11
  const {
12
12
  getEfdRetryCountForDuration,
13
13
  hasEfdRetries,
14
14
  } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
15
+ const {
16
+ SCREENSHOT_UPLOAD_RESULT_ERROR,
17
+ SCREENSHOT_UPLOAD_RESULT_UPLOADED,
18
+ setScreenshotUploadTags,
19
+ } = require('../../dd-trace/src/ci-visibility/test-screenshot')
15
20
  const log = require('../../dd-trace/src/log')
16
21
  const {
22
+ requestWebdriverioScreenshotUpload,
17
23
  sendWebdriverioWorkerMessage,
18
24
  SUITE_FINISH,
25
+ WEBDRIVERIO_WORKER_ENV,
19
26
  } = require('../../datadog-instrumentations/src/mocha/webdriverio-protocol')
20
27
 
21
28
  const {
@@ -54,6 +61,10 @@ const {
54
61
  getFailedTestReplayPromise,
55
62
  getTestSuiteExecutionKey,
56
63
  isModifiedTest,
64
+ setRumTestCorrelation,
65
+ TEST_BROWSER_NAME,
66
+ TEST_BROWSER_VERSION,
67
+ TEST_IS_RUM_ACTIVE,
57
68
  } = require('../../dd-trace/src/plugins/util/test')
58
69
  const { COMPONENT } = require('../../dd-trace/src/constants')
59
70
  const {
@@ -83,11 +94,16 @@ const jasmineSpecExecuteStartCh = 'tracing:orchestrion:jasmine-core:Spec_execute
83
94
  const jasmineTestFunctionStartCh = 'tracing:orchestrion:@wdio/utils:testFrameworkFnWrapper:start'
84
95
  const testFinishCh = channel('ci:mocha:test:finish')
85
96
  const testRetryCh = channel('ci:mocha:test:retry')
97
+ const WEBDRIVERIO_FRAMEWORK = 'webdriverio'
86
98
  const WEBDRIVERIO_JASMINE_ADAPTER = 'jasmine'
99
+ const WEBDRIVERIO_SCREENSHOT_CAPTURE_TIMEOUT_MS = 30_000
87
100
  const workerFinishCh = channel('ci:mocha:worker:finish')
88
101
  const WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT = Symbol('webdriverioJasmineFailedExpectationCount')
89
102
  const WEBDRIVERIO_JASMINE_FUNCTION_TYPE = Symbol('webdriverioJasmineFunctionType')
90
103
  const WEBDRIVERIO_JASMINE_TEST = Symbol('webdriverioJasmineTest')
104
+ const isWebdriverioWorker = !!getEnvironmentVariable(WEBDRIVERIO_WORKER_ENV)
105
+ const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
106
+ const BASE64_RE = /^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/
91
107
 
92
108
  /**
93
109
  * @typedef {object} WebdriverioJasmineResult
@@ -140,6 +156,63 @@ function getJasmineError (result) {
140
156
  return error
141
157
  }
142
158
 
159
+ /**
160
+ * Applies WebdriverIO RUM correlation without replacing active-browser metadata with an inactive browser.
161
+ *
162
+ * @param {object} context
163
+ * @param {object|undefined} activeSpan
164
+ * @returns {object|undefined}
165
+ */
166
+ function setWebdriverioRumTestCorrelation (context, activeSpan) {
167
+ const browserVersion = context.browserVersion
168
+ const correlationContext = {
169
+ isRumActive: context.isRumActive,
170
+ testExecutionId: context.testExecutionId,
171
+ }
172
+ const testSpan = setRumTestCorrelation(correlationContext, activeSpan)
173
+ context.testExecutionId = correlationContext.testExecutionId
174
+ if (!testSpan) return
175
+
176
+ const hasActiveRum = testSpan.context().getTag(TEST_IS_RUM_ACTIVE)
177
+ if (!context.isRumActive && hasActiveRum) return testSpan
178
+ if (context.browserName) testSpan.setTag(TEST_BROWSER_NAME, context.browserName)
179
+ if (browserVersion) testSpan.setTag(TEST_BROWSER_VERSION, browserVersion)
180
+ return testSpan
181
+ }
182
+
183
+ /**
184
+ * Converts a screenshot capture failure into its upload result.
185
+ *
186
+ * @param {unknown} error
187
+ * @returns {string}
188
+ */
189
+ function handleWebdriverioScreenshotError (error) {
190
+ log.error('Error capturing WebdriverIO failure screenshot: %s', error?.message || String(error))
191
+ return SCREENSHOT_UPLOAD_RESULT_ERROR
192
+ }
193
+
194
+ /**
195
+ * Decodes and validates PNG screenshot data returned by WebdriverIO.
196
+ *
197
+ * @param {unknown} screenshot - Base64-encoded PNG data
198
+ * @returns {Buffer} Decoded PNG data
199
+ */
200
+ function decodeWebdriverioScreenshot (screenshot) {
201
+ if (typeof screenshot !== 'string' || !BASE64_RE.test(screenshot)) {
202
+ throw new TypeError('WebdriverIO returned invalid Base64 screenshot data')
203
+ }
204
+
205
+ const content = Buffer.from(screenshot, 'base64')
206
+ if (
207
+ content.toString('base64') !== screenshot ||
208
+ content.length < PNG_SIGNATURE.length ||
209
+ !content.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
210
+ ) {
211
+ throw new TypeError('WebdriverIO returned invalid PNG screenshot data')
212
+ }
213
+ return content
214
+ }
215
+
143
216
  /**
144
217
  * Resolves the spec file responsible for a run-level Jasmine failure.
145
218
  *
@@ -174,14 +247,37 @@ class MochaPlugin extends CiPlugin {
174
247
 
175
248
  this._testTitleToParams = {}
176
249
  this.sourceRoot = process.cwd()
177
- this._pendingTestSuiteSpans = []
178
- this._timeOrigin = dateNow()
179
- this._perfOrigin = performance.now()
180
-
181
- this.addSub('ci:mocha:session:start', () => {
182
- this._pendingTestSuiteSpans = []
183
- this._timeOrigin = dateNow()
184
- this._perfOrigin = performance.now()
250
+ this._webdriverioScreenshotUploads = new WeakMap()
251
+ this._pendingWebdriverioScreenshotUploads = 0
252
+ this._webdriverioScreenshotUploadCallbacks = []
253
+
254
+ this.addSub('ci:webdriverio:screenshot:capabilities', (ctx) => {
255
+ ctx.enabled = Boolean(
256
+ this._tracerConfig.testOptimization?.DD_TEST_FAILURE_SCREENSHOTS_ENABLED &&
257
+ this.tracer._exporter?.canUploadTestScreenshots?.()
258
+ )
259
+ })
260
+
261
+ this.addSub('ci:webdriverio:screenshot:upload', ({
262
+ capturedAtMs,
263
+ idempotencyKey,
264
+ onDone,
265
+ screenshot,
266
+ traceId,
267
+ }) => {
268
+ const exporter = this.tracer?._exporter
269
+ if (!exporter?.canUploadTestScreenshots?.() || !exporter.uploadTestScreenshot) {
270
+ onDone(new Error('WebdriverIO screenshot upload is not supported by the active Test Optimization transport'))
271
+ return
272
+ }
273
+ let content
274
+ try {
275
+ content = decodeWebdriverioScreenshot(screenshot)
276
+ } catch (error) {
277
+ onDone(error)
278
+ return
279
+ }
280
+ exporter.uploadTestScreenshot({ content, traceId, idempotencyKey, capturedAtMs }, onDone)
185
281
  })
186
282
 
187
283
  this.addSub('ci:mocha:worker:configuration', ({
@@ -195,6 +291,18 @@ class MochaPlugin extends CiPlugin {
195
291
  this.testFramework = testFramework
196
292
  this.testFrameworkAdapter = testFrameworkAdapter
197
293
  this._setRepositoryRoot(repositoryRoot)
294
+ if (
295
+ testFramework === WEBDRIVERIO_FRAMEWORK &&
296
+ this._tracerConfig.testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED &&
297
+ !(isWebdriverioWorker
298
+ ? libraryConfig.isTestFailureScreenshotsEnabled
299
+ : this.tracer._exporter?.canUploadTestScreenshots?.())
300
+ ) {
301
+ log.warn(
302
+ 'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but WebdriverIO screenshot upload is not supported by the %s',
303
+ 'active Test Optimization transport.'
304
+ )
305
+ }
198
306
  if (testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
199
307
  this._webdriverioJasmineState = {
200
308
  completedTestStatuses: new Map(),
@@ -210,6 +318,14 @@ class MochaPlugin extends CiPlugin {
210
318
  }
211
319
  })
212
320
 
321
+ this.addSub('ci:webdriverio:rum:page-navigate', (ctx) => {
322
+ if (this.testFramework !== 'webdriverio') return
323
+ ctx.isTestOptimizationRunner = true
324
+
325
+ const activeSpan = storage('legacy').getStore()?.span
326
+ setWebdriverioRumTestCorrelation(ctx, activeSpan)
327
+ })
328
+
213
329
  this.addBind(jasmineTestFunctionStartCh, (ctx) => {
214
330
  if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
215
331
  return storage('legacy').getStore()
@@ -232,7 +348,7 @@ class MochaPlugin extends CiPlugin {
232
348
 
233
349
  const functionType = currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE]
234
350
  if (functionType === 'Test') {
235
- ctx.retryCallback = error => this.#retryWebdriverioJasmineTest(test, error)
351
+ ctx.retryCallback = this.#retryWebdriverioJasmineTestWithRumCorrelation.bind(this, ctx, test)
236
352
  }
237
353
  const nextStore = {
238
354
  ...test.currentStore,
@@ -271,12 +387,11 @@ class MochaPlugin extends CiPlugin {
271
387
  this.addSub(jasmineExecuteAsyncErrorCh, (ctx) => {
272
388
  const currentStore = ctx.currentStore || storage('legacy').getStore()
273
389
  const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
274
- if (
275
- this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER &&
276
- test &&
277
- currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE] === 'Hook'
278
- ) {
279
- test.hasFinalHookFailure = true
390
+ if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER && test) {
391
+ this.#startWebdriverioScreenshotUpload(test.span)
392
+ if (currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE] === 'Hook') {
393
+ test.hasFinalHookFailure = true
394
+ }
280
395
  }
281
396
  })
282
397
 
@@ -291,6 +406,7 @@ class MochaPlugin extends CiPlugin {
291
406
  this._webdriverioJasmineState.currentResult?.failedExpectations?.length > failedExpectationCount
292
407
  ) {
293
408
  test.hasFinalHookFailure = true
409
+ this.#startWebdriverioScreenshotUpload(test.span)
294
410
  }
295
411
  })
296
412
 
@@ -482,19 +598,7 @@ class MochaPlugin extends CiPlugin {
482
598
  if (!testSuiteSpan.context().getTag(TEST_STATUS)) {
483
599
  testSuiteSpan.setTag(TEST_STATUS, status)
484
600
  }
485
- const exporter = this.tracer._exporter
486
- if (exporter.deferTestSuiteSpan) {
487
- exporter.deferTestSuiteSpan(testSuiteSpan)
488
- testSuiteSpan.finish()
489
- } else if (exporter.exportDeferredTestSuiteSpans) {
490
- const finishTime = this._now()
491
- this._pendingTestSuiteSpans.push({
492
- span: testSuiteSpan,
493
- finishTime,
494
- })
495
- } else {
496
- testSuiteSpan.finish()
497
- }
601
+ testSuiteSpan.finish()
498
602
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
499
603
  }
500
604
  })
@@ -544,8 +648,12 @@ class MochaPlugin extends CiPlugin {
544
648
  })
545
649
 
546
650
  this.addSub('ci:mocha:worker:finish', ({ onDone } = {}) => {
547
- this._finishPendingTestSuiteSpans()
548
- this.tracer._exporter.flush(onDone)
651
+ const flush = () => this.tracer._exporter.flush(onDone)
652
+ if (this._pendingWebdriverioScreenshotUploads === 0) {
653
+ flush()
654
+ return
655
+ }
656
+ this._webdriverioScreenshotUploadCallbacks.push(flush)
549
657
  })
550
658
 
551
659
  this.addSub('ci:mocha:test:finish', ({
@@ -562,6 +670,7 @@ class MochaPlugin extends CiPlugin {
562
670
  earlyFlakeAbortReason,
563
671
  }) => {
564
672
  if (span) {
673
+ const finishTime = span._getTime()
565
674
  span.setTag(TEST_STATUS, status)
566
675
  if (finalStatus) {
567
676
  span.setTag(TEST_FINAL_STATUS, finalStatus)
@@ -590,20 +699,27 @@ class MochaPlugin extends CiPlugin {
590
699
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atf)
591
700
  }
592
701
 
593
- this.telemetry.ciVisEvent(
594
- TELEMETRY_EVENT_FINISHED,
595
- 'test',
596
- this.getTestTelemetryTags(span)
597
- )
598
-
599
- span.finish()
600
- finishAllTraceSpans(span)
601
- this.activeTestSpan = null
602
702
  this.cancelDiBreakpointHitWait()
603
703
  if (this.di && this.libraryConfig?.isDiEnabled && this.runningTestProbe && isLastRetry) {
604
704
  this.removeDiProbe(this.runningTestProbe)
605
705
  this.runningTestProbe = null
606
706
  }
707
+
708
+ const finishSpan = () => {
709
+ this.telemetry.ciVisEvent(
710
+ TELEMETRY_EVENT_FINISHED,
711
+ 'test',
712
+ this.getTestTelemetryTags(span)
713
+ )
714
+ span.finish(finishTime)
715
+ finishAllTraceSpans(span)
716
+ if (this.activeTestSpan === span) {
717
+ this.activeTestSpan = null
718
+ }
719
+ }
720
+ if (status !== 'fail' || !this.#startWebdriverioScreenshotUpload(span, finishSpan)) {
721
+ finishSpan()
722
+ }
607
723
  }
608
724
  })
609
725
 
@@ -633,6 +749,7 @@ class MochaPlugin extends CiPlugin {
633
749
  } else {
634
750
  span.setTag(TEST_STATUS, 'fail')
635
751
  span.setTag('error', err)
752
+ this.#startWebdriverioScreenshotUpload(span)
636
753
  }
637
754
 
638
755
  ctx.parentStore = ctx.currentStore
@@ -657,8 +774,9 @@ class MochaPlugin extends CiPlugin {
657
774
  promises,
658
775
  }) => {
659
776
  if (span) {
777
+ const finishTime = span._getTime()
660
778
  const finishSpan = () => {
661
- span.finish()
779
+ span.finish(finishTime)
662
780
  finishAllTraceSpans(span)
663
781
  if (this.activeTestSpan === span) {
664
782
  this.activeTestSpan = null
@@ -701,17 +819,42 @@ class MochaPlugin extends CiPlugin {
701
819
  }
702
820
  }
703
821
 
822
+ let finishWait
704
823
  if (!isFirstFailure &&
705
824
  willBeRetried &&
706
825
  this.di &&
707
826
  this.libraryConfig?.isDiEnabled &&
708
- this.runningTestProbe &&
709
- promises) {
710
- promises.finishTestPromise = this.waitForInFlightDiBreakpointHits().then(finishSpan, finishSpan)
711
- return
827
+ this.runningTestProbe) {
828
+ finishWait = this.waitForInFlightDiBreakpointHits()
712
829
  }
713
-
714
- finishSpan()
830
+ let screenshotFinished = false
831
+ let canFinishScreenshot = false
832
+ let replayFinished = !finishWait
833
+ let spanFinished = false
834
+ const finishWhenReady = () => {
835
+ if (!spanFinished && replayFinished && (screenshotStarted === false || screenshotFinished)) {
836
+ spanFinished = true
837
+ finishSpan()
838
+ }
839
+ }
840
+ const screenshotStarted = this.#startWebdriverioScreenshotUpload(span, () => {
841
+ screenshotFinished = true
842
+ if (canFinishScreenshot) {
843
+ finishWhenReady()
844
+ }
845
+ })
846
+ canFinishScreenshot = true
847
+ if (finishWait) {
848
+ const finishReplay = () => {
849
+ replayFinished = true
850
+ finishWhenReady()
851
+ }
852
+ const finishTestPromise = finishWait.then(finishReplay, finishReplay)
853
+ if (promises) {
854
+ promises.finishTestPromise = finishTestPromise
855
+ }
856
+ }
857
+ finishWhenReady()
715
858
  }
716
859
  })
717
860
 
@@ -755,7 +898,6 @@ class MochaPlugin extends CiPlugin {
755
898
  this.testSessionSpan.setTag('error', error)
756
899
  this.testModuleSpan.setTag('error', error)
757
900
  if (isFrameworkError) {
758
- this.tracer._exporter.setDeferredTestSuiteError?.(error)
759
901
  for (const testSuiteSpan of this._testSuiteSpansByTestSuite.values()) {
760
902
  testSuiteSpan.setTag(TEST_STATUS, 'fail')
761
903
  testSuiteSpan.setTag('error', error)
@@ -763,8 +905,6 @@ class MochaPlugin extends CiPlugin {
763
905
  }
764
906
  }
765
907
 
766
- this._finishPendingTestSuiteSpans()
767
-
768
908
  if (isParallel) {
769
909
  this.testSessionSpan.setTag(MOCHA_IS_PARALLEL, 'true')
770
910
  }
@@ -802,7 +942,6 @@ class MochaPlugin extends CiPlugin {
802
942
  this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
803
943
  }
804
944
 
805
- this.tracer._exporter.exportDeferredTestSuiteSpans?.()
806
945
  this.testModuleSpan.finish()
807
946
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
808
947
  this.testSessionSpan.finish()
@@ -824,27 +963,6 @@ class MochaPlugin extends CiPlugin {
824
963
  })
825
964
  }
826
965
 
827
- /**
828
- * Returns the current time in the coordinate system used by spans.
829
- *
830
- * @returns {number}
831
- */
832
- _now () {
833
- return this._timeOrigin + performance.now() - this._perfOrigin
834
- }
835
-
836
- /**
837
- * Finishes suites retained for a later framework finalization event.
838
- *
839
- * @returns {void}
840
- */
841
- _finishPendingTestSuiteSpans () {
842
- for (const { span, finishTime } of this._pendingTestSuiteSpans) {
843
- span.finish(finishTime)
844
- }
845
- this._pendingTestSuiteSpans = []
846
- }
847
-
848
966
  /**
849
967
  * Starts a Jasmine test span around WebdriverIO's test-function wrapper.
850
968
  *
@@ -1080,6 +1198,9 @@ class MochaPlugin extends CiPlugin {
1080
1198
  }
1081
1199
 
1082
1200
  const testStatus = getJasmineStatus(status)
1201
+ if (testStatus === 'fail') {
1202
+ this.#startWebdriverioScreenshotUpload(test.span)
1203
+ }
1083
1204
  if (
1084
1205
  testStatus !== 'skip' &&
1085
1206
  test.isEarlyFlakeDetection &&
@@ -1177,6 +1298,25 @@ class MochaPlugin extends CiPlugin {
1177
1298
  this.#startWebdriverioJasmineAttempt(test, test.currentStore)
1178
1299
  }
1179
1300
 
1301
+ /**
1302
+ * Advances a native WebdriverIO retry without stopping its active RUM session.
1303
+ *
1304
+ * @param {object} context
1305
+ * @param {object} test
1306
+ * @param {Error|undefined} error
1307
+ * @returns {Promise<void>}
1308
+ */
1309
+ async #retryWebdriverioJasmineTestWithRumCorrelation (context, test, error) {
1310
+ const correlationContext = await context.rumRetryCallback?.() || {}
1311
+ setWebdriverioRumTestCorrelation(correlationContext, test.span)
1312
+
1313
+ await this.#retryWebdriverioJasmineTest(test, error)
1314
+
1315
+ const testExecutionId = test.span.context().toTraceId()
1316
+ await context.rumRetryCallback?.(testExecutionId)
1317
+ setWebdriverioRumTestCorrelation(correlationContext, test.span)
1318
+ }
1319
+
1180
1320
  /**
1181
1321
  * Finishes an intermediate native WebdriverIO retry and starts its next span.
1182
1322
  *
@@ -1239,8 +1379,7 @@ class MochaPlugin extends CiPlugin {
1239
1379
  if (test._ddShouldWaitForHitProbe) {
1240
1380
  delete test._ddShouldWaitForHitProbe
1241
1381
  state.pendingTestFinishes++
1242
- const finishTest = () => {
1243
- this.#completeWebdriverioJasmineTest(test, result)
1382
+ const onTestFinished = () => {
1244
1383
  state.pendingTestFinishes--
1245
1384
  if (state.pendingTestFinishes === 0) {
1246
1385
  const callbacks = state.pendingTestFinishCallbacks
@@ -1250,10 +1389,14 @@ class MochaPlugin extends CiPlugin {
1250
1389
  }
1251
1390
  }
1252
1391
  }
1392
+ const finishTest = () => {
1393
+ const finishPromise = this.#completeWebdriverioJasmineTest(test, result)
1394
+ return finishPromise?.then(onTestFinished, onTestFinished) || onTestFinished()
1395
+ }
1253
1396
  return this.waitForDiBreakpointHits().then(finishTest, finishTest)
1254
1397
  }
1255
1398
 
1256
- this.#completeWebdriverioJasmineTest(test, result)
1399
+ return this.#completeWebdriverioJasmineTest(test, result)
1257
1400
  }
1258
1401
 
1259
1402
  /**
@@ -1312,6 +1455,191 @@ class MochaPlugin extends CiPlugin {
1312
1455
  }
1313
1456
  }
1314
1457
 
1458
+ /**
1459
+ * Captures and uploads the current WebdriverIO browser state once for a failed test attempt.
1460
+ *
1461
+ * @param {object} span - Failed test span
1462
+ * @param {() => void} [onDone] - Called after the screenshot upload finishes
1463
+ * @returns {boolean} Whether a screenshot upload exists for the span
1464
+ */
1465
+ #startWebdriverioScreenshotUpload (span, onDone) {
1466
+ if (
1467
+ !span ||
1468
+ this.testFramework !== WEBDRIVERIO_FRAMEWORK ||
1469
+ !this._tracerConfig.testOptimization?.DD_TEST_FAILURE_SCREENSHOTS_ENABLED
1470
+ ) {
1471
+ return false
1472
+ }
1473
+
1474
+ const previousUpload = this._webdriverioScreenshotUploads.get(span)
1475
+ if (previousUpload) {
1476
+ if (onDone) {
1477
+ if (previousUpload.finished) {
1478
+ onDone()
1479
+ } else {
1480
+ previousUpload.callbacks.push(onDone)
1481
+ }
1482
+ }
1483
+ return true
1484
+ }
1485
+
1486
+ const exporter = this.tracer?._exporter
1487
+ const canUpload = isWebdriverioWorker
1488
+ ? this.libraryConfig?.isTestFailureScreenshotsEnabled
1489
+ : exporter?.canUploadTestScreenshots?.() && exporter.uploadTestScreenshot
1490
+ if (!canUpload) {
1491
+ return false
1492
+ }
1493
+
1494
+ const browser = globalThis._wdioGlobals?.get?.('browser') || globalThis.browser
1495
+ const upload = {
1496
+ callbacks: onDone ? [onDone] : [],
1497
+ finished: false,
1498
+ }
1499
+ this._webdriverioScreenshotUploads.set(span, upload)
1500
+ this._pendingWebdriverioScreenshotUploads++
1501
+ const finishUpload = (result) => this.#finishWebdriverioScreenshotUpload(span, upload, result)
1502
+ let captureTimeout
1503
+ const failCapture = (error) => {
1504
+ if (upload.finished) return
1505
+ clearTimeout(captureTimeout)
1506
+ captureTimeout = undefined
1507
+ finishUpload(handleWebdriverioScreenshotError(error))
1508
+ }
1509
+ const uploadScreenshots = (content) => {
1510
+ if (upload.finished) return
1511
+ clearTimeout(captureTimeout)
1512
+ captureTimeout = undefined
1513
+ try {
1514
+ this.#uploadWebdriverioScreenshots(span, content, finishUpload)
1515
+ } catch (error) {
1516
+ failCapture(error)
1517
+ }
1518
+ }
1519
+ captureTimeout = setTimeout(() => {
1520
+ failCapture(new Error(
1521
+ `WebdriverIO screenshot capture timed out after ${WEBDRIVERIO_SCREENSHOT_CAPTURE_TIMEOUT_MS}ms`
1522
+ ))
1523
+ }, WEBDRIVERIO_SCREENSHOT_CAPTURE_TIMEOUT_MS)
1524
+ captureTimeout.unref?.()
1525
+ try {
1526
+ if (typeof browser?.takeScreenshot !== 'function') {
1527
+ throw new TypeError('browser.takeScreenshot is not available')
1528
+ }
1529
+ const screenshot = browser.takeScreenshot()
1530
+ if (typeof screenshot?.then === 'function') {
1531
+ screenshot.then(
1532
+ uploadScreenshots,
1533
+ failCapture
1534
+ )
1535
+ } else {
1536
+ uploadScreenshots(screenshot)
1537
+ }
1538
+ } catch (error) {
1539
+ failCapture(error)
1540
+ }
1541
+ return true
1542
+ }
1543
+
1544
+ /**
1545
+ * Completes one WebdriverIO screenshot upload and its waiters.
1546
+ *
1547
+ * @param {object} span - Failed test span
1548
+ * @param {{callbacks: Array<() => void>, finished: boolean}} upload - Upload state
1549
+ * @param {string} result - Aggregate screenshot upload result
1550
+ * @returns {void}
1551
+ */
1552
+ #finishWebdriverioScreenshotUpload (span, upload, result) {
1553
+ if (upload.finished) return
1554
+
1555
+ upload.finished = true
1556
+ setScreenshotUploadTags(span, result)
1557
+ const uploadCallbacks = upload.callbacks
1558
+ upload.callbacks = []
1559
+
1560
+ this._pendingWebdriverioScreenshotUploads--
1561
+ let workerCallbacks = []
1562
+ if (this._pendingWebdriverioScreenshotUploads === 0) {
1563
+ workerCallbacks = this._webdriverioScreenshotUploadCallbacks
1564
+ this._webdriverioScreenshotUploadCallbacks = []
1565
+ }
1566
+ for (const callback of uploadCallbacks) {
1567
+ callback()
1568
+ }
1569
+ for (const callback of workerCallbacks) {
1570
+ callback()
1571
+ }
1572
+ }
1573
+
1574
+ /**
1575
+ * Uploads the PNG data returned by WebdriverIO for one or more browser sessions.
1576
+ *
1577
+ * @param {object} span - Failed test span
1578
+ * @param {string|string[]} screenshots - Base64-encoded PNG data
1579
+ * @param {(result: string) => void} onDone - Aggregate upload completion callback
1580
+ * @returns {void}
1581
+ */
1582
+ #uploadWebdriverioScreenshots (span, screenshots, onDone) {
1583
+ const screenshotList = Array.isArray(screenshots) ? screenshots : [screenshots]
1584
+ if (screenshotList.length === 0) {
1585
+ log.error('Error capturing WebdriverIO failure screenshot: WebdriverIO returned no screenshot data')
1586
+ onDone(SCREENSHOT_UPLOAD_RESULT_ERROR)
1587
+ return
1588
+ }
1589
+ const exporter = this.tracer?._exporter
1590
+ const traceId = span.context().toTraceId()
1591
+ const capturedAtMs = Date.now()
1592
+ let pendingUploads = screenshotList.length
1593
+ let hasUploadError = false
1594
+ const finishOne = (error) => {
1595
+ if (error) {
1596
+ hasUploadError = true
1597
+ log.error('Error uploading WebdriverIO failure screenshot: %s', error?.message || String(error))
1598
+ }
1599
+ if (--pendingUploads === 0) {
1600
+ onDone(hasUploadError ? SCREENSHOT_UPLOAD_RESULT_ERROR : SCREENSHOT_UPLOAD_RESULT_UPLOADED)
1601
+ }
1602
+ }
1603
+ for (let index = 0; index < screenshotList.length; index++) {
1604
+ const screenshot = screenshotList[index]
1605
+ if (typeof screenshot !== 'string' || !screenshot) {
1606
+ finishOne(new TypeError('WebdriverIO returned invalid screenshot data'))
1607
+ continue
1608
+ }
1609
+ const options = {
1610
+ screenshot,
1611
+ traceId,
1612
+ idempotencyKey: `${traceId}:webdriverio-failure-${index}.png`,
1613
+ capturedAtMs,
1614
+ }
1615
+ if (isWebdriverioWorker) {
1616
+ try {
1617
+ requestWebdriverioScreenshotUpload(options, finishOne)
1618
+ } catch (error) {
1619
+ finishOne(error)
1620
+ }
1621
+ } else {
1622
+ let content
1623
+ try {
1624
+ content = decodeWebdriverioScreenshot(screenshot)
1625
+ } catch (error) {
1626
+ finishOne(error)
1627
+ continue
1628
+ }
1629
+ try {
1630
+ exporter.uploadTestScreenshot({
1631
+ content,
1632
+ traceId,
1633
+ idempotencyKey: options.idempotencyKey,
1634
+ capturedAtMs,
1635
+ }, finishOne)
1636
+ } catch (error) {
1637
+ finishOne(error)
1638
+ }
1639
+ }
1640
+ }
1641
+ }
1642
+
1315
1643
  /**
1316
1644
  * Reports Jasmine suite statuses and flushes worker traces before its run settles.
1317
1645
  *
@@ -98,7 +98,13 @@ class MongodbCoreQueryPlugin extends DatabasePlugin {
98
98
  const MAX_DEPTH = 10
99
99
  const MAX_QUERY_LENGTH = 10_000
100
100
 
101
+ /** @param {{ q?: Record<string, unknown> }[] | { documents?: { q?: Record<string, unknown> }[] }} statements */
101
102
  function extractQuery (statements) {
103
+ if (!Array.isArray(statements)) {
104
+ const { documents } = statements
105
+ statements = Array.isArray(documents) ? documents : []
106
+ }
107
+
102
108
  if (statements.length === 1 && statements[0].q) return statements[0].q
103
109
 
104
110
  const extractedQueries = []