dd-trace 6.9.0 → 6.11.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 (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -1,11 +1,17 @@
1
1
  'use strict'
2
2
 
3
+ const { performance } = require('node:perf_hooks')
4
+ const dateNow = Date.now
3
5
  const { fileURLToPath } = require('node:url')
4
6
 
5
7
  const { channel } = require('dc-polyfill')
6
8
 
7
9
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
8
10
  const { storage } = require('../../datadog-core')
11
+ const {
12
+ getEfdRetryCountForDuration,
13
+ hasEfdRetries,
14
+ } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
9
15
  const log = require('../../dd-trace/src/log')
10
16
  const {
11
17
  sendWebdriverioWorkerMessage,
@@ -44,6 +50,8 @@ const {
44
50
  TEST_FINAL_STATUS,
45
51
  TEST_HAS_DYNAMIC_NAME,
46
52
  TEST_FRAMEWORK_ADAPTER,
53
+ DYNAMIC_NAME_RE,
54
+ getFailedTestReplayPromise,
47
55
  getTestSuiteExecutionKey,
48
56
  isModifiedTest,
49
57
  } = require('../../dd-trace/src/plugins/util/test')
@@ -62,14 +70,24 @@ const {
62
70
 
63
71
  const jasmineAdapterRunAsyncEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineAdapter_run:asyncEnd'
64
72
  const jasmineDoneCh = 'ci:webdriverio:jasmine:done'
73
+ const jasmineExecuteAsyncEndCh = 'tracing:orchestrion:@wdio/utils:executeAsync:asyncEnd'
74
+ const jasmineExecuteAsyncErrorCh = 'tracing:orchestrion:@wdio/utils:executeAsync:error'
75
+ const jasmineExecuteAsyncStartCh = 'tracing:orchestrion:@wdio/utils:executeAsync:start'
65
76
  const jasmineReporterSpecDoneEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specDone:end'
77
+ const jasmineReporterSpecDoneStartCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specDone:start'
66
78
  const jasmineReporterSpecStartedEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specStarted:end'
67
79
  const jasmineReporterSuiteDoneEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_suiteDone:end'
68
80
  const jasmineReporterSuiteStartedEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_suiteStarted:end'
81
+ const jasmineSpecAttemptDoneEndCh = 'tracing:orchestrion:jasmine-core:Spec_attemptDone:end'
82
+ const jasmineSpecExecuteStartCh = 'tracing:orchestrion:jasmine-core:Spec_execute:start'
69
83
  const jasmineTestFunctionStartCh = 'tracing:orchestrion:@wdio/utils:testFrameworkFnWrapper:start'
70
84
  const testFinishCh = channel('ci:mocha:test:finish')
85
+ const testRetryCh = channel('ci:mocha:test:retry')
71
86
  const WEBDRIVERIO_JASMINE_ADAPTER = 'jasmine'
72
87
  const workerFinishCh = channel('ci:mocha:worker:finish')
88
+ const WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT = Symbol('webdriverioJasmineFailedExpectationCount')
89
+ const WEBDRIVERIO_JASMINE_FUNCTION_TYPE = Symbol('webdriverioJasmineFunctionType')
90
+ const WEBDRIVERIO_JASMINE_TEST = Symbol('webdriverioJasmineTest')
73
91
 
74
92
  /**
75
93
  * @typedef {object} WebdriverioJasmineResult
@@ -156,6 +174,15 @@ class MochaPlugin extends CiPlugin {
156
174
 
157
175
  this._testTitleToParams = {}
158
176
  this.sourceRoot = process.cwd()
177
+ this._pendingTestSuiteSpans = []
178
+ this._timeOrigin = dateNow()
179
+ this._perfOrigin = performance.now()
180
+
181
+ this.addSub('ci:mocha:session:start', () => {
182
+ this._pendingTestSuiteSpans = []
183
+ this._timeOrigin = dateNow()
184
+ this._perfOrigin = performance.now()
185
+ })
159
186
 
160
187
  this.addSub('ci:mocha:worker:configuration', ({
161
188
  libraryConfig,
@@ -170,7 +197,10 @@ class MochaPlugin extends CiPlugin {
170
197
  this._setRepositoryRoot(repositoryRoot)
171
198
  if (testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
172
199
  this._webdriverioJasmineState = {
200
+ completedTestStatuses: new Map(),
173
201
  currentResult: undefined,
202
+ pendingTestFinishCallbacks: [],
203
+ pendingTestFinishes: 0,
174
204
  specs: specs || [],
175
205
  suiteErrors: new Map(),
176
206
  suiteFiles: new Map(),
@@ -188,11 +218,82 @@ class MochaPlugin extends CiPlugin {
188
218
  const result = this._webdriverioJasmineState?.currentResult
189
219
  const type = ctx.arguments?.[1]
190
220
  if (type === 'Test' || (type === 'Hook' && result)) {
191
- return this.#startWebdriverioJasmineTest(result)
221
+ return this.#configureWebdriverioJasmineFunction(ctx, result, type)
222
+ }
223
+ return storage('legacy').getStore()
224
+ })
225
+
226
+ this.addBind(jasmineExecuteAsyncStartCh, (ctx) => {
227
+ const currentStore = storage('legacy').getStore()
228
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
229
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER || !test) {
230
+ return currentStore
231
+ }
232
+
233
+ const functionType = currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE]
234
+ if (functionType === 'Test') {
235
+ ctx.retryCallback = error => this.#retryWebdriverioJasmineTest(test, error)
236
+ }
237
+ const nextStore = {
238
+ ...test.currentStore,
239
+ [WEBDRIVERIO_JASMINE_FUNCTION_TYPE]: functionType,
240
+ }
241
+ if (functionType === 'Hook') {
242
+ nextStore[WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT] =
243
+ this._webdriverioJasmineState.currentResult?.failedExpectations?.length || 0
244
+ }
245
+ return nextStore
246
+ })
247
+
248
+ this.addBind(jasmineSpecExecuteStartCh, (ctx) => {
249
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
250
+ return storage('legacy').getStore()
192
251
  }
252
+
253
+ this.#configureWebdriverioJasmineLifecycle(ctx)
193
254
  return storage('legacy').getStore()
194
255
  })
195
256
 
257
+ this.addSub(jasmineSpecAttemptDoneEndCh, (ctx) => {
258
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
259
+ return
260
+ }
261
+
262
+ const status = typeof ctx.result === 'string' ? ctx.result : ctx.self?.result?.status
263
+ const runnerStatus = this.#prepareWebdriverioJasmineAttempt(ctx.self, status)
264
+ if (typeof ctx.result === 'string') {
265
+ ctx.result = runnerStatus
266
+ } else if (ctx.self?.result) {
267
+ ctx.self.result.status = runnerStatus
268
+ }
269
+ })
270
+
271
+ this.addSub(jasmineExecuteAsyncErrorCh, (ctx) => {
272
+ const currentStore = ctx.currentStore || storage('legacy').getStore()
273
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
274
+ if (
275
+ this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER &&
276
+ test &&
277
+ currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE] === 'Hook'
278
+ ) {
279
+ test.hasFinalHookFailure = true
280
+ }
281
+ })
282
+
283
+ this.addSub(jasmineExecuteAsyncEndCh, (ctx) => {
284
+ const currentStore = ctx.currentStore || storage('legacy').getStore()
285
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
286
+ const failedExpectationCount = currentStore?.[WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT]
287
+ if (
288
+ this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER &&
289
+ test &&
290
+ currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE] === 'Hook' &&
291
+ this._webdriverioJasmineState.currentResult?.failedExpectations?.length > failedExpectationCount
292
+ ) {
293
+ test.hasFinalHookFailure = true
294
+ }
295
+ })
296
+
196
297
  this.addSub(jasmineReporterSuiteStartedEndCh, (ctx) => {
197
298
  if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
198
299
  const suite = ctx.arguments?.[0]
@@ -239,9 +340,32 @@ class MochaPlugin extends CiPlugin {
239
340
  }
240
341
  })
241
342
 
343
+ this.addBind(jasmineReporterSpecDoneStartCh, (ctx) => {
344
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
345
+ return storage('legacy').getStore()
346
+ }
347
+
348
+ const result = ctx.arguments?.[0]
349
+ const test = this._webdriverioJasmineState?.tests.get(result?.id)
350
+ const recoveredEarlyFlakeDetection = test?.isEarlyFlakeDetection &&
351
+ !test.hasFinalHookFailure && test.statuses.includes('pass')
352
+ if (
353
+ result.status === 'failed' &&
354
+ ((test?.isQuarantined && !test.isAttemptToFix) || recoveredEarlyFlakeDetection)
355
+ ) {
356
+ test.reportedStatus = result.status
357
+ result.status = 'passed'
358
+ }
359
+ return test?.currentStore || storage('legacy').getStore()
360
+ })
361
+
242
362
  this.addSub(jasmineReporterSpecDoneEndCh, (ctx) => {
243
363
  if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
244
- this.#finishWebdriverioJasmineTest(ctx.arguments?.[0], ctx.self?._specs)
364
+ const result = ctx.result
365
+ const finishPromise = this.#finishWebdriverioJasmineTest(ctx.arguments?.[0], ctx.self?._specs)
366
+ if (finishPromise) {
367
+ ctx.result = finishPromise.then(() => result)
368
+ }
245
369
  }
246
370
  })
247
371
 
@@ -358,7 +482,19 @@ class MochaPlugin extends CiPlugin {
358
482
  if (!testSuiteSpan.context().getTag(TEST_STATUS)) {
359
483
  testSuiteSpan.setTag(TEST_STATUS, status)
360
484
  }
361
- testSuiteSpan.finish()
485
+ const exporter = this.tracer._exporter
486
+ if (exporter.deferTestSuiteSpan) {
487
+ exporter.deferTestSuiteSpan(testSuiteSpan)
488
+ testSuiteSpan.finish()
489
+ } else if (exporter.exportDeferredTestSuiteSpans) {
490
+ const finishTime = this._now()
491
+ this._pendingTestSuiteSpans.push({
492
+ span: testSuiteSpan,
493
+ finishTime,
494
+ })
495
+ } else {
496
+ testSuiteSpan.finish()
497
+ }
362
498
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
363
499
  }
364
500
  })
@@ -408,6 +544,7 @@ class MochaPlugin extends CiPlugin {
408
544
  })
409
545
 
410
546
  this.addSub('ci:mocha:worker:finish', ({ onDone } = {}) => {
547
+ this._finishPendingTestSuiteSpans()
411
548
  this.tracer._exporter.flush(onDone)
412
549
  })
413
550
 
@@ -510,10 +647,13 @@ class MochaPlugin extends CiPlugin {
510
647
  this.addSub('ci:mocha:test:retry', ({
511
648
  span,
512
649
  isFirstAttempt,
650
+ isFirstFailure = isFirstAttempt,
513
651
  willBeRetried,
514
652
  err,
515
653
  test,
654
+ isAttemptToFixRetry,
516
655
  isAtrRetry,
656
+ isEfdRetry,
517
657
  promises,
518
658
  }) => {
519
659
  if (span) {
@@ -528,8 +668,12 @@ class MochaPlugin extends CiPlugin {
528
668
  span.setTag(TEST_STATUS, 'fail')
529
669
  if (!isFirstAttempt) {
530
670
  span.setTag(TEST_IS_RETRY, 'true')
531
- if (isAtrRetry) {
671
+ if (isAttemptToFixRetry) {
672
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atf)
673
+ } else if (isAtrRetry) {
532
674
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
675
+ } else if (isEfdRetry) {
676
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
533
677
  } else {
534
678
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext)
535
679
  }
@@ -543,7 +687,7 @@ class MochaPlugin extends CiPlugin {
543
687
  'test',
544
688
  this.getTestTelemetryTags(span)
545
689
  )
546
- if (isFirstAttempt && willBeRetried && this.di && this.libraryConfig?.isDiEnabled) {
690
+ if (isFirstFailure && willBeRetried && this.di && this.libraryConfig?.isDiEnabled) {
547
691
  const probeInformation = this.addDiProbe(err)
548
692
  if (probeInformation) {
549
693
  const { file, line, stackIndex, setProbePromise } = probeInformation
@@ -557,7 +701,7 @@ class MochaPlugin extends CiPlugin {
557
701
  }
558
702
  }
559
703
 
560
- if (!isFirstAttempt &&
704
+ if (!isFirstFailure &&
561
705
  willBeRetried &&
562
706
  this.di &&
563
707
  this.libraryConfig?.isDiEnabled &&
@@ -594,6 +738,7 @@ class MochaPlugin extends CiPlugin {
594
738
  isEarlyFlakeDetectionFaulty,
595
739
  isTestManagementEnabled,
596
740
  isParallel,
741
+ isFrameworkError,
597
742
  onDone,
598
743
  }) => {
599
744
  this._exportPendingWorkerTraces()
@@ -609,8 +754,17 @@ class MochaPlugin extends CiPlugin {
609
754
  if (error) {
610
755
  this.testSessionSpan.setTag('error', error)
611
756
  this.testModuleSpan.setTag('error', error)
757
+ if (isFrameworkError) {
758
+ this.tracer._exporter.setDeferredTestSuiteError?.(error)
759
+ for (const testSuiteSpan of this._testSuiteSpansByTestSuite.values()) {
760
+ testSuiteSpan.setTag(TEST_STATUS, 'fail')
761
+ testSuiteSpan.setTag('error', error)
762
+ }
763
+ }
612
764
  }
613
765
 
766
+ this._finishPendingTestSuiteSpans()
767
+
614
768
  if (isParallel) {
615
769
  this.testSessionSpan.setTag(MOCHA_IS_PARALLEL, 'true')
616
770
  }
@@ -648,6 +802,7 @@ class MochaPlugin extends CiPlugin {
648
802
  this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
649
803
  }
650
804
 
805
+ this.tracer._exporter.exportDeferredTestSuiteSpans?.()
651
806
  this.testModuleSpan.finish()
652
807
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
653
808
  this.testSessionSpan.finish()
@@ -669,6 +824,27 @@ class MochaPlugin extends CiPlugin {
669
824
  })
670
825
  }
671
826
 
827
+ /**
828
+ * Returns the current time in the coordinate system used by spans.
829
+ *
830
+ * @returns {number}
831
+ */
832
+ _now () {
833
+ return this._timeOrigin + performance.now() - this._perfOrigin
834
+ }
835
+
836
+ /**
837
+ * Finishes suites retained for a later framework finalization event.
838
+ *
839
+ * @returns {void}
840
+ */
841
+ _finishPendingTestSuiteSpans () {
842
+ for (const { span, finishTime } of this._pendingTestSuiteSpans) {
843
+ span.finish(finishTime)
844
+ }
845
+ this._pendingTestSuiteSpans = []
846
+ }
847
+
672
848
  /**
673
849
  * Starts a Jasmine test span around WebdriverIO's test-function wrapper.
674
850
  *
@@ -683,6 +859,9 @@ class MochaPlugin extends CiPlugin {
683
859
  if (!state || !result?.id) {
684
860
  return currentStore
685
861
  }
862
+ if (state.completedTestStatuses.has(result.id)) {
863
+ return currentStore
864
+ }
686
865
 
687
866
  const existingTest = state.tests.get(result.id)
688
867
  if (existingTest) {
@@ -702,20 +881,333 @@ class MochaPlugin extends CiPlugin {
702
881
  return currentStore
703
882
  }
704
883
 
705
- const span = this.startTestSpan({
706
- testName: result.fullName || result.description,
884
+ const testName = result.fullName || result.description
885
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.sourceRoot)
886
+ const properties = this.libraryConfig?.testManagementTests?.mocha
887
+ ?.suites?.[testSuite]?.tests?.[testName]?.properties || {}
888
+ const isAttemptToFix = this.libraryConfig?.isTestManagementTestsEnabled && properties.attempt_to_fix
889
+ const isDisabled = this.libraryConfig?.isTestManagementTestsEnabled && properties.disabled
890
+ const isQuarantined = this.libraryConfig?.isTestManagementTestsEnabled && properties.quarantined
891
+ const isModified = this.libraryConfig?.isImpactedTestsEnabled && isModifiedTest(
892
+ getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot || this.sourceRoot),
893
+ null,
894
+ null,
895
+ this.libraryConfig.modifiedFiles,
896
+ this.constructor.id
897
+ )
898
+ const knownTests = this.libraryConfig?.knownTests?.mocha
899
+ const isNew = this.libraryConfig?.isKnownTestsEnabled && knownTests &&
900
+ !(knownTests[testSuite] || []).includes(testName)
901
+ const isEarlyFlakeDetection = this.libraryConfig?.isEarlyFlakeDetectionEnabled &&
902
+ hasEfdRetries(this.libraryConfig.earlyFlakeDetectionRetryPolicy) &&
903
+ !isAttemptToFix && !isDisabled && (isNew || isModified)
904
+ const isAtr = this.libraryConfig?.isFlakyTestRetriesEnabled &&
905
+ !isAttemptToFix && !isEarlyFlakeDetection
906
+ let retryCount = 0
907
+ if (isAttemptToFix) {
908
+ retryCount = this.libraryConfig.testManagementAttemptToFixRetries
909
+ } else if (isEarlyFlakeDetection) {
910
+ retryCount = this.libraryConfig.earlyFlakeDetectionRetryPolicy.schedulingRetryCount
911
+ } else if (isAtr) {
912
+ retryCount = this.libraryConfig.flakyTestRetriesCount
913
+ }
914
+
915
+ const test = {
916
+ attempt: 0,
917
+ attemptStart: undefined,
918
+ currentStore: undefined,
919
+ earlyFlakeAbortReason: undefined,
920
+ hasDynamicName: isNew && DYNAMIC_NAME_RE.test(testName),
921
+ hasFailedAttempt: false,
922
+ hasFinalHookFailure: false,
923
+ isAttemptToFix,
924
+ isAtr,
925
+ isDisabled,
926
+ isEarlyFlakeDetection,
927
+ isModified,
928
+ isNew,
929
+ isQuarantined,
930
+ reportedStatus: undefined,
931
+ retryCount,
932
+ retryWait: undefined,
933
+ runnerStatus: undefined,
934
+ span: undefined,
935
+ statuses: [],
936
+ testName,
707
937
  testSuiteAbsolutePath,
708
938
  title: result.description,
939
+ }
940
+ state.tests.set(result.id, test)
941
+ this.#startWebdriverioJasmineAttempt(test, currentStore)
942
+
943
+ return test.currentStore
944
+ }
945
+
946
+ /**
947
+ * Applies a managed Jasmine test's skip and retry policy to WebdriverIO's function wrapper.
948
+ *
949
+ * @param {object} context
950
+ * @param {WebdriverioJasmineResult|undefined} result
951
+ * @param {'Test'|'Hook'} type
952
+ * @returns {object|undefined}
953
+ */
954
+ #configureWebdriverioJasmineFunction (context, result, type) {
955
+ const currentStore = this.#startWebdriverioJasmineTest(result)
956
+ const test = this._webdriverioJasmineState?.tests.get(result?.id)
957
+ if (!test) {
958
+ return currentStore
959
+ }
960
+
961
+ if (test.isAttemptToFix || test.isAtr || test.isEarlyFlakeDetection) {
962
+ context.arguments[6] = 0
963
+ }
964
+ return {
965
+ ...test.currentStore,
966
+ [WEBDRIVERIO_JASMINE_FUNCTION_TYPE]: type,
967
+ }
968
+ }
969
+
970
+ /**
971
+ * Starts one WebdriverIO Jasmine test attempt under its suite span.
972
+ *
973
+ * @param {object} test
974
+ * @param {object|undefined} parentStore
975
+ * @returns {void}
976
+ */
977
+ #startWebdriverioJasmineAttempt (test, parentStore) {
978
+ test.attemptStart = performance.now()
979
+ test.hasFinalHookFailure = false
980
+ const span = this.startTestSpan({
981
+ hasDynamicName: test.hasDynamicName,
982
+ isAttemptToFix: test.isAttemptToFix,
983
+ isDisabled: test.isDisabled,
984
+ isEfdRetry: test.isEarlyFlakeDetection && test.attempt > 0,
985
+ isModified: test.isModified,
986
+ isNew: test.isNew,
987
+ isParallel: true,
988
+ isQuarantined: test.isQuarantined,
989
+ testName: test.testName,
990
+ testSuiteAbsolutePath: test.testSuiteAbsolutePath,
991
+ title: test.title,
709
992
  })
710
- const testStore = { ...currentStore, span }
711
- state.tests.set(result.id, {
712
- currentStore: testStore,
993
+ test.span = span
994
+ test.currentStore = {
995
+ ...parentStore,
713
996
  span,
714
- testSuiteAbsolutePath,
715
- })
997
+ [WEBDRIVERIO_JASMINE_TEST]: test,
998
+ }
716
999
  this.activeTestSpan = span
1000
+ }
717
1001
 
718
- return testStore
1002
+ /**
1003
+ * Delays Jasmine's parent runner until every Datadog-managed spec execution has completed.
1004
+ *
1005
+ * @param {object} context
1006
+ * @returns {void}
1007
+ */
1008
+ #configureWebdriverioJasmineLifecycle (context) {
1009
+ const isLegacyJasmine = Boolean(context.self?.queueableFn)
1010
+ const spec = isLegacyJasmine ? context.self : context.arguments?.[0]
1011
+ const onComplete = context.arguments?.[1]
1012
+ const originalTestFunction = spec?.queueableFn?.fn
1013
+ if (!spec?.id || typeof onComplete !== 'function' || typeof originalTestFunction !== 'function') {
1014
+ return
1015
+ }
1016
+
1017
+ if (this.libraryConfig?.isTestManagementTestsEnabled) {
1018
+ this.#startWebdriverioJasmineTest(spec.result)
1019
+ const test = this._webdriverioJasmineState?.tests.get(spec.id)
1020
+ if (test?.isDisabled && !test.isAttemptToFix) {
1021
+ spec.pend('Skipped by Datadog Test Management')
1022
+ }
1023
+ }
1024
+
1025
+ const executionArguments = [...context.arguments]
1026
+ context.arguments[1] = (...completeArguments) => {
1027
+ const test = this._webdriverioJasmineState?.tests.get(spec.id)
1028
+ if (!test?.willRetry) {
1029
+ return onComplete(...completeArguments)
1030
+ }
1031
+
1032
+ test.willRetry = false
1033
+ const retry = () => {
1034
+ try {
1035
+ this.#startNextWebdriverioJasmineAttempt(test)
1036
+ spec.reset()
1037
+ spec.queueableFn.fn = originalTestFunction
1038
+ if (isLegacyJasmine) {
1039
+ spec.execute(
1040
+ executionArguments[0],
1041
+ onComplete,
1042
+ executionArguments[2],
1043
+ executionArguments[3]
1044
+ )
1045
+ } else {
1046
+ context.self._executeSpec(spec, onComplete)
1047
+ }
1048
+ } catch (error) {
1049
+ log.error('WebdriverIO Jasmine retry error', error)
1050
+ onComplete(...completeArguments)
1051
+ }
1052
+ }
1053
+ const retryWait = test.retryWait
1054
+ test.retryWait = undefined
1055
+ if (retryWait?.then) {
1056
+ retryWait.then(retry, retry)
1057
+ } else {
1058
+ retry()
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ /**
1064
+ * Selects the runner-visible status and whether Jasmine should execute the full spec again.
1065
+ *
1066
+ * @param {object|undefined} spec
1067
+ * @param {string|undefined} status
1068
+ * @returns {string|undefined}
1069
+ */
1070
+ #prepareWebdriverioJasmineAttempt (spec, status) {
1071
+ const state = this._webdriverioJasmineState
1072
+ const completedStatus = state?.completedTestStatuses.get(spec?.id)
1073
+ if (completedStatus) {
1074
+ return completedStatus
1075
+ }
1076
+
1077
+ const test = state?.tests.get(spec?.id)
1078
+ if (!test || !status) {
1079
+ return status
1080
+ }
1081
+
1082
+ const testStatus = getJasmineStatus(status)
1083
+ if (
1084
+ testStatus !== 'skip' &&
1085
+ test.isEarlyFlakeDetection &&
1086
+ test.attempt === 0
1087
+ ) {
1088
+ test.retryCount = getEfdRetryCountForDuration(
1089
+ performance.now() - test.attemptStart,
1090
+ this.libraryConfig.earlyFlakeDetectionRetryPolicy
1091
+ )
1092
+ if (test.retryCount === 0) {
1093
+ test.earlyFlakeAbortReason = 'slow'
1094
+ }
1095
+ }
1096
+
1097
+ const hasManagedRetry = testStatus !== 'skip' && test.attempt < test.retryCount
1098
+ test.willRetry = hasManagedRetry && (
1099
+ test.isAttemptToFix ||
1100
+ test.isEarlyFlakeDetection ||
1101
+ (test.isAtr && testStatus === 'fail' && !test.hasFinalHookFailure)
1102
+ )
1103
+
1104
+ let runnerStatus = status
1105
+ const recoveredEarlyFlakeDetection = test.isEarlyFlakeDetection &&
1106
+ !test.hasFinalHookFailure && test.statuses.includes('pass')
1107
+ if (
1108
+ testStatus === 'fail' &&
1109
+ (test.willRetry || (test.isQuarantined && !test.isAttemptToFix) || recoveredEarlyFlakeDetection)
1110
+ ) {
1111
+ runnerStatus = 'passed'
1112
+ } else if (testStatus !== 'skip' && !test.willRetry && test.isAttemptToFix) {
1113
+ runnerStatus = test.statuses.every(previousStatus => previousStatus === 'pass') && testStatus === 'pass'
1114
+ ? 'passed'
1115
+ : 'failed'
1116
+ }
1117
+
1118
+ if (runnerStatus !== status) {
1119
+ test.reportedStatus = status
1120
+ }
1121
+ if (!test.willRetry) {
1122
+ test.runnerStatus = runnerStatus
1123
+ }
1124
+ return runnerStatus
1125
+ }
1126
+
1127
+ /**
1128
+ * Finishes one Datadog-managed Jasmine attempt before the full spec is executed again.
1129
+ *
1130
+ * @param {object} test
1131
+ * @param {WebdriverioJasmineResult} result
1132
+ * @returns {void}
1133
+ */
1134
+ #finishWebdriverioJasmineRetry (test, result) {
1135
+ const status = getJasmineStatus(test.reportedStatus || result.status)
1136
+ const error = getJasmineError(result)
1137
+ test.statuses.push(status)
1138
+
1139
+ if (error) {
1140
+ const isFirstFailure = !test.hasFailedAttempt
1141
+ test.hasFailedAttempt = true
1142
+ const promises = {}
1143
+ testRetryCh.publish({
1144
+ err: error,
1145
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1146
+ isAtrRetry: test.isAtr && test.attempt > 0,
1147
+ isEfdRetry: test.isEarlyFlakeDetection && test.attempt > 0,
1148
+ isFirstAttempt: test.attempt === 0,
1149
+ isFirstFailure,
1150
+ promises,
1151
+ test,
1152
+ willBeRetried: true,
1153
+ ...test.currentStore,
1154
+ })
1155
+ test.retryWait = getFailedTestReplayPromise(promises)
1156
+ } else {
1157
+ testFinishCh.publish({
1158
+ hasBeenRetried: test.isAtr && test.attempt > 0,
1159
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1160
+ isAtrRetry: false,
1161
+ isLastRetry: false,
1162
+ status,
1163
+ ...test.currentStore,
1164
+ })
1165
+ }
1166
+ }
1167
+
1168
+ /**
1169
+ * Advances a Jasmine test and starts its next attempt span.
1170
+ *
1171
+ * @param {object} test
1172
+ * @returns {void}
1173
+ */
1174
+ #startNextWebdriverioJasmineAttempt (test) {
1175
+ test.attempt++
1176
+ test.reportedStatus = undefined
1177
+ this.#startWebdriverioJasmineAttempt(test, test.currentStore)
1178
+ }
1179
+
1180
+ /**
1181
+ * Finishes an intermediate native WebdriverIO retry and starts its next span.
1182
+ *
1183
+ * @param {object} test
1184
+ * @param {Error|undefined} error
1185
+ * @returns {Promise<void>|undefined}
1186
+ */
1187
+ #retryWebdriverioJasmineTest (test, error) {
1188
+ test.statuses.push('fail')
1189
+ const isFirstFailure = !test.hasFailedAttempt
1190
+ test.hasFailedAttempt = true
1191
+ const promises = {}
1192
+ testRetryCh.publish({
1193
+ err: error,
1194
+ isAttemptToFixRetry: false,
1195
+ isAtrRetry: false,
1196
+ isEfdRetry: false,
1197
+ isFirstAttempt: test.attempt === 0,
1198
+ isFirstFailure,
1199
+ promises,
1200
+ test,
1201
+ willBeRetried: true,
1202
+ ...test.currentStore,
1203
+ })
1204
+
1205
+ const retryWait = getFailedTestReplayPromise(promises)
1206
+ if (retryWait?.then) {
1207
+ const startNextAttempt = () => this.#startNextWebdriverioJasmineAttempt(test)
1208
+ return retryWait.then(startNextAttempt, startNextAttempt)
1209
+ }
1210
+ this.#startNextWebdriverioJasmineAttempt(test)
719
1211
  }
