dd-trace 6.12.0 → 6.14.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 (251) 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/cypress.js +9 -11
  8. package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
  9. package/ci/test-optimization-validation/package-check.js +1 -1
  10. package/ci/test-optimization-validation/report-writer.js +7 -5
  11. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  12. package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
  13. package/ci/vitest-no-worker-init-setup.mjs +13 -13
  14. package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
  15. package/index.d.ts +127 -26
  16. package/package.json +22 -19
  17. package/packages/datadog-esbuild/index.js +3 -1
  18. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  19. package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
  20. package/packages/datadog-instrumentations/src/fs.js +24 -20
  21. package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
  22. package/packages/datadog-instrumentations/src/graphql.js +16 -16
  23. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  24. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
  25. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  26. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +266 -0
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  33. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
  34. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +13 -0
  35. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +279 -0
  36. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +12 -1
  37. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
  38. package/packages/datadog-instrumentations/src/jest.js +165 -38
  39. package/packages/datadog-instrumentations/src/knex.js +76 -0
  40. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  41. package/packages/datadog-instrumentations/src/mariadb.js +13 -0
  42. package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
  43. package/packages/datadog-instrumentations/src/mocha/main.js +25 -15
  44. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  45. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
  46. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
  47. package/packages/datadog-instrumentations/src/net.js +14 -17
  48. package/packages/datadog-instrumentations/src/next.js +212 -62
  49. package/packages/datadog-instrumentations/src/openai.js +42 -40
  50. package/packages/datadog-instrumentations/src/pg.js +15 -0
  51. package/packages/datadog-instrumentations/src/pino.js +15 -4
  52. package/packages/datadog-instrumentations/src/playwright.js +137 -27
  53. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
  54. package/packages/datadog-instrumentations/src/sequelize.js +94 -2
  55. package/packages/datadog-instrumentations/src/undici.js +53 -4
  56. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +44 -7
  57. package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
  58. package/packages/datadog-instrumentations/src/vitest-worker.js +82 -14
  59. package/packages/datadog-instrumentations/src/webdriverio.js +986 -1
  60. package/packages/datadog-instrumentations/src/winston.js +6 -3
  61. package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
  62. package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
  63. package/packages/datadog-plugin-amqp10/src/util.js +2 -1
  64. package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
  65. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
  66. package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
  67. package/packages/datadog-plugin-cucumber/src/index.js +0 -2
  68. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
  69. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  70. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
  71. package/packages/datadog-plugin-graphql/src/execute.js +998 -196
  72. package/packages/datadog-plugin-graphql/src/index.js +27 -7
  73. package/packages/datadog-plugin-graphql/src/jit.js +136 -0
  74. package/packages/datadog-plugin-graphql/src/request.js +18 -27
  75. package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
  76. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
  77. package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
  78. package/packages/datadog-plugin-graphql/src/utils.js +208 -120
  79. package/packages/datadog-plugin-graphql/src/validate.js +10 -23
  80. package/packages/datadog-plugin-http/src/client.js +2 -15
  81. package/packages/datadog-plugin-http2/src/client.js +2 -15
  82. package/packages/datadog-plugin-jest/src/index.js +0 -6
  83. package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
  84. package/packages/datadog-plugin-mocha/src/index.js +403 -75
  85. package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
  86. package/packages/datadog-plugin-mysql/src/index.js +41 -15
  87. package/packages/datadog-plugin-net/src/tcp.js +3 -1
  88. package/packages/datadog-plugin-next/src/index.js +63 -29
  89. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  90. package/packages/datadog-plugin-playwright/src/index.js +176 -28
  91. package/packages/datadog-plugin-undici/src/index.js +29 -56
  92. package/packages/datadog-plugin-vitest/src/index.js +6 -12
  93. package/packages/dd-trace/src/aiguard/evaluation.js +96 -21
  94. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  95. package/packages/dd-trace/src/aiguard/messages/anthropic.js +29 -10
  96. package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
  97. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
  98. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  99. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  100. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  101. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  102. package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
  103. package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
  104. package/packages/dd-trace/src/appsec/graphql.js +6 -2
  105. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  106. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
  107. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
  108. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +11 -36
  109. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
  110. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  111. package/packages/dd-trace/src/appsec/lambda.js +44 -26
  112. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  113. package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
  114. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
  115. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
  116. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
  117. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +7 -150
  118. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
  119. package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
  121. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +397 -234
  122. package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
  124. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
  125. package/packages/dd-trace/src/ci-visibility/final-flush.js +21 -0
  126. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
  127. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
  128. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
  129. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +176 -0
  130. package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
  131. package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
  132. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  133. package/packages/dd-trace/src/config/index.js +75 -14
  134. package/packages/dd-trace/src/config/parsers.js +11 -1
  135. package/packages/dd-trace/src/config/stable.js +3 -3
  136. package/packages/dd-trace/src/config/supported-configurations.json +47 -2
  137. package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
  138. package/packages/dd-trace/src/debugger/config.js +12 -1
  139. package/packages/dd-trace/src/debugger/constants.js +1 -0
  140. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
  141. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +33 -0
  142. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -39
  143. package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
  144. package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
  145. package/packages/dd-trace/src/debugger/index.js +23 -3
  146. package/packages/dd-trace/src/dogstatsd.js +532 -71
  147. package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
  148. package/packages/dd-trace/src/encode/0.4.js +5 -5
  149. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
  150. package/packages/dd-trace/src/evp_proxy/direct.js +27 -23
  151. package/packages/dd-trace/src/exporters/agent/index.js +24 -3
  152. package/packages/dd-trace/src/exporters/agent/writer.js +28 -11
  153. package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
  154. package/packages/dd-trace/src/exporters/agentless/writer.js +100 -118
  155. package/packages/dd-trace/src/exporters/common/agents.js +28 -6
  156. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
  157. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
  158. package/packages/dd-trace/src/exporters/common/request.js +31 -11
  159. package/packages/dd-trace/src/exporters/common/url.js +40 -1
  160. package/packages/dd-trace/src/exporters/common/writer.js +31 -7
  161. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  162. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  163. package/packages/dd-trace/src/flush.js +105 -0
  164. package/packages/dd-trace/src/id.js +14 -0
  165. package/packages/dd-trace/src/index.js +5 -2
  166. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  167. package/packages/dd-trace/src/llmobs/experiments/dataset.js +52 -2
  168. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  169. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  170. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  171. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  172. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  173. package/packages/dd-trace/src/llmobs/index.js +111 -48
  174. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  175. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
  176. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
  177. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  178. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  179. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
  180. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  181. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  182. package/packages/dd-trace/src/llmobs/util.js +0 -33
  183. package/packages/dd-trace/src/llmobs/writers/base.js +106 -53
  184. package/packages/dd-trace/src/log/index.js +7 -1
  185. package/packages/dd-trace/src/log-submission/log-submission-plugin.js +249 -0
  186. package/packages/dd-trace/src/noop/span.js +5 -0
  187. package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
  188. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  189. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  190. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  191. package/packages/dd-trace/src/opentelemetry/logs/index.js +9 -25
  192. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  193. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
  194. package/packages/dd-trace/src/opentelemetry/metrics/index.js +18 -23
  195. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  196. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +18 -6
  197. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +14 -2
  198. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  199. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  200. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
  201. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
  202. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  203. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
  204. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  205. package/packages/dd-trace/src/opentracing/propagation/text_map.js +494 -351
  206. package/packages/dd-trace/src/opentracing/span.js +28 -0
  207. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  208. package/packages/dd-trace/src/payload-tagging/index.js +2 -1
  209. package/packages/dd-trace/src/plugin_manager.js +2 -0
  210. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
  211. package/packages/dd-trace/src/plugins/tracing.js +12 -10
  212. package/packages/dd-trace/src/plugins/util/ci.js +3 -1
  213. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
  214. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  215. package/packages/dd-trace/src/plugins/util/test.js +157 -41
  216. package/packages/dd-trace/src/plugins/util/url.js +2 -2
  217. package/packages/dd-trace/src/plugins/util/web.js +6 -74
  218. package/packages/dd-trace/src/profiling/config.js +1 -0
  219. package/packages/dd-trace/src/profiling/oom.js +11 -5
  220. package/packages/dd-trace/src/profiling/profiler.js +30 -13
  221. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  222. package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
  223. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  224. package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
  225. package/packages/dd-trace/src/proxy.js +108 -10
  226. package/packages/dd-trace/src/remote_config/index.js +319 -101
  227. package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
  228. package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
  229. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  230. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +10 -1
  231. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +22 -7
  232. package/packages/dd-trace/src/sampling_rule.js +36 -9
  233. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +51 -0
  234. package/packages/dd-trace/src/serverless/vercel.js +155 -0
  235. package/packages/dd-trace/src/serverless.js +45 -21
  236. package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
  237. package/packages/dd-trace/src/span_stats.js +64 -31
  238. package/packages/dd-trace/src/startup-log.js +3 -2
  239. package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
  240. package/packages/dd-trace/src/telemetry/send-data.js +18 -16
  241. package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
  242. package/packages/dd-trace/src/tracer.js +26 -5
  243. package/packages/dd-trace/src/tracer_metadata.js +11 -3
  244. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  245. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  246. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  247. package/vendor/dist/protobufjs/index.js +1 -1
  248. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  249. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
  250. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +0 -53
  251. package/packages/dd-trace/src/encode/agentless-json.js +0 -209
