dd-trace 6.7.0 → 6.9.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 (272) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +14 -8
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/playwright.js +5 -5
  20. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  21. package/ci/test-optimization-validation/generated-files.js +47 -37
  22. package/ci/test-optimization-validation/generated-test-contract.js +9 -7
  23. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  24. package/ci/test-optimization-validation/literal-glob.js +52 -0
  25. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  26. package/ci/test-optimization-validation/manifest-scaffold.js +301 -52
  27. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  28. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  29. package/ci/test-optimization-validation/offline-output.js +6 -6
  30. package/ci/test-optimization-validation/package-check.js +94 -0
  31. package/ci/test-optimization-validation/plan-writer.js +183 -10
  32. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  33. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  34. package/ci/test-optimization-validation/redaction.js +18 -27
  35. package/ci/test-optimization-validation/report-writer.js +182 -48
  36. package/ci/test-optimization-validation/result-semantics.js +20 -3
  37. package/ci/test-optimization-validation/runner-command.js +41 -21
  38. package/ci/test-optimization-validation/runner-contract.js +79 -43
  39. package/ci/test-optimization-validation/safe-files.js +4 -4
  40. package/ci/test-optimization-validation/scenarios/basic-reporting.js +60 -6
  41. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  42. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  43. package/ci/test-optimization-validation/scenarios/test-management.js +2 -2
  44. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  45. package/ci/test-optimization-validation/test-output.js +2 -1
  46. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  47. package/ci/vitest-no-worker-init-setup.mjs +377 -84
  48. package/index.d.ts +175 -14
  49. package/init.js +1 -17
  50. package/initialize.mjs +11 -0
  51. package/loader-hook.mjs +6 -4
  52. package/package.json +12 -4
  53. package/packages/datadog-core/src/utils/src/kebabcase.js +8 -2
  54. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  55. package/packages/datadog-esbuild/index.js +4 -2
  56. package/packages/datadog-esbuild/src/utils.js +17 -2
  57. package/packages/datadog-instrumentations/src/ai.js +61 -34
  58. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +85 -9
  60. package/packages/datadog-instrumentations/src/cucumber.js +131 -51
  61. package/packages/datadog-instrumentations/src/dns.js +21 -2
  62. package/packages/datadog-instrumentations/src/electron.js +1 -0
  63. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  64. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  65. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  66. package/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +10 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +19 -2
  68. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  69. package/packages/datadog-instrumentations/src/helpers/register.js +9 -3
  70. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
  71. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  72. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +175 -0
  73. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  74. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  75. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
  76. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +50 -18
  77. package/packages/datadog-instrumentations/src/helpers/router-helper.js +38 -7
  78. package/packages/datadog-instrumentations/src/helpers/shared-utils.js +39 -0
  79. package/packages/datadog-instrumentations/src/http/client.js +1 -3
  80. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  81. package/packages/datadog-instrumentations/src/jest.js +986 -246
  82. package/packages/datadog-instrumentations/src/mocha/common.js +1 -3
  83. package/packages/datadog-instrumentations/src/mocha/main.js +109 -51
  84. package/packages/datadog-instrumentations/src/mocha/utils.js +120 -32
  85. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +44 -0
  86. package/packages/datadog-instrumentations/src/mocha/worker.js +441 -43
  87. package/packages/datadog-instrumentations/src/moleculer/client.js +1 -1
  88. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  89. package/packages/datadog-instrumentations/src/next.js +155 -23
  90. package/packages/datadog-instrumentations/src/openai.js +14 -0
  91. package/packages/datadog-instrumentations/src/pg.js +180 -6
  92. package/packages/datadog-instrumentations/src/playwright.js +176 -81
  93. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  94. package/packages/datadog-instrumentations/src/promise.js +3 -3
  95. package/packages/datadog-instrumentations/src/rhea.js +1 -1
  96. package/packages/datadog-instrumentations/src/router.js +195 -19
  97. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +567 -65
  98. package/packages/datadog-instrumentations/src/vitest-main.js +438 -54
  99. package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
  100. package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
  101. package/packages/datadog-instrumentations/src/webdriverio.js +1090 -0
  102. package/packages/datadog-instrumentations/src/when.js +3 -3
  103. package/packages/datadog-instrumentations/src/ws.js +5 -1
  104. package/packages/datadog-plugin-aerospike/src/index.js +1 -3
  105. package/packages/datadog-plugin-amqplib/src/client.js +1 -1
  106. package/packages/datadog-plugin-amqplib/src/producer.js +1 -1
  107. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  108. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +2 -2
  109. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +2 -2
  110. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -3
  111. package/packages/datadog-plugin-azure-service-bus/src/producer.js +5 -3
  112. package/packages/datadog-plugin-bullmq/src/producer.js +3 -3
  113. package/packages/datadog-plugin-child_process/src/index.js +3 -3
  114. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -11
  115. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  116. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
  117. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -1
  118. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  119. package/packages/datadog-plugin-electron/src/net.js +10 -4
  120. package/packages/datadog-plugin-fastify/src/tracing.js +1 -1
  121. package/packages/datadog-plugin-fs/src/index.js +0 -4
  122. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +14 -7
  123. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +19 -10
  124. package/packages/datadog-plugin-graphql/src/execute.js +1 -0
  125. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -1
  126. package/packages/datadog-plugin-http/src/client.js +3 -2
  127. package/packages/datadog-plugin-http2/src/client.js +1 -1
  128. package/packages/datadog-plugin-jest/src/index.js +3 -21
  129. package/packages/datadog-plugin-jest/src/util.js +4 -1
  130. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
  131. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  132. package/packages/datadog-plugin-mocha/src/index.js +342 -6
  133. package/packages/datadog-plugin-next/src/index.js +57 -9
  134. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  135. package/packages/datadog-plugin-pg/src/index.js +65 -1
  136. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  137. package/packages/datadog-plugin-prisma/src/index.js +5 -1
  138. package/packages/datadog-plugin-rhea/src/consumer.js +16 -13
  139. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  140. package/packages/datadog-plugin-undici/src/index.js +2 -1
  141. package/packages/datadog-plugin-vitest/src/index.js +179 -18
  142. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  143. package/packages/datadog-webpack/index.js +2 -2
  144. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  145. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  146. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -1
  147. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  148. package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
  149. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  150. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  151. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  152. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +1 -1
  153. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +1 -1
  154. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  155. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +1 -1
  156. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +1 -3
  157. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +2 -2
  158. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +2 -2
  159. package/packages/dd-trace/src/appsec/index.js +48 -476
  160. package/packages/dd-trace/src/appsec/sdk/track_event.js +10 -1
  161. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  162. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  163. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  164. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +7 -6
  165. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +29 -15
  166. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +19 -1
  167. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +25 -1
  168. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/webdriverio.js +26 -0
  169. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -1
  170. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  171. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  172. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -3
  173. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  174. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +2 -2
  175. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  176. package/packages/dd-trace/src/config/defaults.js +14 -6
  177. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -2
  178. package/packages/dd-trace/src/config/helper.js +2 -0
  179. package/packages/dd-trace/src/config/index.js +31 -5
  180. package/packages/dd-trace/src/config/parsers.js +19 -1
  181. package/packages/dd-trace/src/config/remote_config.js +1 -1
  182. package/packages/dd-trace/src/config/supported-configurations.json +13 -6
  183. package/packages/dd-trace/src/datastreams/encoding.js +3 -4
  184. package/packages/dd-trace/src/debugger/devtools_client/log.js +2 -1
  185. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +1 -4
  186. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -2
  187. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +2 -2
  188. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +16 -2
  189. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  190. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  191. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  192. package/packages/dd-trace/src/exporters/common/url.js +3 -5
  193. package/packages/dd-trace/src/exporters/electron/index.js +4 -1
  194. package/packages/dd-trace/src/external-logger/src/index.js +1 -2
  195. package/packages/dd-trace/src/flare/index.js +2 -2
  196. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  197. package/packages/dd-trace/src/guardrails/log.js +1 -10
  198. package/packages/dd-trace/src/id.js +1 -1
  199. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -1
  200. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  201. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -2
  202. package/packages/dd-trace/src/llmobs/experiments/dataset.js +66 -14
  203. package/packages/dd-trace/src/llmobs/experiments/experiment.js +289 -95
  204. package/packages/dd-trace/src/llmobs/experiments/index.js +66 -19
  205. package/packages/dd-trace/src/llmobs/experiments/noop.js +95 -10
  206. package/packages/dd-trace/src/llmobs/experiments/result.js +13 -2
  207. package/packages/dd-trace/src/llmobs/experiments/util.js +220 -0
  208. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  209. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  210. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  211. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +1 -1
  212. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  213. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +1 -1
  214. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  215. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  216. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  217. package/packages/dd-trace/src/llmobs/sdk.js +152 -78
  218. package/packages/dd-trace/src/llmobs/span_processor.js +9 -7
  219. package/packages/dd-trace/src/llmobs/tagger.js +1 -2
  220. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  221. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  222. package/packages/dd-trace/src/llmobs/writers/spans.js +12 -6
  223. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  224. package/packages/dd-trace/src/log/channels.js +1 -9
  225. package/packages/dd-trace/src/log/levels.js +12 -0
  226. package/packages/dd-trace/src/noop/proxy.js +2 -2
  227. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  228. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  229. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  230. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  231. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  232. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +3 -2
  233. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  234. package/packages/dd-trace/src/opentracing/span.js +1 -1
  235. package/packages/dd-trace/src/opentracing/span_context.js +1 -1
  236. package/packages/dd-trace/src/plugins/ci_plugin.js +124 -23
  237. package/packages/dd-trace/src/plugins/index.js +2 -0
  238. package/packages/dd-trace/src/plugins/plugin.js +1 -1
  239. package/packages/dd-trace/src/plugins/tracing.js +1 -0
  240. package/packages/dd-trace/src/plugins/util/ci.js +13 -6
  241. package/packages/dd-trace/src/plugins/util/git.js +7 -5
  242. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +3 -3
  243. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  244. package/packages/dd-trace/src/plugins/util/llm.js +2 -2
  245. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  246. package/packages/dd-trace/src/plugins/util/tags.js +2 -0
  247. package/packages/dd-trace/src/plugins/util/test.js +356 -86
  248. package/packages/dd-trace/src/plugins/util/url.js +5 -5
  249. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  250. package/packages/dd-trace/src/plugins/util/web.js +24 -2
  251. package/packages/dd-trace/src/priority_sampler.js +6 -3
  252. package/packages/dd-trace/src/profiler.js +1 -1
  253. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  254. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  255. package/packages/dd-trace/src/profiling/profilers/poisson.js +1 -1
  256. package/packages/dd-trace/src/profiling/profilers/shared.js +2 -2
  257. package/packages/dd-trace/src/proxy.js +1 -0
  258. package/packages/dd-trace/src/ritm.js +10 -20
  259. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  260. package/packages/dd-trace/src/sampling_rule.js +4 -2
  261. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  262. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  263. package/packages/dd-trace/src/startup-log.js +3 -0
  264. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  265. package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -1
  266. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  267. package/packages/dd-trace/src/util.js +16 -0
  268. package/vendor/dist/@apm-js-collab/code-transformer/index.js +12 -12
  269. package/vendor/dist/pprof-format/index.js +1 -1
  270. package/vendor/dist/source-map/index.js +1 -1
  271. package/version.js +10 -8
  272. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -0,0 +1,1090 @@
