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