@@ -1,11 +1,16 @@
1
1
  'use strict'
2
2
 
3
+ // Capture real timers at module load time before tests can install fake timers.
4
+ const realSetTimeout = setTimeout
5
+
3
6
  const { AsyncResource } = require('node:async_hooks')
4
7
  const { fileURLToPath } = require('node:url')
5
8
 
6
9
  const { EMPTY_EFD_RETRY_POLICY } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
7
- const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
10
+ const { RUM_TEST_EXECUTION_ID_COOKIE_NAME } = require('../../dd-trace/src/ci-visibility/rum')
11
+ const { getEnvironmentVariable, getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
8
12
  const log = require('../../dd-trace/src/log')
13
+ const shimmer = require('../../datadog-shimmer')
9
14
  const {
10
15
  collectTestOptimizationSummariesFromTraces,
11
16
  getIsFaultyEarlyFlakeDetection,
@@ -16,10 +21,13 @@ const {
16
21
  MOCHA_WORKER_TRACE_PAYLOAD_CODE,
17
22
  TEST_SUITE_EXECUTION_ID,
18
23
  } = require('../../dd-trace/src/plugins/util/test')
24
+ const { publishWithCompletion } = require('./helpers/channel')
19
25
  const { addHook, channel, tracingChannel } = require('./helpers/instrument')
20
26
  const {
21
27
  CONFIGURATION_REQUEST,
22
28
  CONFIGURATION_RESPONSE,
29
+ SCREENSHOT_UPLOAD,
30
+ SCREENSHOT_UPLOAD_RESPONSE,
23
31
  sendWebdriverioWorkerMessage,
24
32
  SUITE_FINISH,
25
33
  WEBDRIVERIO_WORKER_ENV,
@@ -28,8 +36,10 @@ const {
28
36
  WORKER_READY,
29
37
  WORKER_READY_RESPONSE,
30
38
  } = require('./mocha/webdriverio-protocol')
39
+ const { detectRum, stopRumSessionAndReportActivity } = require('./rum-browser-scripts')
31
40
 
32
41
  const jasmineDoneCh = channel('ci:webdriverio:jasmine:done')
42
+ const rumPageNavigateCh = channel('ci:webdriverio:rum:page-navigate')
33
43
  const testFinishCh = channel('ci:mocha:test:finish')
34
44
  const testSessionStartCh = channel('ci:mocha:session:start')
35
45
  const testSessionFinishCh = channel('ci:mocha:session:finish')
@@ -38,7 +48,10 @@ const testSuiteStartCh = channel('ci:mocha:test-suite:start')
38
48
  const testSuiteFinishCh = channel('ci:mocha:test-suite:finish')
39
49
  const knownTestsCh = channel('ci:mocha:known-tests')
40
50
  const libraryConfigurationCh = channel('ci:mocha:library-configuration')
51
+ const logSubmissionFlushCh = channel('ci:log-submission:flush')
41
52
  const modifiedFilesCh = channel('ci:mocha:modified-files')
53
+ const screenshotCapabilitiesCh = channel('ci:webdriverio:screenshot:capabilities')
54
+ const screenshotUploadCh = channel('ci:webdriverio:screenshot:upload')
42
55
  const testManagementTestsCh = channel('ci:mocha:test-management-tests')
43
56
  const workerConfigurationCh = channel('ci:mocha:worker:configuration')
44
57
  const workerReportLogsCh = channel('ci:mocha:worker-report:logs')
@@ -46,15 +59,25 @@ const workerReportTelemetryCh = channel('ci:mocha:worker-report:telemetry')
46
59
  const workerReportTraceCh = channel('ci:mocha:worker-report:trace')
47
60
 
48
61
  const jasmineAdapterInitCh = tracingChannel('orchestrion:@wdio/jasmine-framework:JasmineAdapter_init')
62
+ const baseReporterWaitForSyncCh = tracingChannel('orchestrion:@wdio/runner:BaseReporter_waitForSync')
63
+ const runnerRunCh = tracingChannel('orchestrion:@wdio/runner:Runner_run')
64
+ const executeAsyncCh = tracingChannel('orchestrion:@wdio/utils:executeAsync')
49
65
  const launcherStartInstanceCh = tracingChannel('orchestrion:@wdio/cli:Launcher_startInstance')
50
66
  const localRunnerRunCh = tracingChannel('orchestrion:@wdio/local-runner:LocalRunner_run')
51
67
  const localRunnerShutdownCh = tracingChannel('orchestrion:@wdio/local-runner:LocalRunner_shutdown')
68
+ const testFrameworkFnWrapperCh = tracingChannel('orchestrion:@wdio/utils:testFrameworkFnWrapper')
69
+ const newWindowCh = tracingChannel('orchestrion:webdriverio:newWindow')
70
+ const webdriverBidiInitCh = tracingChannel('orchestrion:webdriver:initiateBidi')
71
+ const webdriverCommandCh = tracingChannel('orchestrion:webdriver:command')
72
+ const urlCh = tracingChannel('orchestrion:webdriverio:url')
52
73
 
53
74
  const NODE_OPTIONS_SEPARATOR_RE = /\s/
54
75
  const JASMINE_FRAMEWORK_ADAPTER = 'jasmine'
55
76
  const MOCHA_FRAMEWORK_ADAPTER = 'mocha'
56
77
  const SUPPORTED_FRAMEWORK_ADAPTERS = new Set([JASMINE_FRAMEWORK_ADAPTER, MOCHA_FRAMEWORK_ADAPTER])
57
78
  const TEST_FRAMEWORK = 'webdriverio'
79
+ const SET_RUM_COOKIE_SCRIPT = 'cookie => { globalThis.document.cookie = cookie }'
80
+ const RUM_FLUSH_WAIT_TIME = getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')
58
81
  const isWebdriverioWorker = !!getEnvironmentVariable(WEBDRIVERIO_WORKER_ENV)
59
82
  let jasmineWorkerRequestId = 0
60
83
 
@@ -65,6 +88,22 @@ if (loadCh.hasSubscribers) {
65
88
 
66
89
  const coordinatorStates = new WeakMap()
67
90
  const localRunnerVersions = new WeakMap()
91
+ const rumBrowsers = new Set()
92
+ const rumCorrelationBrowsers = new Set()
93
+ const rumRunnerBrowsers = new WeakSet()
94
+ const rumBrowserPreloadScripts = new WeakMap()
95
+ const rumBrowserSessionEndHandlers = new WeakMap()
96
+ const rumBrowserTestExecutionIds = new WeakMap()
97
+ let isRumCleanupPending = false
98
+
99
+ /**
100
+ * Returns the current BiDi browsing context, including an iframe context when one is selected.
101
+ *
102
+ * @returns {object}
103
+ */
104
+ function getCurrentRumContext () {
105
+ return globalThis.window
106
+ }
68
107
 
69
108
  addHook({
70
109
  name: '@wdio/local-runner',
@@ -76,6 +115,834 @@ addHook({
76
115
  return LocalRunner
77
116
  })
78
117
 
118
+ /**
119
+ * Returns whether this browser can preload and clean correlation across every browsing context.
120
+ *
121
+ * @param {object} browser
122
+ * @returns {boolean}
123
+ */
124
+ function canCorrelateRumBrowser (browser) {
125
+ return browser?.isBidi &&
126
+ typeof browser.scriptAddPreloadScript === 'function' &&
127
+ typeof browser.scriptRemovePreloadScript === 'function' &&
128
+ typeof browser.storageDeleteCookies === 'function'
129
+ }
130
+
131
+ /**
132
+ * Releases retained state when a WebDriver session ends.
133
+ *
134
+ * @param {object} browser
135
+ * @returns {void}
136
+ */
137
+ function releaseRumBrowser (browser) {
138
+ rumBrowsers.delete(browser)
139
+ rumCorrelationBrowsers.delete(browser)
140
+ rumRunnerBrowsers.delete(browser)
141
+ rumBrowserPreloadScripts.delete(browser)
142
+ rumBrowserTestExecutionIds.delete(browser)
143
+
144
+ const sessionEndHandler = rumBrowserSessionEndHandlers.get(browser)
145
+ rumBrowserSessionEndHandlers.delete(browser)
146
+ if (sessionEndHandler && typeof browser.removeListener === 'function') {
147
+ browser.removeListener('result', sessionEndHandler)
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Retains a browser and watches for WebDriver's deleteSession result event.
153
+ *
154
+ * @param {object} browser
155
+ * @returns {void}
156
+ */
157
+ function retainRumBrowser (browser) {
158
+ rumBrowsers.add(browser)
159
+ if (rumBrowserSessionEndHandlers.has(browser) || typeof browser.on !== 'function') return
160
+
161
+ const sessionEndHandler = ({ command, result }) => {
162
+ const closedFinalWindow = command === 'closeWindow' &&
163
+ Array.isArray(result?.value) && result.value.length === 0
164
+ if (command === 'deleteSession' || closedFinalWindow) releaseRumBrowser(browser)
165
+ }
166
+ rumBrowserSessionEndHandlers.set(browser, sessionEndHandler)
167
+ browser.on('result', sessionEndHandler)
168
+ }
169
+
170
+ /**
171
+ * Returns whether RUM is inactive because its configured sampling rate is below 100%.
172
+ *
173
+ * @param {{isRumActive: boolean, isRumInstrumented: boolean, rumSamplingRate: number|null}} rumState
174
+ * @returns {boolean}
175
+ */
176
+ function isRumSampledOut (rumState) {
177
+ return rumState.isRumInstrumented && !rumState.isRumActive &&
178
+ rumState.rumSamplingRate !== null && rumState.rumSamplingRate < 100
179
+ }
180
+
181
+ /**
182
+ * Installs RUM correlation on every subsequent document in the BiDi session.
183
+ *
184
+ * @param {object} browser
185
+ * @param {string} testExecutionId
186
+ * @returns {Promise<void>}
187
+ */
188
+ async function installRumPreloadScript (browser, testExecutionId) {
189
+ if (!browser.isBidi ||
190
+ typeof browser.scriptAddPreloadScript !== 'function') return
191
+
192
+ try {
193
+ const preloadScript = rumBrowserPreloadScripts.get(browser)
194
+ if (preloadScript && preloadScript.sessionId === browser.sessionId &&
195
+ preloadScript.testExecutionId === testExecutionId) return
196
+ if (preloadScript && preloadScript.sessionId === browser.sessionId) {
197
+ if (!await removeRumPreloadScript(browser)) return
198
+ } else {
199
+ rumBrowserPreloadScripts.delete(browser)
200
+ }
201
+
202
+ const cookie = `${RUM_TEST_EXECUTION_ID_COOKIE_NAME}=${testExecutionId}; path=/`
203
+ const { script } = await browser.scriptAddPreloadScript({
204
+ arguments: [{ type: 'string', value: cookie }],
205
+ functionDeclaration: SET_RUM_COOKIE_SCRIPT,
206
+ })
207
+ rumBrowserPreloadScripts.set(browser, { script, sessionId: browser.sessionId, testExecutionId })
208
+ } catch (error) {
209
+ log.error('WebdriverIO RUM correlation preload error', error)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Removes a browser-wide RUM correlation preload.
215
+ *
216
+ * @param {object} browser
217
+ * @returns {Promise<boolean>}
218
+ */
219
+ async function removeRumPreloadScript (browser) {
220
+ const preloadScript = rumBrowserPreloadScripts.get(browser)
221
+ if (!preloadScript) return true
222
+ if (preloadScript.sessionId !== browser.sessionId) {
223
+ if (rumBrowserPreloadScripts.get(browser) === preloadScript) rumBrowserPreloadScripts.delete(browser)
224
+ return true
225
+ }
226
+ if (typeof browser.scriptRemovePreloadScript !== 'function') return false
227
+
228
+ try {
229
+ await browser.scriptRemovePreloadScript({ script: preloadScript.script })
230
+ if (rumBrowserPreloadScripts.get(browser) === preloadScript) rumBrowserPreloadScripts.delete(browser)
231
+ return true
232
+ } catch (error) {
233
+ log.error('WebdriverIO RUM correlation preload cleanup error', error)
234
+ return false
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Returns every descendant browsing context in a BiDi context tree.
240
+ *
241
+ * @param {object[]} contexts
242
+ * @returns {string[]}
243
+ */
244
+ function getRumChildContexts (contexts) {
245
+ const childContexts = []
246
+ const pendingContexts = [...contexts]
247
+ while (pendingContexts.length) {
248
+ const context = pendingContexts.shift()
249
+ if (!context?.children) continue
250
+ for (const child of context.children) {
251
+ childContexts.push(child.context)
252
+ pendingContexts.push(child)
253
+ }
254
+ }
255
+ return childContexts
256
+ }
257
+
258
+ /**
259
+ * Gets the active test execution ID and applies browser metadata to its span.
260
+ *
261
+ * @param {object} browser
262
+ * @param {boolean|undefined} isRumActive
263
+ * @returns {string|undefined}
264
+ */
265
+ function getRumTestExecutionId (browser, isRumActive) {
266
+ const correlationContext = {
267
+ browserName: browser.capabilities?.browserName,
268
+ browserVersion: browser.capabilities?.browserVersion,
269
+ isRumActive,
270
+ isTestOptimizationRunner: isWebdriverioWorker,
271
+ testExecutionId: undefined,
272
+ }
273
+ try {
274
+ rumPageNavigateCh.publish(correlationContext)
275
+ } catch (error) {
276
+ log.error('WebdriverIO RUM correlation channel error', error)
277
+ }
278
+ if (correlationContext.isTestOptimizationRunner || correlationContext.testExecutionId) {
279
+ rumRunnerBrowsers.add(browser)
280
+ }
281
+ return correlationContext.testExecutionId
282
+ }
283
+
284
+ /**
285
+ * Installs RUM correlation before a BiDi navigation starts.
286
+ *
287
+ * @this {object}
288
+ * @returns {Promise<void>}
289
+ */
290
+ async function preloadRumNavigation () {
291
+ try {
292
+ const browser = this
293
+ const testExecutionId = getRumTestExecutionId(browser)
294
+ if (!canCorrelateRumBrowser(browser)) return
295
+ if (!rumRunnerBrowsers.has(browser)) return
296
+
297
+ retainRumBrowser(browser)
298
+ if (!testExecutionId) return
299
+
300
+ rumCorrelationBrowsers.add(browser)
301
+ rumBrowserTestExecutionIds.set(browser, testExecutionId)
302
+ await installRumPreloadScript(browser, testExecutionId)
303
+ } catch (error) {
304
+ log.error('WebdriverIO RUM correlation preload error', error)
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Correlates the current browser window with one test execution.
310
+ *
311
+ * @param {object} browser
312
+ * @param {string} testExecutionId
313
+ * @returns {Promise<void>}
314
+ */
315
+ async function correlateRumWindow (browser, testExecutionId) {
316
+ if (!canCorrelateRumBrowser(browser)) return
317
+
318
+ try {
319
+ await browser.setCookies({
320
+ name: RUM_TEST_EXECUTION_ID_COOKIE_NAME,
321
+ value: testExecutionId,
322
+ })
323
+ } catch (error) {
324
+ log.error('WebdriverIO RUM correlation cookie error', error)
325
+ }
326
+ await installRumPreloadScript(browser, testExecutionId)
327
+ }
328
+
329
+ /**
330
+ * Detects RUM after a browser navigation and correlates it with the active test.
331
+ *
332
+ * @param {{self?: object}} context
333
+ * @returns {Promise<void>}
334
+ */
335
+ async function handleRumNavigation (context) {
336
+ const browser = context.self
337
+ try {
338
+ if (!browser) return
339
+
340
+ const canCorrelateRum = canCorrelateRumBrowser(browser)
341
+ if (!canCorrelateRum) {
342
+ getRumTestExecutionId(browser, false)
343
+ return
344
+ }
345
+ if (!rumRunnerBrowsers.has(browser)) {
346
+ getRumTestExecutionId(browser)
347
+ if (!rumRunnerBrowsers.has(browser)) return
348
+ }
349
+ retainRumBrowser(browser)
350
+ if (typeof browser.execute !== 'function') return
351
+
352
+ let rumState
353
+ try {
354
+ rumState = await browser.execute(detectRum)
355
+ } catch (error) {
356
+ log.error('WebdriverIO RUM detection error', error)
357
+ return
358
+ }
359
+
360
+ if (!rumState) return
361
+
362
+ const { isRumActive } = rumState
363
+ const sampledOut = isRumSampledOut(rumState)
364
+ if (sampledOut) {
365
+ log.debug("RUM was detected on the page, but it isn't active because the sampling rate is below 100%")
366
+ }
367
+ const testExecutionId = getRumTestExecutionId(browser, isRumActive)
368
+ if (sampledOut) return
369
+
370
+ if (!testExecutionId) {
371
+ return
372
+ }
373
+ if (!isRumActive && browser.isBidi) return
374
+
375
+ await correlateRumBrowser(browser, testExecutionId)
376
+ } catch (error) {
377
+ log.error('WebdriverIO RUM correlation error', error)
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Stops RUM in the current window.
383
+ *
384
+ * @param {object} browser
385
+ * @returns {Promise<boolean>}
386
+ */
387
+ async function stopRumWindow (browser) {
388
+ let isRumActive
389
+ try {
390
+ isRumActive = await browser.execute(stopRumSessionAndReportActivity)
391
+ } catch (error) {
392
+ log.error('WebdriverIO RUM cleanup error', error)
393
+ }
394
+
395
+ if (isRumActive) {
396
+ getRumTestExecutionId(browser, true)
397
+ }
398
+ return !!isRumActive
399
+ }
400
+
401
+ /**
402
+ * Removes the RUM correlation cookie from the current window.
403
+ *
404
+ * @param {object} browser
405
+ * @returns {Promise<void>}
406
+ */
407
+ async function deleteRumCookie (browser) {
408
+ try {
409
+ await browser.deleteCookies([RUM_TEST_EXECUTION_ID_COOKIE_NAME])
410
+ } catch (error) {
411
+ log.error('WebdriverIO RUM correlation cookie cleanup error', error)
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Removes every RUM correlation cookie without loading its application origin.
417
+ *
418
+ * @param {object} browser
419
+ * @returns {Promise<void>}
420
+ */
421
+ async function cleanupRumCookies (browser) {
422
+ if (!browser.isBidi || typeof browser.storageDeleteCookies !== 'function') return
423
+
424
+ try {
425
+ await browser.storageDeleteCookies({
426
+ filter: { name: RUM_TEST_EXECUTION_ID_COOKIE_NAME },
427
+ })
428
+ } catch (error) {
429
+ log.error('WebdriverIO RUM cross-origin cookie cleanup error', error)
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Runs one RUM operation in every open browser window and restores the original window.
435
+ *
436
+ * @param {object} browser
437
+ * @template T
438
+ * @param {(browser: object, value?: T) => Promise<void>} operation
439
+ * @param {T} [value]
440
+ * @returns {Promise<void>}
441
+ */
442
+ async function forEachRumWindow (browser, operation, value) {
443
+ if (typeof browser.getWindowHandle !== 'function' ||
444
+ typeof browser.getWindowHandles !== 'function' ||
445
+ typeof browser.switchToWindow !== 'function') {
446
+ await operation(browser, value)
447
+ return
448
+ }
449
+
450
+ let currentWindowHandle
451
+ let windowHandles
452
+ try {
453
+ currentWindowHandle = await browser.getWindowHandle()
454
+ windowHandles = await browser.getWindowHandles()
455
+ } catch (error) {
456
+ log.error('WebdriverIO RUM window discovery error', error)
457
+ await operation(browser, value)
458
+ return
459
+ }
460
+
461
+ let currentFrameContext
462
+ if (browser.isBidi &&
463
+ typeof browser.execute === 'function' && typeof browser.switchFrame === 'function') {
464
+ try {
465
+ const currentContext = await browser.execute(getCurrentRumContext)
466
+ if (currentContext?.context && currentContext.context !== currentWindowHandle) {
467
+ currentFrameContext = currentContext.context
468
+ }
469
+ } catch (error) {
470
+ log.error('WebdriverIO RUM frame discovery error', error)
471
+ }
472
+ }
473
+
474
+ let activeWindowHandle = currentWindowHandle
475
+ let isTopLevel = !currentFrameContext
476
+ for (const windowHandle of windowHandles) {
477
+ if (windowHandle !== activeWindowHandle || !isTopLevel) {
478
+ try {
479
+ // WebDriver window commands must run in order because each one changes the active window.
480
+ // eslint-disable-next-line no-await-in-loop
481
+ await browser.switchToWindow(windowHandle)
482
+ activeWindowHandle = windowHandle
483
+ isTopLevel = true
484
+ } catch (error) {
485
+ log.error('WebdriverIO RUM window switch error', error)
486
+ continue
487
+ }
488
+ }
489
+ // eslint-disable-next-line no-await-in-loop
490
+ await operation(browser, value)
491
+
492
+ if (browser.isBidi &&
493
+ typeof browser.browsingContextGetTree === 'function' && typeof browser.switchFrame === 'function') {
494
+ let childContexts = []
495
+ try {
496
+ // eslint-disable-next-line no-await-in-loop
497
+ const contextTree = await browser.browsingContextGetTree({ root: windowHandle })
498
+ childContexts = getRumChildContexts(contextTree.contexts || [])
499
+ } catch (error) {
500
+ log.error('WebdriverIO RUM frame discovery error', error)
501
+ }
502
+
503
+ for (const childContext of childContexts) {
504
+ try {
505
+ // Reset to the window root so WebdriverIO can resolve nested frame paths consistently.
506
+ // eslint-disable-next-line no-await-in-loop
507
+ await browser.switchToWindow(windowHandle)
508
+ // eslint-disable-next-line no-await-in-loop
509
+ await browser.switchFrame(childContext)
510
+ isTopLevel = false
511
+ } catch (error) {
512
+ isTopLevel = true
513
+ log.error('WebdriverIO RUM frame switch error', error)
514
+ continue
515
+ }
516
+ // eslint-disable-next-line no-await-in-loop
517
+ await operation(browser, value)
518
+ }
519
+ }
520
+ }
521
+
522
+ const canRestoreWindow = windowHandles.includes(currentWindowHandle)
523
+ if (canRestoreWindow && (activeWindowHandle !== currentWindowHandle || !isTopLevel || currentFrameContext)) {
524
+ try {
525
+ if (activeWindowHandle !== currentWindowHandle || !isTopLevel) await browser.switchToWindow(currentWindowHandle)
526
+ if (currentFrameContext) await browser.switchFrame(currentFrameContext)
527
+ } catch (error) {
528
+ log.error('WebdriverIO RUM browsing context restore error', error)
529
+ }
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Stops RUM in every window before a browser session is closed.
535
+ *
536
+ * @param {object} browser
537
+ * @returns {Promise<boolean>}
538
+ */
539
+ async function stopRumBrowser (browser) {
540
+ await removeRumPreloadScript(browser)
541
+ let isRumActive = false
542
+ await forEachRumWindow(browser, async (browser) => {
543
+ if (await stopRumWindow(browser)) isRumActive = true
544
+ })
545
+ return isRumActive
546
+ }
547
+
548
+ /**
549
+ * Removes the current test's RUM correlation while keeping reusable sessions active.
550
+ *
551
+ * @param {object} browser
552
+ * @returns {Promise<void>}
553
+ */
554
+ async function cleanupRumBrowser (browser) {
555
+ await removeRumPreloadScript(browser)
556
+ await forEachRumWindow(browser, deleteRumCookie)
557
+ await cleanupRumCookies(browser)
558
+ rumBrowserTestExecutionIds.delete(browser)
559
+ }
560
+
561
+ /**
562
+ * Removes the current test's RUM correlation from every prepared browser.
563
+ *
564
+ * @returns {Promise<void>}
565
+ */
566
+ async function cleanupRumBrowsers () {
567
+ isRumCleanupPending = false
568
+ const browsers = [...rumCorrelationBrowsers]
569
+ rumCorrelationBrowsers.clear()
570
+ for (const browser of browsers) {
571
+ // Cleanup is intentionally sequential to avoid overlapping WebDriver commands.
572
+ // eslint-disable-next-line no-await-in-loop
573
+ await cleanupRumBrowser(browser)
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Cleans up every retained RUM browser before the WebdriverIO worker exits.
579
+ *
580
+ * @returns {Promise<void>}
581
+ */
582
+ async function cleanupAllRumBrowsers () {
583
+ isRumCleanupPending = false
584
+ const browsers = [...rumBrowsers]
585
+ rumBrowsers.clear()
586
+ rumCorrelationBrowsers.clear()
587
+ let isRumActive = false
588
+ for (const browser of browsers) {
589
+ // WebDriver commands are intentionally sequential for each browser.
590
+ // eslint-disable-next-line no-await-in-loop
591
+ if (await stopRumBrowser(browser)) isRumActive = true
592
+ }
593
+ if (isRumActive) {
594
+ await new Promise(resolve => realSetTimeout(resolve, RUM_FLUSH_WAIT_TIME))
595
+ }
596
+ for (const browser of browsers) {
597
+ // WebDriver commands are intentionally sequential for each browser.
598
+ // eslint-disable-next-line no-await-in-loop
599
+ await cleanupRumCookies(browser)
600
+ releaseRumBrowser(browser)
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Stops and flushes RUM before WebDriver deletes one retained session.
606
+ *
607
+ * @param {object} browser
608
+ * @returns {Promise<void>}
609
+ */
610
+ async function cleanupRumBrowserBeforeSessionEnd (browser) {
611
+ if (!rumBrowsers.has(browser)) return
612
+
613
+ if (await stopRumBrowser(browser)) {
614
+ await new Promise(resolve => realSetTimeout(resolve, RUM_FLUSH_WAIT_TIME))
615
+ }
616
+ await cleanupRumCookies(browser)
617
+ }
618
+
619
+ /**
620
+ * Stops and flushes RUM before closing the last window terminates its WebDriver session.
621
+ *
622
+ * @param {object} browser
623
+ * @returns {Promise<void>}
624
+ */
625
+ async function cleanupRumBrowserBeforeLastWindowClose (browser) {
626
+ if (!rumBrowsers.has(browser) || typeof browser.getWindowHandles !== 'function') return
627
+
628
+ let windowHandles
629
+ try {
630
+ windowHandles = await browser.getWindowHandles()
631
+ } catch (error) {
632
+ log.error('WebdriverIO RUM window discovery error', error)
633
+ return
634
+ }
635
+ if (windowHandles.length === 1) await cleanupRumBrowserBeforeSessionEnd(browser)
636
+ }
637
+
638
+ /**
639
+ * Prepares RUM around low-level WebDriver commands that bypass WebdriverIO's navigation helpers.
640
+ *
641
+ * @this {object}
642
+ * @param {string} command
643
+ * @returns {Promise<void>|undefined}
644
+ */
645
+ function handleRumProtocolCommand (command) {
646
+ if (command === 'navigateTo') return preloadRumNavigation.call(this)
647
+ if (command === 'deleteSession') return cleanupRumBrowserBeforeSessionEnd(this)
648
+ if (command === 'closeWindow') return cleanupRumBrowserBeforeLastWindowClose(this)
649
+ }
650
+
651
+ /**
652
+ * Wraps only WebDriver commands that need to wait for RUM work before their request starts.
653
+ *
654
+ * @param {{arguments?: unknown[], result?: Function}} context
655
+ * @returns {void}
656
+ */
657
+ function wrapRumProtocolCommand (context) {
658
+ const command = context.arguments?.[2]?.command
659
+ const protocolCommand = context.result
660
+ if ((command !== 'navigateTo' && command !== 'deleteSession' && command !== 'closeWindow') ||
661
+ typeof protocolCommand !== 'function') return
662
+
663
+ context.result = async function (...args) {
664
+ try {
665
+ await handleRumProtocolCommand.call(this, command)
666
+ } catch (error) {
667
+ log.error('WebdriverIO RUM protocol command error', error)
668
+ }
669
+ return protocolCommand.apply(this, args)
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Wraps the runtime-created BiDi navigation handler so RUM preparation can finish before it sends the command.
675
+ * A tracing-channel start subscriber is synchronous and cannot delay the static async handler itself.
676
+ *
677
+ * @param {{result?: {_bidiHandler?: {value?: object}}}} context
678
+ * @returns {void}
679
+ */
680
+ function wrapRumBidiNavigation (context) {
681
+ const bidiHandler = context.result?._bidiHandler?.value
682
+ if (typeof bidiHandler?.browsingContextNavigate !== 'function') return
683
+
684
+ try {
685
+ shimmer.wrap(bidiHandler, 'browsingContextNavigate', navigate => async function (...args) {
686
+ try {
687
+ await preloadRumNavigation.call(this.client)
688
+ } catch (error) {
689
+ log.error('WebdriverIO RUM BiDi navigation error', error)
690
+ }
691
+ return navigate.apply(this, args)
692
+ })
693
+ } catch (error) {
694
+ log.error('WebdriverIO RUM BiDi instrumentation error', error)
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Reapplies RUM correlation to every open browser window for a retry.
700
+ *
701
+ * @param {object} browser
702
+ * @param {string} testExecutionId
703
+ * @returns {Promise<void>}
704
+ */
705
+ async function correlateRumBrowser (browser, testExecutionId) {
706
+ if (!canCorrelateRumBrowser(browser)) return
707
+
708
+ retainRumBrowser(browser)
709
+ rumCorrelationBrowsers.add(browser)
710
+ rumBrowserTestExecutionIds.set(browser, testExecutionId)
711
+ await forEachRumWindow(browser, correlateRumWindow, testExecutionId)
712
+ }
713
+
714
+ /**
715
+ * Reapplies RUM correlation to browsers retained across a native retry.
716
+ *
717
+ * @param {object[]|undefined} browsers
718
+ * @param {string|undefined} testExecutionId
719
+ * @returns {Promise<void>}
720
+ */
721
+ async function correlateRumBrowsers (browsers, testExecutionId) {
722
+ if (!browsers || !testExecutionId) return
723
+
724
+ for (const browser of browsers) {
725
+ // WebDriver commands are intentionally sequential for each browser.
726
+ // eslint-disable-next-line no-await-in-loop
727
+ await correlateRumBrowser(browser, testExecutionId)
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Correlates RUM browsers discovered outside the current test before its next function runs.
733
+ *
734
+ * @returns {Promise<void>}
735
+ */
736
+ async function startRumTest () {
737
+ for (const browser of rumBrowsers) {
738
+ let isRumActive = false
739
+ let shouldCorrelateRum = false
740
+ // WebDriver commands are intentionally sequential for each browser.
741
+ // eslint-disable-next-line no-await-in-loop
742
+ await forEachRumWindow(browser, async (browser) => {
743
+ try {
744
+ const rumState = await browser.execute(detectRum)
745
+ if (!rumState) return
746
+ if (rumState.isRumActive) isRumActive = true
747
+ shouldCorrelateRum = true
748
+ } catch (error) {
749
+ log.error('WebdriverIO RUM detection error', error)
750
+ }
751
+ })
752
+ if (!shouldCorrelateRum) continue
753
+
754
+ const testExecutionId = getRumTestExecutionId(browser, isRumActive || undefined)
755
+ if (!testExecutionId || rumBrowserTestExecutionIds.get(browser) === testExecutionId) continue
756
+
757
+ // eslint-disable-next-line no-await-in-loop
758
+ await correlateRumBrowser(browser, testExecutionId)
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Replaces the completed test's RUM correlation before WebdriverIO runs callbacks for the next test or hook.
764
+ *
765
+ * @returns {Promise<void>}
766
+ */
767
+ async function prepareNextRumTest () {
768
+ await cleanupRumBrowsers()
769
+ await startRumTest()
770
+ }
771
+
772
+ /**
773
+ * Detects active RUM browsers and optionally updates the active test without ending their sessions.
774
+ *
775
+ * @param {boolean} [updateTest]
776
+ * @returns {Promise<object|undefined>}
777
+ */
778
+ async function detectActiveRumBrowsers (updateTest = true) {
779
+ let activeRumBrowser
780
+ for (const browser of rumCorrelationBrowsers) {
781
+ let isBrowserRumActive = false
782
+ // WebDriver commands are intentionally sequential for each browser.
783
+ // eslint-disable-next-line no-await-in-loop
784
+ await forEachRumWindow(browser, async (browser) => {
785
+ try {
786
+ const rumState = await browser.execute(detectRum)
787
+ if (rumState?.isRumActive) isBrowserRumActive = true
788
+ } catch (error) {
789
+ log.error('WebdriverIO RUM detection error', error)
790
+ }
791
+ })
792
+ if (isBrowserRumActive) {
793
+ activeRumBrowser ??= browser
794
+ if (updateTest) getRumTestExecutionId(browser, true)
795
+ }
796
+ }
797
+ return activeRumBrowser
798
+ }
799
+
800
+ /**
801
+ * Preserves the active test's RUM correlation across a native WebdriverIO retry.
802
+ *
803
+ * @returns {Promise<void>}
804
+ */
805
+ async function retryRumTest () {
806
+ const browsers = [...rumCorrelationBrowsers]
807
+ if (browsers.length === 0) return
808
+
809
+ const activeRumBrowser = await detectActiveRumBrowsers(false)
810
+ const testExecutionId = getRumTestExecutionId(activeRumBrowser || browsers[0], !!activeRumBrowser)
811
+ if (!testExecutionId) return
812
+
813
+ await correlateRumBrowsers(browsers, testExecutionId)
814
+ }
815
+
816
+ /**
817
+ * Reapplies RUM correlation for a native Jasmine retry and reports whether RUM is active.
818
+ *
819
+ * @param {string|undefined} testExecutionId
820
+ * @returns {Promise<{
821
+ * browserName: string|undefined,
822
+ * browserVersion: string|undefined,
823
+ * isRumActive: boolean
824
+ * }>}
825
+ */
826
+ async function retryRumBrowsers (testExecutionId) {
827
+ const browsers = [...rumCorrelationBrowsers]
828
+ let activeRumBrowser
829
+ if (testExecutionId) {
830
+ await correlateRumBrowsers(browsers, testExecutionId)
831
+ } else {
832
+ activeRumBrowser = await detectActiveRumBrowsers(false)
833
+ }
834
+
835
+ const browser = activeRumBrowser || browsers[0]
836
+ return {
837
+ browserName: browser?.capabilities?.browserName,
838
+ browserVersion: browser?.capabilities?.browserVersion,
839
+ isRumActive: !!activeRumBrowser,
840
+ }
841
+ }
842
+
843
+ /**
844
+ * Delays BiDi navigation until its RUM correlation preload is installed.
845
+ *
846
+ * @param {{rumPreloadCallback?: () => Promise<void>}} context
847
+ * @returns {void}
848
+ */
849
+ function waitForRumNavigationStart (context) {
850
+ if (!rumPageNavigateCh.hasSubscribers) return
851
+ context.rumPreloadCallback = preloadRumNavigation
852
+ }
853
+
854
+ /**
855
+ * Makes a promise-returning RUM operation compatible with the rewriter's completion callbacks.
856
+ *
857
+ * @param {{
858
+ * resolveCallback?: (onDone: () => void) => void,
859
+ * rejectCallback?: (onDone: () => void) => void
860
+ * }} context
861
+ * @param {() => Promise<unknown>} operation
862
+ * @param {boolean} [waitForResolve]
863
+ * @returns {void}
864
+ */
865
+ function setRumWaitCallbacks (context, operation, waitForResolve = true) {
866
+ const waitForOperation = onDone => operation().then(onDone, onDone)
867
+ if (waitForResolve) context.resolveCallback = waitForOperation
868
+ context.rejectCallback = waitForOperation
869
+ }
870
+
871
+ /**
872
+ * Delays URL completion until RUM detection and correlation settle.
873
+ *
874
+ * @param {object} context
875
+ * @returns {void}
876
+ */
877
+ function waitForRumNavigation (context) {
878
+ if (!rumPageNavigateCh.hasSubscribers) return
879
+ setRumWaitCallbacks(context, () => handleRumNavigation(context))
880
+ }
881
+
882
+ /**
883
+ * Detects delayed RUM at test and afterEach completion.
884
+ * Correlation remains active until every afterEach hook has run.
885
+ *
886
+ * @param {{arguments?: unknown[]}} context
887
+ * @returns {void}
888
+ */
889
+ function waitForRumCleanup (context) {
890
+ const type = context.arguments?.[1]
891
+ const hookName = context.arguments?.[7]
892
+ if (rumCorrelationBrowsers.size === 0) return
893
+
894
+ if (type === 'Test') {
895
+ isRumCleanupPending = true
896
+ setRumWaitCallbacks(context, detectActiveRumBrowsers)
897
+ } else if (type === 'Hook' && hookName === 'afterEach') {
898
+ setRumWaitCallbacks(context, detectActiveRumBrowsers)
899
+ }
900
+ }
901
+
902
+ /**
903
+ * Detects delayed RUM after test or afterEach failures.
904
+ * Correlation remains active until every afterEach hook has run.
905
+ *
906
+ * @param {{arguments?: unknown[]}} context
907
+ * @returns {void}
908
+ */
909
+ function waitForFailedRumCleanup (context) {
910
+ const type = context.arguments?.[1]
911
+ const hookName = context.arguments?.[7]
912
+ if (rumCorrelationBrowsers.size === 0) return
913
+
914
+ if (type === 'Test') {
915
+ isRumCleanupPending = true
916
+ setRumWaitCallbacks(context, detectActiveRumBrowsers, false)
917
+ } else if (type === 'Hook' && hookName === 'afterEach') {
918
+ setRumWaitCallbacks(context, detectActiveRumBrowsers, false)
919
+ } else {
920
+ isRumCleanupPending = true
921
+ if (type === 'Hook' && hookName === 'beforeEach') {
922
+ setRumWaitCallbacks(context, detectActiveRumBrowsers, false)
923
+ }
924
+ }
925
+ }
926
+
927
+ /**
928
+ * Prepares retained RUM state before the next test or non-afterEach hook starts.
929
+ *
930
+ * @param {{arguments?: unknown[], rumCleanupCallback?: () => Promise<void>}} context
931
+ * @returns {void}
932
+ */
933
+ function waitForRumTestStart (context) {
934
+ const type = context.arguments?.[1]
935
+ const hookName = context.arguments?.[7]
936
+ if (type === 'Hook' && hookName === 'afterEach') return
937
+
938
+ const startsTest = type === 'Test' || (type === 'Hook' && hookName === 'beforeEach')
939
+ if (startsTest) {
940
+ context.rumCleanupCallback = isRumCleanupPending ? prepareNextRumTest : startRumTest
941
+ } else if (isRumCleanupPending) {
942
+ context.rumCleanupCallback = cleanupRumBrowsers
943
+ }
944
+ }
945
+
79
946
  /**
80
947
  * @typedef {object} WebdriverioRunnerConfig
81
948
  * @property {string} framework
@@ -158,6 +1025,7 @@ function createWorkerConfiguration () {
158
1025
  isImpactedTestsEnabled: false,
159
1026
  isItrEnabled: false,
160
1027
  isKnownTestsEnabled: false,
1028
+ isTestFailureScreenshotsEnabled: false,
161
1029
  isSuitesSkippingEnabled: false,
162
1030
  isTestDynamicInstrumentationEnabled: false,
163
1031
  isTestManagementTestsEnabled: false,
@@ -476,6 +1344,10 @@ function configureCoordinator (state, response) {
476
1344
  repositoryRoot,
477
1345
  } = response || {}
478
1346
 
1347
+ const screenshotCapabilities = {}
1348
+ screenshotCapabilitiesCh.publish(screenshotCapabilities)
1349
+ configuration.isTestFailureScreenshotsEnabled = screenshotCapabilities.enabled === true
1350
+
479
1351
  configuration.repositoryRoot = repositoryRoot
480
1352
  if (err || !libraryConfig) {
481
1353
  completeCoordinatorInitialization(state)
@@ -841,6 +1713,28 @@ function handleWorkerMessage (state, workerRecord, message) {
841
1713
  handleConfigurationRequest(state, workerRecord, message)
842
1714
  return
843
1715
  }
1716
+ if (message.name === SCREENSHOT_UPLOAD) {
1717
+ const { requestId, ...content } = message.content || {}
1718
+ const respond = (error) => sendWorkerMessage(workerRecord, {
1719
+ origin: 'datadog',
1720
+ name: SCREENSHOT_UPLOAD_RESPONSE,
1721
+ content: {
1722
+ error: error?.message,
1723
+ requestId,
1724
+ },
1725
+ })
1726
+ if (!requestId || !screenshotUploadCh.hasSubscribers) {
1727
+ respond(new Error('WebdriverIO screenshot upload is not available'))
1728
+ } else {
1729
+ try {
1730
+ screenshotUploadCh.publish({ ...content, onDone: respond })
1731
+ } catch (error) {
1732
+ log.error('WebdriverIO screenshot upload error: %s', error?.message || String(error))
1733
+ respond(error)
1734
+ }
1735
+ }
1736
+ return
1737
+ }
844
1738
  if (message.name === SUITE_FINISH) {
845
1739
  handleSuiteResults(workerRecord, message)
846
1740
  return
@@ -1000,8 +1894,99 @@ function finishCoordinator (state, error, onDone) {
1000
1894
  })
1001
1895
  }
1002
1896
 
1897
+ /**
1898
+ * Delays WebdriverIO worker exit until pending log-submission requests settle.
1899
+ *
1900
+ * @param {{
1901
+ * resolveCallback?: (onDone: () => void) => void,
1902
+ * rejectCallback?: (onDone: () => void) => void
1903
+ * }} context
1904
+ * @returns {void}
1905
+ */
1906
+ function waitForLogSubmissionAtWorkerExit (context) {
1907
+ const shouldCleanRum = rumBrowsers.size > 0
1908
+ const shouldFlushLogs = isWebdriverioWorker && logSubmissionFlushCh.hasSubscribers
1909
+ if (!shouldCleanRum && !shouldFlushLogs) return
1910
+
1911
+ const waitForExit = onDone => {
1912
+ const finish = () => shouldFlushLogs
1913
+ ? publishWithCompletion(logSubmissionFlushCh, {}, onDone)
1914
+ : onDone()
1915
+ if (shouldCleanRum) {
1916
+ cleanupAllRumBrowsers().then(finish, finish)
1917
+ } else {
1918
+ finish()
1919
+ }
1920
+ }
1921
+ context.resolveCallback = waitForExit
1922
+ context.rejectCallback = waitForExit
1923
+ }
1924
+
1925
+ /**
1926
+ * Cleans retained RUM browsers before WebdriverIO deletes the worker session.
1927
+ *
1928
+ * @param {{rumCleanupCallback?: () => Promise<void>}} context
1929
+ * @returns {void}
1930
+ */
1931
+ function waitForRumCleanupBeforeSessionEnd (context) {
1932
+ context.rumCleanupCallback = cleanupAllRumBrowsers
1933
+ }
1934
+
1935
+ baseReporterWaitForSyncCh.asyncEnd.subscribe(
1936
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForLogSubmissionAtWorkerExit)
1937
+ )
1938
+
1939
+ runnerRunCh.start.subscribe(
1940
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForRumCleanupBeforeSessionEnd)
1941
+ )
1942
+
1943
+ urlCh.start.subscribe(
1944
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForRumNavigationStart)
1945
+ )
1946
+
1947
+ urlCh.asyncEnd.subscribe(
1948
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForRumNavigation)
1949
+ )
1950
+
1951
+ newWindowCh.start.subscribe(
1952
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForRumNavigationStart)
1953
+ )
1954
+
1955
+ newWindowCh.asyncEnd.subscribe(
1956
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForRumNavigation)
1957
+ )
1958
+
1959
+ webdriverCommandCh.end.subscribe(
1960
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (wrapRumProtocolCommand)
1961
+ )
1962
+
1963
+ webdriverBidiInitCh.end.subscribe(
1964
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (wrapRumBidiNavigation)
1965
+ )
1966
+
1967
+ testFrameworkFnWrapperCh.asyncEnd.subscribe(
1968
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForRumCleanup)
1969
+ )
1970
+
1971
+ testFrameworkFnWrapperCh.start.subscribe(
1972
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForRumTestStart)
1973
+ )
1974
+
1975
+ testFrameworkFnWrapperCh.error.subscribe(
1976
+ /** @type {import('node:diagnostics_channel').ChannelListener} */ (waitForFailedRumCleanup)
1977
+ )
1978
+
1003
1979
  // dc-polyfill supports partial tracing-channel subscribers, unlike the Node.js type definition.
1004
1980
  // @ts-expect-error
1981
+ executeAsyncCh.subscribe({
1982
+ start (context) {
1983
+ context.rumStartCallback = startRumTest
1984
+ context.rumRetryCallback = retryRumBrowsers
1985
+ context.retryCallback ??= retryRumTest
1986
+ },
1987
+ })
1988
+
1989
+ // @ts-expect-error See the partial tracing-channel subscriber above.
1005
1990
  jasmineAdapterInitCh.subscribe({
1006
1991
  asyncEnd (context) {
1007
1992
  initializeJasmineWorker(context.result || context.self, context)