1
+ 'use strict'
2
+
3
+ const { AsyncResource } = require('node:async_hooks')
4
+ const { fileURLToPath } = require('node:url')
5
+
6
+ const { EMPTY_EFD_RETRY_POLICY } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
7
+ const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
8
+ const log = require('../../dd-trace/src/log')
9
+ const {
10
+ collectTestOptimizationSummariesFromTraces,
11
+ getIsFaultyEarlyFlakeDetection,
12
+ getTestSuitePath,
13
+ logTestOptimizationSummary,
14
+ MOCHA_WORKER_LOGS_PAYLOAD_CODE,
15
+ MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE,
16
+ MOCHA_WORKER_TRACE_PAYLOAD_CODE,
17
+ TEST_SUITE_EXECUTION_ID,
18
+ } = require('../../dd-trace/src/plugins/util/test')
19
+ const { addHook, channel, tracingChannel } = require('./helpers/instrument')
20
+ const {
21
+ CONFIGURATION_REQUEST,
22
+ CONFIGURATION_RESPONSE,
23
+ sendWebdriverioWorkerMessage,
24
+ SUITE_FINISH,
25
+ WEBDRIVERIO_WORKER_ENV,
26
+ WEBDRIVERIO_WORKER_EVENT,
27
+ WEBDRIVERIO_WORKER_ORIGIN,
28
+ WORKER_READY,
29
+ WORKER_READY_RESPONSE,
30
+ } = require('./mocha/webdriverio-protocol')
31
+
32
+ const jasmineDoneCh = channel('ci:webdriverio:jasmine:done')
33
+ const testFinishCh = channel('ci:mocha:test:finish')
34
+ const testSessionStartCh = channel('ci:mocha:session:start')
35
+ const testSessionFinishCh = channel('ci:mocha:session:finish')
36
+ const testSuiteErrorCh = channel('ci:mocha:test-suite:error')
37
+ const testSuiteStartCh = channel('ci:mocha:test-suite:start')
38
+ const testSuiteFinishCh = channel('ci:mocha:test-suite:finish')
39
+ const knownTestsCh = channel('ci:mocha:known-tests')
40
+ const libraryConfigurationCh = channel('ci:mocha:library-configuration')
41
+ const modifiedFilesCh = channel('ci:mocha:modified-files')
42
+ const testManagementTestsCh = channel('ci:mocha:test-management-tests')
43
+ const workerConfigurationCh = channel('ci:mocha:worker:configuration')
44
+ const workerReportLogsCh = channel('ci:mocha:worker-report:logs')
45
+ const workerReportTelemetryCh = channel('ci:mocha:worker-report:telemetry')
46
+ const workerReportTraceCh = channel('ci:mocha:worker-report:trace')
47
+
48
+ const jasmineAdapterInitCh = tracingChannel('orchestrion:@wdio/jasmine-framework:JasmineAdapter_init')
49
+ const launcherStartInstanceCh = tracingChannel('orchestrion:@wdio/cli:Launcher_startInstance')
50
+ const localRunnerRunCh = tracingChannel('orchestrion:@wdio/local-runner:LocalRunner_run')
51
+ const localRunnerShutdownCh = tracingChannel('orchestrion:@wdio/local-runner:LocalRunner_shutdown')
52
+
53
+ const NODE_OPTIONS_SEPARATOR_RE = /\s/
54
+ const JASMINE_FRAMEWORK_ADAPTER = 'jasmine'
55
+ const MOCHA_FRAMEWORK_ADAPTER = 'mocha'
56
+ const SUPPORTED_FRAMEWORK_ADAPTERS = new Set([JASMINE_FRAMEWORK_ADAPTER, MOCHA_FRAMEWORK_ADAPTER])
57
+ const TEST_FRAMEWORK = 'webdriverio'
58
+ const isWebdriverioWorker = !!getEnvironmentVariable(WEBDRIVERIO_WORKER_ENV)
59
+ let jasmineWorkerRequestId = 0
60
+
61
+ const loadCh = channel('dd-trace:instrumentation:load')
62
+ if (loadCh.hasSubscribers) {
63
+ loadCh.publish({ name: '@wdio/local-runner' })
64
+ }
65
+
66
+ const coordinatorStates = new WeakMap()
67
+ const localRunnerVersions = new WeakMap()
68
+
69
+ addHook({
70
+ name: '@wdio/local-runner',
71
+ versions: ['>=9.0.0'],
72
+ file: 'build/index.js',
73
+ patchDefault: true,
74
+ }, (LocalRunner, version) => {
75
+ localRunnerVersions.set(LocalRunner, version)
76
+ return LocalRunner
77
+ })
78
+
79
+ /**
80
+ * @typedef {object} WebdriverioRunnerConfig
81
+ * @property {string} framework
82
+ * @property {string|undefined} rootDir
83
+ * @property {typeof process.env|undefined} runnerEnv
84
+ */
85
+
86
+ /**
87
+ * @typedef {object} WebdriverioLocalRunner
88
+ * @property {WebdriverioRunnerConfig|undefined} config
89
+ * @property {WebdriverioRunnerConfig|undefined} _config
90
+ */
91
+
92
+ /**
93
+ * @typedef {object} WorkerRecord
94
+ * @property {object} worker
95
+ * @property {string[]} specs
96
+ * @property {Map<string, WebdriverioSuiteResult>} reportedSuiteResults
97
+ * @property {Map<string, WebdriverioSuiteContext>} suiteContexts
98
+ * @property {string} testSuiteExecutionId
99
+ * @property {boolean|undefined} hasTests
100
+ * @property {number|undefined} exitCode
101
+ * @property {number|undefined} retries
102
+ */
103
+
104
+ /**
105
+ * @typedef {object} WebdriverioSuiteContext
106
+ * @property {object|undefined} currentStore
107
+ * @property {Error|undefined} error
108
+ * @property {string|undefined} status
109
+ * @property {string} testSuiteAbsolutePath
110
+ * @property {string} testSuiteExecutionId
111
+ */
112
+
113
+ /**
114
+ * @typedef {object} WebdriverioSuiteResult
115
+ * @property {{message?: string, stack?: string}|undefined} error
116
+ * @property {string|undefined} status
117
+ */
118
+
119
+ /**
120
+ * @typedef {object} CoordinatorState
121
+ * @property {WebdriverioLocalRunner} localRunner
122
+ * @property {AsyncResource} asyncResource
123
+ * @property {object} configuration
124
+ * @property {boolean} initialized
125
+ * @property {boolean} initializing
126
+ * @property {Array<(configuration: object) => void>} initializationCallbacks
127
+ * @property {boolean} sessionStarted
128
+ * @property {boolean} finished
129
+ * @property {string|undefined} frameworkVersion
130
+ * @property {string} testFrameworkAdapter
131
+ * @property {number} activeWorkers
132
+ * @property {Map<string, object>} attemptToFixExecutions
133
+ * @property {number} maxActiveWorkers
134
+ * @property {Set<string>} newTestsWithDynamicNames
135
+ * @property {number} nextWorkerId
136
+ * @property {Set<string>} scheduledFiles
137
+ * @property {Map<string, object>} testManagementExecutions
138
+ * @property {unknown} runError
139
+ * @property {Set<WorkerRecord>} workers
140
+ * @property {Map<object, string>} suiteStatuses
141
+ */
142
+
143
+ /**
144
+ * Creates the configuration consumed by WebdriverIO workers.
145
+ *
146
+ * @returns {object}
147
+ */
148
+ function createWorkerConfiguration () {
149
+ return {
150
+ earlyFlakeDetectionFaultyThreshold: 30,
151
+ earlyFlakeDetectionRetryPolicy: EMPTY_EFD_RETRY_POLICY,
152
+ flakyTestRetriesCount: 0,
153
+ isCodeCoverageEnabled: false,
154
+ isCoverageReportUploadEnabled: false,
155
+ isDiEnabled: false,
156
+ isEarlyFlakeDetectionEnabled: false,
157
+ isFlakyTestRetriesEnabled: false,
158
+ isImpactedTestsEnabled: false,
159
+ isItrEnabled: false,
160
+ isKnownTestsEnabled: false,
161
+ isSuitesSkippingEnabled: false,
162
+ isTestDynamicInstrumentationEnabled: false,
163
+ isTestManagementTestsEnabled: false,
164
+ knownTests: {},
165
+ modifiedFiles: [],
166
+ repositoryRoot: undefined,
167
+ testFramework: TEST_FRAMEWORK,
168
+ testManagementAttemptToFixRetries: 0,
169
+ testManagementTests: {},
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Configures a Jasmine worker for reporting-only execution and notifies its coordinator.
175
+ *
176
+ * @param {{_jrunner?: {env?: {addReporter?: (reporter: object) => void}}, _specs?: string[]}} adapter
177
+ * @param {{
178
+ * resolveCallback?: (onDone: () => void) => void,
179
+ * rejectCallback?: (onDone: () => void) => void
180
+ * }} context
181
+ * @returns {void}
182
+ */
183
+ function initializeJasmineWorker (adapter, context) {
184
+ if (!isWebdriverioWorker) {
185
+ return
186
+ }
187
+
188
+ workerConfigurationCh.publish({
189
+ libraryConfig: createWorkerConfiguration(),
190
+ repositoryRoot: undefined,
191
+ specs: adapter?._specs || [],
192
+ testFramework: TEST_FRAMEWORK,
193
+ testFrameworkAdapter: JASMINE_FRAMEWORK_ADAPTER,
194
+ })
195
+
196
+ if (jasmineDoneCh.hasSubscribers) {
197
+ adapter?._jrunner?.env?.addReporter?.({
198
+ /**
199
+ * Publishes run-level failures that are absent from suiteDone.
200
+ *
201
+ * @param {object} result
202
+ * @returns {void}
203
+ */
204
+ jasmineDone (result) {
205
+ jasmineDoneCh.publish({ result })
206
+ },
207
+ })
208
+ }
209
+
210
+ /**
211
+ * Waits until the coordinator has started this worker's parent spans.
212
+ *
213
+ * @param {() => void} onDone
214
+ * @returns {void}
215
+ */
216
+ const waitForCoordinator = onDone => {
217
+ const requestId = `${process.pid}-${++jasmineWorkerRequestId}`
218
+ let finished = false
219
+
220
+ /**
221
+ * Releases Jasmine initialization exactly once.
222
+ *
223
+ * @returns {void}
224
+ */
225
+ function finish () {
226
+ if (finished) {
227
+ return
228
+ }
229
+ finished = true
230
+ clearTimeout(timeout)
231
+ process.off('message', onMessage)
232
+ process.off('disconnect', finish)
233
+ onDone()
234
+ }
235
+
236
+ /**
237
+ * Receives coordinator readiness for this worker.
238
+ *
239
+ * @param {object} message
240
+ * @returns {void}
241
+ */
242
+ function onMessage (message) {
243
+ if (message?.name === WORKER_READY_RESPONSE && message.content?.requestId === requestId) {
244
+ finish()
245
+ }
246
+ }
247
+
248
+ const timeout = setTimeout(finish, 30_000)
249
+ process.on('message', onMessage)
250
+ process.once('disconnect', finish)
251
+ sendWebdriverioWorkerMessage({
252
+ origin: 'datadog',
253
+ name: WORKER_READY,
254
+ content: {
255
+ requestId,
256
+ testFrameworkAdapter: JASMINE_FRAMEWORK_ADAPTER,
257
+ },
258
+ }, error => {
259
+ if (error) {
260
+ log.error('WebdriverIO Test Optimization IPC error', error)
261
+ }
262
+ finish()
263
+ })
264
+ }
265
+
266
+ context.resolveCallback = waitForCoordinator
267
+ context.rejectCallback = waitForCoordinator
268
+ }
269
+
270
+ /**
271
+ * Gets the public runner configuration, or the private equivalent used by older releases.
272
+ *
273
+ * @param {WebdriverioLocalRunner} localRunner
274
+ * @returns {WebdriverioRunnerConfig|undefined}
275
+ */
276
+ function getRunnerConfiguration (localRunner) {
277
+ return localRunner.config || localRunner._config
278
+ }
279
+
280
+ /**
281
+ * Gets or creates coordinator state for a WebdriverIO local runner.
282
+ *
283
+ * @param {WebdriverioLocalRunner} localRunner
284
+ * @returns {CoordinatorState}
285
+ */
286
+ function getCoordinatorState (localRunner) {
287
+ let state = coordinatorStates.get(localRunner)
288
+ if (state) {
289
+ return state
290
+ }
291
+
292
+ state = {
293
+ localRunner,
294
+ asyncResource: new AsyncResource('dd-trace-webdriverio-coordinator'),
295
+ configuration: createWorkerConfiguration(),
296
+ initialized: false,
297
+ initializing: false,
298
+ initializationCallbacks: [],
299
+ sessionStarted: false,
300
+ finished: false,
301
+ frameworkVersion: localRunnerVersions.get(localRunner.constructor),
302
+ testFrameworkAdapter: getRunnerConfiguration(localRunner)?.framework,
303
+ activeWorkers: 0,
304
+ attemptToFixExecutions: new Map(),
305
+ maxActiveWorkers: 0,
306
+ newTestsWithDynamicNames: new Set(),
307
+ nextWorkerId: 0,
308
+ scheduledFiles: new Set(),
309
+ testManagementExecutions: new Map(),
310
+ runError: undefined,
311
+ workers: new Set(),
312
+ suiteStatuses: new Map(),
313
+ }
314
+ coordinatorStates.set(localRunner, state)
315
+
316
+ return state
317
+ }
318
+
319
+ /**
320
+ * Normalizes a WebdriverIO spec identifier to a filesystem path.
321
+ *
322
+ * @param {string} file
323
+ * @returns {string}
324
+ */
325
+ function normalizeFile (file) {
326
+ return file.startsWith('file://') ? fileURLToPath(file) : file
327
+ }
328
+
329
+ /**
330
+ * Adds resolved WebdriverIO spec files to the coordinator's complete schedule.
331
+ *
332
+ * @param {CoordinatorState} state
333
+ * @param {string[]} files
334
+ * @returns {void}
335
+ */
336
+ function addScheduledFiles (state, files) {
337
+ for (const file of files) {
338
+ state.scheduledFiles.add(normalizeFile(file))
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Checks whether worker NODE_OPTIONS contain the complete launcher options.
344
+ *
345
+ * @param {string|undefined} workerNodeOptions
346
+ * @param {string} launcherNodeOptions
347
+ * @returns {boolean}
348
+ */
349
+ function includesNodeOptions (workerNodeOptions, launcherNodeOptions) {
350
+ if (!workerNodeOptions) {
351
+ return false
352
+ }
353
+
354
+ let index = workerNodeOptions.indexOf(launcherNodeOptions)
355
+ while (index !== -1) {
356
+ const endIndex = index + launcherNodeOptions.length
357
+ const startsAtBoundary = index === 0 || NODE_OPTIONS_SEPARATOR_RE.test(workerNodeOptions[index - 1])
358
+ const endsAtBoundary = endIndex === workerNodeOptions.length ||
359
+ NODE_OPTIONS_SEPARATOR_RE.test(workerNodeOptions[endIndex])
360
+
361
+ if (startsAtBoundary && endsAtBoundary) {
362
+ return true
363
+ }
364
+ index = workerNodeOptions.indexOf(launcherNodeOptions, index + 1)
365
+ }
366
+ return false
367
+ }
368
+
369
+ /**
370
+ * Starts the single test session owned by the WebdriverIO launcher.
371
+ *
372
+ * @param {CoordinatorState} state
373
+ * @returns {void}
374
+ */
375
+ function startSession (state) {
376
+ if (state.sessionStarted) {
377
+ return
378
+ }
379
+ const processArgv = process.argv.slice(2).join(' ')
380
+ const command = processArgv ? `wdio ${processArgv}` : 'wdio'
381
+ const rootDir = getRunnerConfiguration(state.localRunner)?.rootDir || process.cwd()
382
+
383
+ testSessionStartCh.publish({
384
+ command,
385
+ frameworkVersion: state.frameworkVersion,
386
+ rootDir,
387
+ testFramework: TEST_FRAMEWORK,
388
+ testFrameworkAdapter: state.testFrameworkAdapter,
389
+ })
390
+ state.sessionStarted = true
391
+ }
392
+
393
+ /**
394
+ * Completes coordinator initialization and releases waiting workers.
395
+ *
396
+ * @param {CoordinatorState} state
397
+ * @returns {void}
398
+ */
399
+ function completeCoordinatorInitialization (state) {
400
+ if (state.initialized) {
401
+ return
402
+ }
403
+
404
+ state.initialized = true
405
+ state.initializing = false
406
+ startSession(state)
407
+
408
+ const callbacks = state.initializationCallbacks
409
+ state.initializationCallbacks = []
410
+ for (const callback of callbacks) {
411
+ callback(state.configuration)
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Runs one coordinator-owned feature request.
417
+ *
418
+ * @param {CoordinatorState} state
419
+ * @param {import('node:diagnostics_channel').Channel} requestChannel
420
+ * @param {(response: object) => void} onDone
421
+ * @returns {void}
422
+ */
423
+ function requestCoordinatorData (state, requestChannel, onDone) {
424
+ if (!requestChannel.hasSubscribers) {
425
+ onDone({ err: new Error('Test optimization was not initialized correctly') })
426
+ return
427
+ }
428
+
429
+ try {
430
+ requestChannel.runStores({
431
+ onDone: response => state.asyncResource.runInAsyncScope(onDone, undefined, response),
432
+ }, () => {})
433
+ } catch (error) {
434
+ log.error('WebdriverIO Test Optimization feature request error', error)
435
+ onDone({ err: error })
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Maps backend framework data to the Mocha adapter's internal framework key.
441
+ *
442
+ * @param {object|undefined} frameworkData
443
+ * @returns {object|undefined}
444
+ */
445
+ function getMochaFrameworkData (frameworkData) {
446
+ return frameworkData?.[TEST_FRAMEWORK] ?? frameworkData?.mocha
447
+ }
448
+
449
+ /**
450
+ * Applies settings and requests the enabled non-TIA datasets once for the whole run.
451
+ *
452
+ * @param {CoordinatorState} state
453
+ * @param {object|undefined} response
454
+ * @returns {void}
455
+ */
456
+ function configureCoordinator (state, response) {
457
+ const { configuration } = state
458
+ const {
459
+ err,
460
+ isTestDynamicInstrumentationEnabled,
461
+ libraryConfig,
462
+ repositoryRoot,
463
+ } = response || {}
464
+
465
+ configuration.repositoryRoot = repositoryRoot
466
+ if (err || !libraryConfig) {
467
+ completeCoordinatorInitialization(state)
468
+ return
469
+ }
470
+
471
+ configuration.earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
472
+ configuration.earlyFlakeDetectionRetryPolicy = libraryConfig.earlyFlakeDetectionRetryPolicy ?? EMPTY_EFD_RETRY_POLICY
473
+ configuration.flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
474
+ configuration.isDiEnabled = libraryConfig.isDiEnabled
475
+ configuration.isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
476
+ configuration.isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
477
+ configuration.isImpactedTestsEnabled = libraryConfig.isImpactedTestsEnabled
478
+ configuration.isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
479
+ configuration.isTestDynamicInstrumentationEnabled = isTestDynamicInstrumentationEnabled
480
+ configuration.isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
481
+ configuration.testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries
482
+
483
+ let pendingRequests = 1
484
+
485
+ /**
486
+ * Completes initialization after all enabled requests settle.
487
+ *
488
+ * @returns {void}
489
+ */
490
+ function finishRequest () {
491
+ pendingRequests--
492
+ if (pendingRequests === 0) {
493
+ completeCoordinatorInitialization(state)
494
+ }
495
+ }
496
+
497
+ if (configuration.isKnownTestsEnabled) {
498
+ pendingRequests++
499
+ requestCoordinatorData(state, knownTestsCh, ({ err, knownTests } = {}) => {
500
+ const mochaKnownTests = getMochaFrameworkData(knownTests)
501
+ if (err || !mochaKnownTests) {
502
+ configuration.isEarlyFlakeDetectionEnabled = false
503
+ configuration.isKnownTestsEnabled = false
504
+ } else {
505
+ configuration.knownTests = { mocha: mochaKnownTests }
506
+ }
507
+ finishRequest()
508
+ })
509
+ }
510
+
511
+ if (configuration.isTestManagementTestsEnabled) {
512
+ pendingRequests++
513
+ requestCoordinatorData(state, testManagementTestsCh, ({ err, testManagementTests } = {}) => {
514
+ if (err) {
515
+ configuration.isTestManagementTestsEnabled = false
516
+ configuration.testManagementAttemptToFixRetries = 0
517
+ } else {
518
+ const mochaTestManagementTests = getMochaFrameworkData(testManagementTests) || { suites: {} }
519
+ configuration.testManagementTests = { mocha: mochaTestManagementTests }
520
+ }
521
+ finishRequest()
522
+ })
523
+ }
524
+
525
+ if (configuration.isImpactedTestsEnabled) {
526
+ pendingRequests++
527
+ requestCoordinatorData(state, modifiedFilesCh, ({ err, modifiedFiles } = {}) => {
528
+ if (err) {
529
+ configuration.isImpactedTestsEnabled = false
530
+ } else {
531
+ configuration.modifiedFiles = modifiedFiles
532
+ }
533
+ finishRequest()
534
+ })
535
+ }
536
+
537
+ finishRequest()
538
+ }
539
+
540
+ /**
541
+ * Requests settings once so the launcher owns initialization for every worker.
542
+ *
543
+ * @param {CoordinatorState} state
544
+ * @param {(configuration: object) => void} [onDone]
545
+ * @returns {void}
546
+ */
547
+ function initializeCoordinator (state, onDone) {
548
+ if (state.initialized) {
549
+ onDone?.(state.configuration)
550
+ return
551
+ }
552
+ if (onDone) {
553
+ state.initializationCallbacks.push(onDone)
554
+ }
555
+ if (state.initializing) {
556
+ return
557
+ }
558
+
559
+ state.initializing = true
560
+ if (!libraryConfigurationCh.hasSubscribers) {
561
+ completeCoordinatorInitialization(state)
562
+ return
563
+ }
564
+
565
+ try {
566
+ libraryConfigurationCh.runStores({
567
+ basicReportingOnly: state.testFrameworkAdapter === JASMINE_FRAMEWORK_ADAPTER,
568
+ disableTestImpactAnalysis: true,
569
+ frameworkVersion: state.frameworkVersion,
570
+ isParallel: state.maxActiveWorkers > 1,
571
+ testFramework: TEST_FRAMEWORK,
572
+ onDone: response => state.asyncResource.runInAsyncScope(
573
+ configureCoordinator,
574
+ undefined,
575
+ state,
576
+ response
577
+ ),
578
+ }, () => {})
579
+ } catch (error) {
580
+ log.error('WebdriverIO Test Optimization configuration error', error)
581
+ completeCoordinatorInitialization(state)
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Starts suites for one worker.
587
+ *
588
+ * @param {WorkerRecord} workerRecord
589
+ * @param {string[]} files
590
+ * @returns {void}
591
+ */
592
+ function startWorkerSuites (workerRecord, files) {
593
+ for (const rawFile of files) {
594
+ const file = normalizeFile(rawFile)
595
+ if (workerRecord.suiteContexts.has(file)) {
596
+ continue
597
+ }
598
+
599
+ const reportedResult = workerRecord.reportedSuiteResults.get(file)
600
+ const suiteContext = {
601
+ error: getWebdriverioSuiteError(reportedResult?.error),
602
+ status: reportedResult?.status,
603
+ testSuiteAbsolutePath: file,
604
+ testSuiteExecutionId: workerRecord.testSuiteExecutionId,
605
+ }
606
+ testSuiteStartCh.runStores(suiteContext, () => {})
607
+ workerRecord.suiteContexts.set(file, suiteContext)
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Finishes one suite if it is still active.
613
+ *
614
+ * @param {CoordinatorState} state
615
+ * @param {WorkerRecord} workerRecord
616
+ * @param {string} rawFile
617
+ * @param {string} status
618
+ * @returns {void}
619
+ */
620
+ function finishWorkerSuite (state, workerRecord, rawFile, status) {
621
+ const file = normalizeFile(rawFile)
622
+ const suiteContext = workerRecord.suiteContexts.get(file)
623
+ if (!suiteContext || state.suiteStatuses.has(suiteContext)) {
624
+ return
625
+ }
626
+
627
+ state.suiteStatuses.set(suiteContext, status)
628
+ if (suiteContext.error) {
629
+ testSuiteErrorCh.runStores(suiteContext, () => {})
630
+ }
631
+ testSuiteFinishCh.publish({ status, ...suiteContext.currentStore })
632
+ }
633
+
634
+ /**
635
+ * Finishes every active suite belonging to a worker.
636
+ *
637
+ * @param {CoordinatorState} state
638
+ * @param {WorkerRecord} workerRecord
639
+ * @param {string} status
640
+ * @returns {void}
641
+ */
642
+ function finishAllWorkerSuites (state, workerRecord, status) {
643
+ let hasFailedSuite = false
644
+ for (const suiteContext of workerRecord.suiteContexts.values()) {
645
+ if (suiteContext.status === 'fail') {
646
+ hasFailedSuite = true
647
+ break
648
+ }
649
+ }
650
+
651
+ for (const [file, suiteContext] of workerRecord.suiteContexts) {
652
+ let suiteStatus = suiteContext.status ?? status
653
+ if (status === 'fail' && !hasFailedSuite && suiteStatus === 'skip') {
654
+ suiteStatus = 'fail'
655
+ }
656
+ finishWorkerSuite(state, workerRecord, file, suiteStatus)
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Sends a coordinator message to a WebdriverIO child process.
662
+ *
663
+ * @param {WorkerRecord} workerRecord
664
+ * @param {object} message
665
+ * @returns {void}
666
+ */
667
+ function sendWorkerMessage (workerRecord, message) {
668
+ const childProcess = workerRecord.worker.childProcess
669
+ if (!childProcess?.connected) {
670
+ return
671
+ }
672
+
673
+ childProcess.send(message, (error) => {
674
+ if (error) {
675
+ log.error('WebdriverIO Test Optimization IPC error', error)
676
+ }
677
+ })
678
+ }
679
+
680
+ /**
681
+ * Handles a worker request for its execution configuration.
682
+ *
683
+ * @param {CoordinatorState} state
684
+ * @param {WorkerRecord} workerRecord
685
+ * @param {object} message
686
+ * @returns {void}
687
+ */
688
+ function handleConfigurationRequest (state, workerRecord, message) {
689
+ const { files = [], requestId } = message.content || {}
690
+
691
+ initializeCoordinator(state, (configuration) => {
692
+ updateEarlyFlakeDetectionFaultyState(state, files)
693
+ startWorkerSuites(workerRecord, files)
694
+ sendWorkerMessage(workerRecord, {
695
+ origin: 'datadog',
696
+ name: CONFIGURATION_RESPONSE,
697
+ content: {
698
+ configuration,
699
+ requestId,
700
+ },
701
+ })
702
+ })
703
+ }
704
+
705
+ /**
706
+ * Disables EFD when the accumulated WebdriverIO run exceeds its new-suite threshold.
707
+ *
708
+ * @param {CoordinatorState} state
709
+ * @param {string[]} files
710
+ * @returns {void}
711
+ */
712
+ function updateEarlyFlakeDetectionFaultyState (state, files) {
713
+ const { configuration } = state
714
+ if (!configuration.isKnownTestsEnabled) {
715
+ return
716
+ }
717
+
718
+ addScheduledFiles(state, files)
719
+ const scheduledSuites = []
720
+ for (const file of state.scheduledFiles) {
721
+ scheduledSuites.push(getTestSuitePath(file, process.cwd()))
722
+ }
723
+
724
+ const isFaulty = getIsFaultyEarlyFlakeDetection(
725
+ scheduledSuites,
726
+ configuration.knownTests?.mocha || {},
727
+ configuration.earlyFlakeDetectionFaultyThreshold
728
+ )
729
+ if (isFaulty) {
730
+ configuration.isEarlyFlakeDetectionEnabled = false
731
+ configuration.isEarlyFlakeDetectionFaulty = true
732
+ configuration.isKnownTestsEnabled = false
733
+ }
734
+ }
735
+
736
+ /**
737
+ * Reconstructs an Error received from a WebdriverIO worker.
738
+ *
739
+ * @param {{message?: string, stack?: string}|undefined} error
740
+ * @returns {Error|undefined}
741
+ */
742
+ function getWebdriverioSuiteError (error) {
743
+ if (!error?.message) {
744
+ return
745
+ }
746
+
747
+ const suiteError = new Error(error.message)
748
+ if (error.stack) {
749
+ suiteError.stack = error.stack
750
+ }
751
+ return suiteError
752
+ }
753
+
754
+ /**
755
+ * Handles suite results reported by a WebdriverIO worker.
756
+ *
757
+ * @param {WorkerRecord} workerRecord
758
+ * @param {object} message
759
+ * @returns {void}
760
+ */
761
+ function handleSuiteResults (workerRecord, message) {
762
+ const { results = [] } = message.content || {}
763
+ for (const { error, file, status } of results) {
764
+ const normalizedFile = normalizeFile(file)
765
+ workerRecord.reportedSuiteResults.set(normalizedFile, { error, status })
766
+ const suiteContext = workerRecord.suiteContexts.get(normalizedFile)
767
+ if (suiteContext) {
768
+ suiteContext.error = getWebdriverioSuiteError(error)
769
+ suiteContext.status = status
770
+ }
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Handles all messages emitted by one WebdriverIO child process.
776
+ *
777
+ * @param {CoordinatorState} state
778
+ * @param {WorkerRecord} workerRecord
779
+ * @param {object|unknown[]} message
780
+ * @returns {void}
781
+ */
782
+ function handleWorkerMessage (state, workerRecord, message) {
783
+ if (message?.origin === WEBDRIVERIO_WORKER_ORIGIN && message.name === WEBDRIVERIO_WORKER_EVENT) {
784
+ message = message.args
785
+ }
786
+
787
+ if (Array.isArray(message)) {
788
+ const [messageCode, payload] = message
789
+ if (messageCode === MOCHA_WORKER_TRACE_PAYLOAD_CODE) {
790
+ collectTestOptimizationSummariesFromTraces(payload, {
791
+ attemptToFixExecutions: state.attemptToFixExecutions,
792
+ newTestsWithDynamicNames: state.newTestsWithDynamicNames,
793
+ testManagementExecutions: state.testManagementExecutions,
794
+ })
795
+ workerReportTraceCh.publish({
796
+ traces: payload,
797
+ [TEST_SUITE_EXECUTION_ID]: workerRecord.testSuiteExecutionId,
798
+ })
799
+ } else if (messageCode === MOCHA_WORKER_LOGS_PAYLOAD_CODE) {
800
+ workerReportLogsCh.publish(payload)
801
+ } else if (messageCode === MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE) {
802
+ workerReportTelemetryCh.publish(payload)
803
+ }
804
+ return
805
+ }
806
+
807
+ if (!message || typeof message !== 'object') {
808
+ return
809
+ }
810
+
811
+ if (message.name === WORKER_READY) {
812
+ initializeCoordinator(state, () => {
813
+ if (state.testFrameworkAdapter === JASMINE_FRAMEWORK_ADAPTER) {
814
+ startWorkerSuites(workerRecord, workerRecord.specs)
815
+ }
816
+ if (message.content?.requestId) {
817
+ sendWorkerMessage(workerRecord, {
818
+ origin: 'datadog',
819
+ name: WORKER_READY_RESPONSE,
820
+ content: { requestId: message.content.requestId },
821
+ })
822
+ }
823
+ })
824
+ return
825
+ }
826
+ if (message.name === CONFIGURATION_REQUEST) {
827
+ handleConfigurationRequest(state, workerRecord, message)
828
+ return
829
+ }
830
+ if (message.name === SUITE_FINISH) {
831
+ handleSuiteResults(workerRecord, message)
832
+ return
833
+ }
834
+ if (message.name === 'testFrameworkInit') {
835
+ workerRecord.hasTests = message.content?.hasTests
836
+ if (!workerRecord.hasTests && state.frameworkVersion) {
837
+ initializeCoordinator(state, () => {
838
+ startWorkerSuites(workerRecord, workerRecord.specs)
839
+ finishAllWorkerSuites(state, workerRecord, 'skip')
840
+ })
841
+ }
842
+ }
843
+ }
844
+
845
+ /**
846
+ * Handles child-process exit and closes suites missing an explicit result.
847
+ *
848
+ * @param {CoordinatorState} state
849
+ * @param {WorkerRecord} workerRecord
850
+ * @param {object} exit
851
+ * @returns {void}
852
+ */
853
+ function handleWorkerExit (state, workerRecord, exit) {
854
+ state.activeWorkers--
855
+ workerRecord.exitCode = exit.exitCode
856
+ workerRecord.retries = exit.retries
857
+ if (!state.sessionStarted) {
858
+ return
859
+ }
860
+
861
+ const status = workerRecord.hasTests === false ? 'skip' : exit.exitCode === 0 ? 'pass' : 'fail'
862
+ if (status === 'fail' && workerRecord.suiteContexts.size === 0) {
863
+ startWorkerSuites(workerRecord, workerRecord.specs)
864
+ }
865
+ finishAllWorkerSuites(state, workerRecord, status)
866
+ }
867
+
868
+ /**
869
+ * Registers a newly created WebdriverIO worker with the coordinator.
870
+ *
871
+ * @param {CoordinatorState} state
872
+ * @param {object} worker
873
+ * @param {string[]} specs
874
+ * @returns {void}
875
+ */
876
+ function registerWorker (state, worker, specs) {
877
+ const normalizedSpecs = []
878
+ for (const spec of specs) {
879
+ normalizedSpecs.push(normalizeFile(spec))
880
+ }
881
+
882
+ const workerRecord = {
883
+ worker,
884
+ specs: normalizedSpecs,
885
+ reportedSuiteResults: new Map(),
886
+ suiteContexts: new Map(),
887
+ testSuiteExecutionId: String(++state.nextWorkerId),
888
+ hasTests: undefined,
889
+ exitCode: undefined,
890
+ retries: undefined,
891
+ }
892
+ state.activeWorkers++
893
+ if (state.activeWorkers > state.maxActiveWorkers) {
894
+ state.maxActiveWorkers = state.activeWorkers
895
+ }
896
+ state.workers.add(workerRecord)
897
+
898
+ worker.on('message', message => handleWorkerMessage(state, workerRecord, message))
899
+ worker.once('exit', exit => handleWorkerExit(state, workerRecord, exit))
900
+ }
901
+
902
+ /**
903
+ * Calculates the final status for the coordinated session.
904
+ *
905
+ * @param {CoordinatorState} state
906
+ * @returns {string}
907
+ */
908
+ function getSessionStatus (state) {
909
+ let hasPassingSuite = false
910
+
911
+ for (const workerRecord of state.workers) {
912
+ if (workerRecord.exitCode !== undefined && workerRecord.exitCode !== 0) {
913
+ if (workerRecord.retries > 0) {
914
+ continue
915
+ }
916
+ return 'fail'
917
+ }
918
+ for (const suiteContext of workerRecord.suiteContexts.values()) {
919
+ const status = state.suiteStatuses.get(suiteContext)
920
+ if (status === 'fail') {
921
+ return 'fail'
922
+ }
923
+ if (status === 'pass') {
924
+ hasPassingSuite = true
925
+ }
926
+ }
927
+ }
928
+
929
+ return hasPassingSuite ? 'pass' : 'skip'
930
+ }
931
+
932
+ /**
933
+ * Finishes the single WebdriverIO-owned test session.
934
+ *
935
+ * @param {CoordinatorState} state
936
+ * @param {unknown} error
937
+ * @param {() => void} onDone
938
+ * @returns {void}
939
+ */
940
+ function finishCoordinator (state, error, onDone) {
941
+ if (state.finished) {
942
+ onDone()
943
+ return
944
+ }
945
+ if (!state.sessionStarted) {
946
+ if (!error && getSessionStatus(state) !== 'fail') {
947
+ onDone()
948
+ return
949
+ }
950
+ initializeCoordinator(state, () => finishCoordinator(state, error, onDone))
951
+ return
952
+ }
953
+ state.finished = true
954
+
955
+ for (const workerRecord of state.workers) {
956
+ const status = workerRecord.hasTests === false
957
+ ? 'skip'
958
+ : workerRecord.exitCode === 0 ? 'pass' : 'fail'
959
+ finishAllWorkerSuites(state, workerRecord, status)
960
+ }
961
+
962
+ if (!testSessionFinishCh.hasSubscribers) {
963
+ logTestOptimizationSummary({
964
+ attemptToFixExecutions: state.attemptToFixExecutions,
965
+ newTestsWithDynamicNames: state.newTestsWithDynamicNames,
966
+ testManagementExecutions: state.testManagementExecutions,
967
+ })
968
+ onDone()
969
+ return
970
+ }
971
+
972
+ testSessionFinishCh.publish({
973
+ status: error ? 'fail' : getSessionStatus(state),
974
+ error,
975
+ isEarlyFlakeDetectionEnabled: state.configuration.isEarlyFlakeDetectionEnabled,
976
+ isEarlyFlakeDetectionFaulty: state.configuration.isEarlyFlakeDetectionFaulty,
977
+ isParallel: state.maxActiveWorkers > 1,
978
+ isSuitesSkipped: false,
979
+ isTestManagementEnabled: state.configuration.isTestManagementTestsEnabled,
980
+ onDone,
981
+ })
982
+ logTestOptimizationSummary({
983
+ attemptToFixExecutions: state.attemptToFixExecutions,
984
+ newTestsWithDynamicNames: state.newTestsWithDynamicNames,
985
+ testManagementExecutions: state.testManagementExecutions,
986
+ })
987
+ }
988
+
989
+ // dc-polyfill supports partial tracing-channel subscribers, unlike the Node.js type definition.
990
+ // @ts-expect-error
991
+ jasmineAdapterInitCh.subscribe({
992
+ asyncEnd (context) {
993
+ initializeJasmineWorker(context.result || context.self, context)
994
+ },
995
+ })
996
+
997
+ // dc-polyfill supports partial tracing-channel subscribers, unlike the Node.js type definition.
998
+ // @ts-expect-error
999
+ launcherStartInstanceCh.subscribe({
1000
+ start (context) {
1001
+ const localRunner = context.self?.runner
1002
+ const runnerConfiguration = localRunner && getRunnerConfiguration(localRunner)
1003
+ if (!testFinishCh.hasSubscribers || !SUPPORTED_FRAMEWORK_ADAPTERS.has(runnerConfiguration?.framework)) {
1004
+ return
1005
+ }
1006
+
1007
+ const state = getCoordinatorState(localRunner)
1008
+ addScheduledFiles(state, context.arguments?.[0] || [])
1009
+ if (context.self._schedule) {
1010
+ for (const schedule of context.self._schedule) {
1011
+ if (schedule.specs) {
1012
+ for (const { files } of schedule.specs) {
1013
+ addScheduledFiles(state, files)
1014
+ }
1015
+ }
1016
+ }
1017
+ }
1018
+ },
1019
+ })
1020
+
1021
+ // dc-polyfill supports partial tracing-channel subscribers, unlike the Node.js type definition.
1022
+ // @ts-expect-error
1023
+ localRunnerRunCh.subscribe({
1024
+ start (context) {
1025
+ const runnerConfiguration = getRunnerConfiguration(context.self)
1026
+ if (!testFinishCh.hasSubscribers || !SUPPORTED_FRAMEWORK_ADAPTERS.has(runnerConfiguration?.framework)) {
1027
+ return
1028
+ }
1029
+
1030
+ const state = getCoordinatorState(context.self)
1031
+ const workerOptions = context.arguments?.[0]
1032
+ addScheduledFiles(state, workerOptions?.specs || [])
1033
+ let workerEnvironment = runnerConfiguration.runnerEnv || {}
1034
+ const launcherNodeOptions = getEnvironmentVariable('NODE_OPTIONS')
1035
+ const workerNodeOptions = workerEnvironment.NODE_OPTIONS
1036
+
1037
+ if (launcherNodeOptions && !includesNodeOptions(workerNodeOptions, launcherNodeOptions)) {
1038
+ workerEnvironment = {
1039
+ ...workerEnvironment,
1040
+ NODE_OPTIONS: workerNodeOptions
1041
+ ? `${launcherNodeOptions} ${workerNodeOptions}`
1042
+ : launcherNodeOptions,
1043
+ }
1044
+ }
1045
+
1046
+ runnerConfiguration.runnerEnv = {
1047
+ ...workerEnvironment,
1048
+ MOCHA_WORKER_ID: 'webdriverio',
1049
+ [WEBDRIVERIO_WORKER_ENV]: 'true',
1050
+ }
1051
+ context.ddCoordinatorState = state
1052
+ context.ddWorkerSpecs = workerOptions?.specs || []
1053
+ },
1054
+ asyncEnd (context) {
1055
+ const state = context.ddCoordinatorState
1056
+ if (!state) {
1057
+ return
1058
+ }
1059
+ if (context.error) {
1060
+ state.runError ??= context.error
1061
+ return
1062
+ }
1063
+ if (!context.result) {
1064
+ return
1065
+ }
1066
+ registerWorker(state, context.result, context.ddWorkerSpecs)
1067
+ },
1068
+ })
1069
+
1070
+ // @ts-expect-error See the partial tracing-channel subscriber above.
1071
+ localRunnerShutdownCh.subscribe({
1072
+ asyncEnd (context) {
1073
+ const state = coordinatorStates.get(context.self)
1074
+ if (!state) {
1075
+ return
1076
+ }
1077
+
1078
+ // Orchestrion uses the callback for the matching settlement path to delay LocalRunner.shutdown.
1079
+ const waitForCoordinator = onDone => {
1080
+ const error = context.error ?? state.runError
1081
+ if (state.initializing) {
1082
+ state.initializationCallbacks.push(() => finishCoordinator(state, error, onDone))
1083
+ } else {
1084
+ finishCoordinator(state, error, onDone)
1085
+ }
1086
+ }
1087
+ context.resolveCallback = waitForCoordinator
1088
+ context.rejectCallback = waitForCoordinator
1089
+ },
1090
+ })