720
1212
 
721
1213
  /**
@@ -723,7 +1215,7 @@ class MochaPlugin extends CiPlugin {
723
1215
  *
724
1216
  * @param {WebdriverioJasmineResult|undefined} result
725
1217
  * @param {string[]} [specs]
726
- * @returns {void}
1218
+ * @returns {Promise<void>|undefined}
727
1219
  */
728
1220
  #finishWebdriverioJasmineTest (result, specs) {
729
1221
  const state = this._webdriverioJasmineState
@@ -737,20 +1229,86 @@ class MochaPlugin extends CiPlugin {
737
1229
  return
738
1230
  }
739
1231
 
740
- const status = getJasmineStatus(result.status)
1232
+ if (state.currentResult?.id === result.id) {
1233
+ state.currentResult = undefined
1234
+ }
1235
+ if (test.willRetry) {
1236
+ this.#finishWebdriverioJasmineRetry(test, result)
1237
+ return
1238
+ }
1239
+ if (test._ddShouldWaitForHitProbe) {
1240
+ delete test._ddShouldWaitForHitProbe
1241
+ state.pendingTestFinishes++
1242
+ const finishTest = () => {
1243
+ this.#completeWebdriverioJasmineTest(test, result)
1244
+ state.pendingTestFinishes--
1245
+ if (state.pendingTestFinishes === 0) {
1246
+ const callbacks = state.pendingTestFinishCallbacks
1247
+ state.pendingTestFinishCallbacks = []
1248
+ for (const callback of callbacks) {
1249
+ callback()
1250
+ }
1251
+ }
1252
+ }
1253
+ return this.waitForDiBreakpointHits().then(finishTest, finishTest)
1254
+ }
1255
+
1256
+ this.#completeWebdriverioJasmineTest(test, result)
1257
+ }
1258
+
1259
+ /**
1260
+ * Finalizes the last reported attempt for one WebdriverIO Jasmine test.
1261
+ *
1262
+ * @param {object} test
1263
+ * @param {WebdriverioJasmineResult} result
1264
+ * @returns {void}
1265
+ */
1266
+ #completeWebdriverioJasmineTest (test, result) {
1267
+ const state = this._webdriverioJasmineState
1268
+
1269
+ const status = getJasmineStatus(test.reportedStatus || result.status)
741
1270
  const error = getJasmineError(result)
742
1271
  if (error) {
743
1272
  test.span.setTag('error', error)
744
1273
  }
745
- testFinishCh.publish({ span: test.span, status })
746
- state.tests.delete(result.id)
747
- if (state.currentResult?.id === result.id) {
748
- state.currentResult = undefined
1274
+ if (!test.isEarlyFlakeDetection || status !== 'skip') {
1275
+ test.statuses.push(status)
1276
+ }
1277
+ const hasFailedAllRetries = test.attempt > 0 &&
1278
+ (test.isAttemptToFix || test.isAtr || test.isEarlyFlakeDetection) &&
1279
+ test.statuses.every(testStatus => testStatus === 'fail')
1280
+ const isSkipped = status === 'skip'
1281
+ const attemptToFixPassed = !isSkipped && test.isAttemptToFix &&
1282
+ test.statuses.every(testStatus => testStatus === 'pass')
1283
+ const attemptToFixFailed = !isSkipped && test.isAttemptToFix && !attemptToFixPassed
1284
+ let finalStatus = status
1285
+ if (isSkipped || (!test.isAttemptToFix && (test.isDisabled || test.isQuarantined))) {
1286
+ finalStatus = 'skip'
1287
+ } else if (test.isAttemptToFix) {
1288
+ finalStatus = attemptToFixPassed ? 'pass' : 'fail'
1289
+ } else if (test.isEarlyFlakeDetection && status !== 'skip' && !test.hasFinalHookFailure) {
1290
+ finalStatus = test.statuses.includes('pass') ? 'pass' : 'fail'
749
1291
  }
1292
+ testFinishCh.publish({
1293
+ attemptToFixFailed,
1294
+ attemptToFixPassed,
1295
+ earlyFlakeAbortReason: test.earlyFlakeAbortReason,
1296
+ finalStatus,
1297
+ hasBeenRetried: !test.isAttemptToFix && !test.isEarlyFlakeDetection && test.attempt > 0,
1298
+ hasFailedAllRetries,
1299
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1300
+ isAtrRetry: test.isAtr && test.attempt > 0,
1301
+ isLastRetry: true,
1302
+ status,
1303
+ ...test.currentStore,
1304
+ })
1305
+ state.completedTestStatuses.set(result.id, test.runnerStatus || result.status)
1306
+ state.tests.delete(result.id)
750
1307
 
1308
+ const suiteStatus = test.isQuarantined && !test.isAttemptToFix ? 'pass' : finalStatus
751
1309
  const previousStatus = state.suiteStatuses.get(test.testSuiteAbsolutePath)
752
- if (status === 'fail' || !previousStatus || previousStatus === 'skip') {
753
- state.suiteStatuses.set(test.testSuiteAbsolutePath, status)
1310
+ if (suiteStatus === 'fail' || !previousStatus || previousStatus === 'skip') {
1311
+ state.suiteStatuses.set(test.testSuiteAbsolutePath, suiteStatus)
754
1312
  }
755
1313
  }
756
1314
 
@@ -765,28 +1323,28 @@ class MochaPlugin extends CiPlugin {
765
1323
  */
766
1324
  #finishWebdriverioJasmineWorker (context) {
767
1325
  const state = this._webdriverioJasmineState
768
- const results = []
769
- const reportedFiles = new Set()
770
- for (const [file, status] of state.suiteStatuses) {
771
- const error = state.suiteErrors.get(file)
772
- const result = { file, status }
773
- if (error) {
774
- result.error = {
775
- message: error.message,
776
- stack: error.stack,
1326
+ const reportWorker = onDone => {
1327
+ const results = []
1328
+ const reportedFiles = new Set()
1329
+ for (const [file, status] of state.suiteStatuses) {
1330
+ const error = state.suiteErrors.get(file)
1331
+ const result = { file, status }
1332
+ if (error) {
1333
+ result.error = {
1334
+ message: error.message,
1335
+ stack: error.stack,
1336
+ }
777
1337
  }
1338
+ results.push(result)
1339
+ reportedFiles.add(file)
778
1340
  }
779
- results.push(result)
780
- reportedFiles.add(file)
781
- }
782
- for (const spec of state.specs) {
783
- const file = normalizeJasmineFile(spec)
784
- if (!reportedFiles.has(file)) {
785
- results.push({ file, status: 'skip' })
1341
+ for (const spec of state.specs) {
1342
+ const file = normalizeJasmineFile(spec)
1343
+ if (!reportedFiles.has(file)) {
1344
+ results.push({ file, status: 'skip' })
1345
+ }
786
1346
  }
787
- }
788
1347
 
789
- const waitForWorker = onDone => {
790
1348
  sendWebdriverioWorkerMessage({
791
1349
  origin: 'datadog',
792
1350
  name: SUITE_FINISH,
@@ -797,6 +1355,14 @@ class MochaPlugin extends CiPlugin {
797
1355
  }
798
1356
  }, () => workerFinishCh.publish({ onDone }))
799
1357
  }
1358
+
1359
+ const waitForWorker = onDone => {
1360
+ if (state?.pendingTestFinishes) {
1361
+ state.pendingTestFinishCallbacks.push(() => reportWorker(onDone))
1362
+ } else {
1363
+ reportWorker(onDone)
1364
+ }
1365
+ }
800
1366
  context.resolveCallback = waitForWorker
801
1367
  context.rejectCallback = waitForWorker
802
1368
  }