dd-trace 6.9.0 → 6.10.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 (59) hide show
  1. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  2. package/index.d.ts +42 -0
  3. package/package.json +4 -3
  4. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  5. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
  6. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  10. package/packages/datadog-instrumentations/src/jest.js +1 -1
  11. package/packages/datadog-instrumentations/src/mocha/main.js +1 -1
  12. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -8
  13. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  14. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  15. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  16. package/packages/datadog-instrumentations/src/pg.js +116 -92
  17. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  18. package/packages/datadog-instrumentations/src/vitest-main.js +169 -17
  19. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  21. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  22. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  23. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
  24. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  25. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +1 -1
  26. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  27. package/packages/datadog-plugin-jest/src/index.js +5 -4
  28. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  29. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  30. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  31. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  32. package/packages/datadog-plugin-mocha/src/index.js +550 -39
  33. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  34. package/packages/datadog-plugin-next/src/index.js +1 -19
  35. package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
  36. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  37. package/packages/datadog-plugin-pg/src/index.js +1 -0
  38. package/packages/datadog-plugin-vitest/src/index.js +8 -20
  39. package/packages/datadog-plugin-ws/src/util.js +2 -1
  40. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  41. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  42. package/packages/dd-trace/src/carrier.js +356 -0
  43. package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
  44. package/packages/dd-trace/src/config/supported-configurations.json +10 -0
  45. package/packages/dd-trace/src/datastreams/context.js +6 -1
  46. package/packages/dd-trace/src/datastreams/manager.js +5 -1
  47. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  48. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  49. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  50. package/packages/dd-trace/src/llmobs/index.js +34 -2
  51. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  52. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  53. package/packages/dd-trace/src/llmobs/util.js +78 -0
  54. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  55. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  56. package/packages/dd-trace/src/plugin_manager.js +2 -0
  57. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +1 -62
  58. package/packages/dd-trace/src/plugins/util/test.js +84 -5
  59. package/packages/dd-trace/src/plugins/util/web.js +53 -2
@@ -1,11 +1,16 @@
1
1
  'use strict'
2
2
 
3
+ const { performance } = require('node:perf_hooks')
3
4
  const { fileURLToPath } = require('node:url')
4
5
 
5
6
  const { channel } = require('dc-polyfill')
6
7
 
7
8
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
8
9
  const { storage } = require('../../datadog-core')
10
+ const {
11
+ getEfdRetryCountForDuration,
12
+ hasEfdRetries,
13
+ } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
9
14
  const log = require('../../dd-trace/src/log')
10
15
  const {
11
16
  sendWebdriverioWorkerMessage,
@@ -44,6 +49,8 @@ const {
44
49
  TEST_FINAL_STATUS,
45
50
  TEST_HAS_DYNAMIC_NAME,
46
51
  TEST_FRAMEWORK_ADAPTER,
52
+ DYNAMIC_NAME_RE,
53
+ getFailedTestReplayPromise,
47
54
  getTestSuiteExecutionKey,
48
55
  isModifiedTest,
49
56
  } = require('../../dd-trace/src/plugins/util/test')
@@ -62,14 +69,24 @@ const {
62
69
 
63
70
  const jasmineAdapterRunAsyncEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineAdapter_run:asyncEnd'
64
71
  const jasmineDoneCh = 'ci:webdriverio:jasmine:done'
72
+ const jasmineExecuteAsyncEndCh = 'tracing:orchestrion:@wdio/utils:executeAsync:asyncEnd'
73
+ const jasmineExecuteAsyncErrorCh = 'tracing:orchestrion:@wdio/utils:executeAsync:error'
74
+ const jasmineExecuteAsyncStartCh = 'tracing:orchestrion:@wdio/utils:executeAsync:start'
65
75
  const jasmineReporterSpecDoneEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specDone:end'
76
+ const jasmineReporterSpecDoneStartCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specDone:start'
66
77
  const jasmineReporterSpecStartedEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_specStarted:end'
67
78
  const jasmineReporterSuiteDoneEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_suiteDone:end'
68
79
  const jasmineReporterSuiteStartedEndCh = 'tracing:orchestrion:@wdio/jasmine-framework:JasmineReporter_suiteStarted:end'
80
+ const jasmineSpecAttemptDoneEndCh = 'tracing:orchestrion:jasmine-core:Spec_attemptDone:end'
81
+ const jasmineSpecExecuteStartCh = 'tracing:orchestrion:jasmine-core:Spec_execute:start'
69
82
  const jasmineTestFunctionStartCh = 'tracing:orchestrion:@wdio/utils:testFrameworkFnWrapper:start'
70
83
  const testFinishCh = channel('ci:mocha:test:finish')
84
+ const testRetryCh = channel('ci:mocha:test:retry')
71
85
  const WEBDRIVERIO_JASMINE_ADAPTER = 'jasmine'
72
86
  const workerFinishCh = channel('ci:mocha:worker:finish')
87
+ const WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT = Symbol('webdriverioJasmineFailedExpectationCount')
88
+ const WEBDRIVERIO_JASMINE_FUNCTION_TYPE = Symbol('webdriverioJasmineFunctionType')
89
+ const WEBDRIVERIO_JASMINE_TEST = Symbol('webdriverioJasmineTest')
73
90
 
74
91
  /**
75
92
  * @typedef {object} WebdriverioJasmineResult
@@ -170,7 +187,10 @@ class MochaPlugin extends CiPlugin {
170
187
  this._setRepositoryRoot(repositoryRoot)
171
188
  if (testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
172
189
  this._webdriverioJasmineState = {
190
+ completedTestStatuses: new Map(),
173
191
  currentResult: undefined,
192
+ pendingTestFinishCallbacks: [],
193
+ pendingTestFinishes: 0,
174
194
  specs: specs || [],
175
195
  suiteErrors: new Map(),
176
196
  suiteFiles: new Map(),
@@ -188,11 +208,82 @@ class MochaPlugin extends CiPlugin {
188
208
  const result = this._webdriverioJasmineState?.currentResult
189
209
  const type = ctx.arguments?.[1]
190
210
  if (type === 'Test' || (type === 'Hook' && result)) {
191
- return this.#startWebdriverioJasmineTest(result)
211
+ return this.#configureWebdriverioJasmineFunction(ctx, result, type)
212
+ }
213
+ return storage('legacy').getStore()
214
+ })
215
+
216
+ this.addBind(jasmineExecuteAsyncStartCh, (ctx) => {
217
+ const currentStore = storage('legacy').getStore()
218
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
219
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER || !test) {
220
+ return currentStore
221
+ }
222
+
223
+ const functionType = currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE]
224
+ if (functionType === 'Test') {
225
+ ctx.retryCallback = error => this.#retryWebdriverioJasmineTest(test, error)
226
+ }
227
+ const nextStore = {
228
+ ...test.currentStore,
229
+ [WEBDRIVERIO_JASMINE_FUNCTION_TYPE]: functionType,
230
+ }
231
+ if (functionType === 'Hook') {
232
+ nextStore[WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT] =
233
+ this._webdriverioJasmineState.currentResult?.failedExpectations?.length || 0
234
+ }
235
+ return nextStore
236
+ })
237
+
238
+ this.addBind(jasmineSpecExecuteStartCh, (ctx) => {
239
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
240
+ return storage('legacy').getStore()
192
241
  }
242
+
243
+ this.#configureWebdriverioJasmineLifecycle(ctx)
193
244
  return storage('legacy').getStore()
194
245
  })
195
246
 
247
+ this.addSub(jasmineSpecAttemptDoneEndCh, (ctx) => {
248
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
249
+ return
250
+ }
251
+
252
+ const status = typeof ctx.result === 'string' ? ctx.result : ctx.self?.result?.status
253
+ const runnerStatus = this.#prepareWebdriverioJasmineAttempt(ctx.self, status)
254
+ if (typeof ctx.result === 'string') {
255
+ ctx.result = runnerStatus
256
+ } else if (ctx.self?.result) {
257
+ ctx.self.result.status = runnerStatus
258
+ }
259
+ })
260
+
261
+ this.addSub(jasmineExecuteAsyncErrorCh, (ctx) => {
262
+ const currentStore = ctx.currentStore || storage('legacy').getStore()
263
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
264
+ if (
265
+ this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER &&
266
+ test &&
267
+ currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE] === 'Hook'
268
+ ) {
269
+ test.hasFinalHookFailure = true
270
+ }
271
+ })
272
+
273
+ this.addSub(jasmineExecuteAsyncEndCh, (ctx) => {
274
+ const currentStore = ctx.currentStore || storage('legacy').getStore()
275
+ const test = currentStore?.[WEBDRIVERIO_JASMINE_TEST]
276
+ const failedExpectationCount = currentStore?.[WEBDRIVERIO_JASMINE_FAILED_EXPECTATION_COUNT]
277
+ if (
278
+ this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER &&
279
+ test &&
280
+ currentStore[WEBDRIVERIO_JASMINE_FUNCTION_TYPE] === 'Hook' &&
281
+ this._webdriverioJasmineState.currentResult?.failedExpectations?.length > failedExpectationCount
282
+ ) {
283
+ test.hasFinalHookFailure = true
284
+ }
285
+ })
286
+
196
287
  this.addSub(jasmineReporterSuiteStartedEndCh, (ctx) => {
197
288
  if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
198
289
  const suite = ctx.arguments?.[0]
@@ -239,9 +330,32 @@ class MochaPlugin extends CiPlugin {
239
330
  }
240
331
  })
241
332
 
333
+ this.addBind(jasmineReporterSpecDoneStartCh, (ctx) => {
334
+ if (this.testFrameworkAdapter !== WEBDRIVERIO_JASMINE_ADAPTER) {
335
+ return storage('legacy').getStore()
336
+ }
337
+
338
+ const result = ctx.arguments?.[0]
339
+ const test = this._webdriverioJasmineState?.tests.get(result?.id)
340
+ const recoveredEarlyFlakeDetection = test?.isEarlyFlakeDetection &&
341
+ !test.hasFinalHookFailure && test.statuses.includes('pass')
342
+ if (
343
+ result.status === 'failed' &&
344
+ ((test?.isQuarantined && !test.isAttemptToFix) || recoveredEarlyFlakeDetection)
345
+ ) {
346
+ test.reportedStatus = result.status
347
+ result.status = 'passed'
348
+ }
349
+ return test?.currentStore || storage('legacy').getStore()
350
+ })
351
+
242
352
  this.addSub(jasmineReporterSpecDoneEndCh, (ctx) => {
243
353
  if (this.testFrameworkAdapter === WEBDRIVERIO_JASMINE_ADAPTER) {
244
- this.#finishWebdriverioJasmineTest(ctx.arguments?.[0], ctx.self?._specs)
354
+ const result = ctx.result
355
+ const finishPromise = this.#finishWebdriverioJasmineTest(ctx.arguments?.[0], ctx.self?._specs)
356
+ if (finishPromise) {
357
+ ctx.result = finishPromise.then(() => result)
358
+ }
245
359
  }
246
360
  })
247
361
 
@@ -510,10 +624,13 @@ class MochaPlugin extends CiPlugin {
510
624
  this.addSub('ci:mocha:test:retry', ({
511
625
  span,
512
626
  isFirstAttempt,
627
+ isFirstFailure = isFirstAttempt,
513
628
  willBeRetried,
514
629
  err,
515
630
  test,
631
+ isAttemptToFixRetry,
516
632
  isAtrRetry,
633
+ isEfdRetry,
517
634
  promises,
518
635
  }) => {
519
636
  if (span) {
@@ -528,8 +645,12 @@ class MochaPlugin extends CiPlugin {
528
645
  span.setTag(TEST_STATUS, 'fail')
529
646
  if (!isFirstAttempt) {
530
647
  span.setTag(TEST_IS_RETRY, 'true')
531
- if (isAtrRetry) {
648
+ if (isAttemptToFixRetry) {
649
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atf)
650
+ } else if (isAtrRetry) {
532
651
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
652
+ } else if (isEfdRetry) {
653
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
533
654
  } else {
534
655
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext)
535
656
  }
@@ -543,7 +664,7 @@ class MochaPlugin extends CiPlugin {
543
664
  'test',
544
665
  this.getTestTelemetryTags(span)
545
666
  )
546
- if (isFirstAttempt && willBeRetried && this.di && this.libraryConfig?.isDiEnabled) {
667
+ if (isFirstFailure && willBeRetried && this.di && this.libraryConfig?.isDiEnabled) {
547
668
  const probeInformation = this.addDiProbe(err)
548
669
  if (probeInformation) {
549
670
  const { file, line, stackIndex, setProbePromise } = probeInformation
@@ -557,7 +678,7 @@ class MochaPlugin extends CiPlugin {
557
678
  }
558
679
  }
559
680
 
560
- if (!isFirstAttempt &&
681
+ if (!isFirstFailure &&
561
682
  willBeRetried &&
562
683
  this.di &&
563
684
  this.libraryConfig?.isDiEnabled &&
@@ -683,6 +804,9 @@ class MochaPlugin extends CiPlugin {
683
804
  if (!state || !result?.id) {
684
805
  return currentStore
685
806
  }
807
+ if (state.completedTestStatuses.has(result.id)) {
808
+ return currentStore
809
+ }
686
810
 
687
811
  const existingTest = state.tests.get(result.id)
688
812
  if (existingTest) {
@@ -702,20 +826,333 @@ class MochaPlugin extends CiPlugin {
702
826
  return currentStore
703
827
  }
704
828
 
705
- const span = this.startTestSpan({
706
- testName: result.fullName || result.description,
829
+ const testName = result.fullName || result.description
830
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.sourceRoot)
831
+ const properties = this.libraryConfig?.testManagementTests?.mocha
832
+ ?.suites?.[testSuite]?.tests?.[testName]?.properties || {}
833
+ const isAttemptToFix = this.libraryConfig?.isTestManagementTestsEnabled && properties.attempt_to_fix
834
+ const isDisabled = this.libraryConfig?.isTestManagementTestsEnabled && properties.disabled
835
+ const isQuarantined = this.libraryConfig?.isTestManagementTestsEnabled && properties.quarantined
836
+ const isModified = this.libraryConfig?.isImpactedTestsEnabled && isModifiedTest(
837
+ getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot || this.sourceRoot),
838
+ null,
839
+ null,
840
+ this.libraryConfig.modifiedFiles,
841
+ this.constructor.id
842
+ )
843
+ const knownTests = this.libraryConfig?.knownTests?.mocha
844
+ const isNew = this.libraryConfig?.isKnownTestsEnabled && knownTests &&
845
+ !(knownTests[testSuite] || []).includes(testName)
846
+ const isEarlyFlakeDetection = this.libraryConfig?.isEarlyFlakeDetectionEnabled &&
847
+ hasEfdRetries(this.libraryConfig.earlyFlakeDetectionRetryPolicy) &&
848
+ !isAttemptToFix && !isDisabled && (isNew || isModified)
849
+ const isAtr = this.libraryConfig?.isFlakyTestRetriesEnabled &&
850
+ !isAttemptToFix && !isEarlyFlakeDetection
851
+ let retryCount = 0
852
+ if (isAttemptToFix) {
853
+ retryCount = this.libraryConfig.testManagementAttemptToFixRetries
854
+ } else if (isEarlyFlakeDetection) {
855
+ retryCount = this.libraryConfig.earlyFlakeDetectionRetryPolicy.schedulingRetryCount
856
+ } else if (isAtr) {
857
+ retryCount = this.libraryConfig.flakyTestRetriesCount
858
+ }
859
+
860
+ const test = {
861
+ attempt: 0,
862
+ attemptStart: undefined,
863
+ currentStore: undefined,
864
+ earlyFlakeAbortReason: undefined,
865
+ hasDynamicName: isNew && DYNAMIC_NAME_RE.test(testName),
866
+ hasFailedAttempt: false,
867
+ hasFinalHookFailure: false,
868
+ isAttemptToFix,
869
+ isAtr,
870
+ isDisabled,
871
+ isEarlyFlakeDetection,
872
+ isModified,
873
+ isNew,
874
+ isQuarantined,
875
+ reportedStatus: undefined,
876
+ retryCount,
877
+ retryWait: undefined,
878
+ runnerStatus: undefined,
879
+ span: undefined,
880
+ statuses: [],
881
+ testName,
707
882
  testSuiteAbsolutePath,
708
883
  title: result.description,
884
+ }
885
+ state.tests.set(result.id, test)
886
+ this.#startWebdriverioJasmineAttempt(test, currentStore)
887
+
888
+ return test.currentStore
889
+ }
890
+
891
+ /**
892
+ * Applies a managed Jasmine test's skip and retry policy to WebdriverIO's function wrapper.
893
+ *
894
+ * @param {object} context
895
+ * @param {WebdriverioJasmineResult|undefined} result
896
+ * @param {'Test'|'Hook'} type
897
+ * @returns {object|undefined}
898
+ */
899
+ #configureWebdriverioJasmineFunction (context, result, type) {
900
+ const currentStore = this.#startWebdriverioJasmineTest(result)
901
+ const test = this._webdriverioJasmineState?.tests.get(result?.id)
902
+ if (!test) {
903
+ return currentStore
904
+ }
905
+
906
+ if (test.isAttemptToFix || test.isAtr || test.isEarlyFlakeDetection) {
907
+ context.arguments[6] = 0
908
+ }
909
+ return {
910
+ ...test.currentStore,
911
+ [WEBDRIVERIO_JASMINE_FUNCTION_TYPE]: type,
912
+ }
913
+ }
914
+
915
+ /**
916
+ * Starts one WebdriverIO Jasmine test attempt under its suite span.
917
+ *
918
+ * @param {object} test
919
+ * @param {object|undefined} parentStore
920
+ * @returns {void}
921
+ */
922
+ #startWebdriverioJasmineAttempt (test, parentStore) {
923
+ test.attemptStart = performance.now()
924
+ test.hasFinalHookFailure = false
925
+ const span = this.startTestSpan({
926
+ hasDynamicName: test.hasDynamicName,
927
+ isAttemptToFix: test.isAttemptToFix,
928
+ isDisabled: test.isDisabled,
929
+ isEfdRetry: test.isEarlyFlakeDetection && test.attempt > 0,
930
+ isModified: test.isModified,
931
+ isNew: test.isNew,
932
+ isParallel: true,
933
+ isQuarantined: test.isQuarantined,
934
+ testName: test.testName,
935
+ testSuiteAbsolutePath: test.testSuiteAbsolutePath,
936
+ title: test.title,
709
937
  })
710
- const testStore = { ...currentStore, span }
711
- state.tests.set(result.id, {
712
- currentStore: testStore,
938
+ test.span = span
939
+ test.currentStore = {
940
+ ...parentStore,
713
941
  span,
714
- testSuiteAbsolutePath,
715
- })
942
+ [WEBDRIVERIO_JASMINE_TEST]: test,
943
+ }
716
944
  this.activeTestSpan = span
945
+ }
946
+
947
+ /**
948
+ * Delays Jasmine's parent runner until every Datadog-managed spec execution has completed.
949
+ *
950
+ * @param {object} context
951
+ * @returns {void}
952
+ */
953
+ #configureWebdriverioJasmineLifecycle (context) {
954
+ const isLegacyJasmine = Boolean(context.self?.queueableFn)
955
+ const spec = isLegacyJasmine ? context.self : context.arguments?.[0]
956
+ const onComplete = context.arguments?.[1]
957
+ const originalTestFunction = spec?.queueableFn?.fn
958
+ if (!spec?.id || typeof onComplete !== 'function' || typeof originalTestFunction !== 'function') {
959
+ return
960
+ }
961
+
962
+ if (this.libraryConfig?.isTestManagementTestsEnabled) {
963
+ this.#startWebdriverioJasmineTest(spec.result)
964
+ const test = this._webdriverioJasmineState?.tests.get(spec.id)
965
+ if (test?.isDisabled && !test.isAttemptToFix) {
966
+ spec.pend('Skipped by Datadog Test Management')
967
+ }
968
+ }
969
+
970
+ const executionArguments = [...context.arguments]
971
+ context.arguments[1] = (...completeArguments) => {
972
+ const test = this._webdriverioJasmineState?.tests.get(spec.id)
973
+ if (!test?.willRetry) {
974
+ return onComplete(...completeArguments)
975
+ }
976
+
977
+ test.willRetry = false
978
+ const retry = () => {
979
+ try {
980
+ this.#startNextWebdriverioJasmineAttempt(test)
981
+ spec.reset()
982
+ spec.queueableFn.fn = originalTestFunction
983
+ if (isLegacyJasmine) {
984
+ spec.execute(
985
+ executionArguments[0],
986
+ onComplete,
987
+ executionArguments[2],
988
+ executionArguments[3]
989
+ )
990
+ } else {
991
+ context.self._executeSpec(spec, onComplete)
992
+ }
993
+ } catch (error) {
994
+ log.error('WebdriverIO Jasmine retry error', error)
995
+ onComplete(...completeArguments)
996
+ }
997
+ }
998
+ const retryWait = test.retryWait
999
+ test.retryWait = undefined
1000
+ if (retryWait?.then) {
1001
+ retryWait.then(retry, retry)
1002
+ } else {
1003
+ retry()
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * Selects the runner-visible status and whether Jasmine should execute the full spec again.
1010
+ *
1011
+ * @param {object|undefined} spec
1012
+ * @param {string|undefined} status
1013
+ * @returns {string|undefined}
1014
+ */
1015
+ #prepareWebdriverioJasmineAttempt (spec, status) {
1016
+ const state = this._webdriverioJasmineState
1017
+ const completedStatus = state?.completedTestStatuses.get(spec?.id)
1018
+ if (completedStatus) {
1019
+ return completedStatus
1020
+ }
717
1021
 
718
- return testStore
1022
+ const test = state?.tests.get(spec?.id)
1023
+ if (!test || !status) {
1024
+ return status
1025
+ }
1026
+
1027
+ const testStatus = getJasmineStatus(status)
1028
+ if (
1029
+ testStatus !== 'skip' &&
1030
+ test.isEarlyFlakeDetection &&
1031
+ test.attempt === 0
1032
+ ) {
1033
+ test.retryCount = getEfdRetryCountForDuration(
1034
+ performance.now() - test.attemptStart,
1035
+ this.libraryConfig.earlyFlakeDetectionRetryPolicy
1036
+ )
1037
+ if (test.retryCount === 0) {
1038
+ test.earlyFlakeAbortReason = 'slow'
1039
+ }
1040
+ }
1041
+
1042
+ const hasManagedRetry = testStatus !== 'skip' && test.attempt < test.retryCount
1043
+ test.willRetry = hasManagedRetry && (
1044
+ test.isAttemptToFix ||
1045
+ test.isEarlyFlakeDetection ||
1046
+ (test.isAtr && testStatus === 'fail' && !test.hasFinalHookFailure)
1047
+ )
1048
+
1049
+ let runnerStatus = status
1050
+ const recoveredEarlyFlakeDetection = test.isEarlyFlakeDetection &&
1051
+ !test.hasFinalHookFailure && test.statuses.includes('pass')
1052
+ if (
1053
+ testStatus === 'fail' &&
1054
+ (test.willRetry || (test.isQuarantined && !test.isAttemptToFix) || recoveredEarlyFlakeDetection)
1055
+ ) {
1056
+ runnerStatus = 'passed'
1057
+ } else if (testStatus !== 'skip' && !test.willRetry && test.isAttemptToFix) {
1058
+ runnerStatus = test.statuses.every(previousStatus => previousStatus === 'pass') && testStatus === 'pass'
1059
+ ? 'passed'
1060
+ : 'failed'
1061
+ }
1062
+
1063
+ if (runnerStatus !== status) {
1064
+ test.reportedStatus = status
1065
+ }
1066
+ if (!test.willRetry) {
1067
+ test.runnerStatus = runnerStatus
1068
+ }
1069
+ return runnerStatus
1070
+ }
1071
+
1072
+ /**
1073
+ * Finishes one Datadog-managed Jasmine attempt before the full spec is executed again.
1074
+ *
1075
+ * @param {object} test
1076
+ * @param {WebdriverioJasmineResult} result
1077
+ * @returns {void}
1078
+ */
1079
+ #finishWebdriverioJasmineRetry (test, result) {
1080
+ const status = getJasmineStatus(test.reportedStatus || result.status)
1081
+ const error = getJasmineError(result)
1082
+ test.statuses.push(status)
1083
+
1084
+ if (error) {
1085
+ const isFirstFailure = !test.hasFailedAttempt
1086
+ test.hasFailedAttempt = true
1087
+ const promises = {}
1088
+ testRetryCh.publish({
1089
+ err: error,
1090
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1091
+ isAtrRetry: test.isAtr && test.attempt > 0,
1092
+ isEfdRetry: test.isEarlyFlakeDetection && test.attempt > 0,
1093
+ isFirstAttempt: test.attempt === 0,
1094
+ isFirstFailure,
1095
+ promises,
1096
+ test,
1097
+ willBeRetried: true,
1098
+ ...test.currentStore,
1099
+ })
1100
+ test.retryWait = getFailedTestReplayPromise(promises)
1101
+ } else {
1102
+ testFinishCh.publish({
1103
+ hasBeenRetried: test.isAtr && test.attempt > 0,
1104
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1105
+ isAtrRetry: false,
1106
+ isLastRetry: false,
1107
+ status,
1108
+ ...test.currentStore,
1109
+ })
1110
+ }
1111
+ }
1112
+
1113
+ /**
1114
+ * Advances a Jasmine test and starts its next attempt span.
1115
+ *
1116
+ * @param {object} test
1117
+ * @returns {void}
1118
+ */
1119
+ #startNextWebdriverioJasmineAttempt (test) {
1120
+ test.attempt++
1121
+ test.reportedStatus = undefined
1122
+ this.#startWebdriverioJasmineAttempt(test, test.currentStore)
1123
+ }
1124
+
1125
+ /**
1126
+ * Finishes an intermediate native WebdriverIO retry and starts its next span.
1127
+ *
1128
+ * @param {object} test
1129
+ * @param {Error|undefined} error
1130
+ * @returns {Promise<void>|undefined}
1131
+ */
1132
+ #retryWebdriverioJasmineTest (test, error) {
1133
+ test.statuses.push('fail')
1134
+ const isFirstFailure = !test.hasFailedAttempt
1135
+ test.hasFailedAttempt = true
1136
+ const promises = {}
1137
+ testRetryCh.publish({
1138
+ err: error,
1139
+ isAttemptToFixRetry: false,
1140
+ isAtrRetry: false,
1141
+ isEfdRetry: false,
1142
+ isFirstAttempt: test.attempt === 0,
1143
+ isFirstFailure,
1144
+ promises,
1145
+ test,
1146
+ willBeRetried: true,
1147
+ ...test.currentStore,
1148
+ })
1149
+
1150
+ const retryWait = getFailedTestReplayPromise(promises)
1151
+ if (retryWait?.then) {
1152
+ const startNextAttempt = () => this.#startNextWebdriverioJasmineAttempt(test)
1153
+ return retryWait.then(startNextAttempt, startNextAttempt)
1154
+ }
1155
+ this.#startNextWebdriverioJasmineAttempt(test)
719
1156
  }
720
1157
 
721
1158
  /**
@@ -723,7 +1160,7 @@ class MochaPlugin extends CiPlugin {
723
1160
  *
724
1161
  * @param {WebdriverioJasmineResult|undefined} result
725
1162
  * @param {string[]} [specs]
726
- * @returns {void}
1163
+ * @returns {Promise<void>|undefined}
727
1164
  */
728
1165
  #finishWebdriverioJasmineTest (result, specs) {
729
1166
  const state = this._webdriverioJasmineState
@@ -737,20 +1174,86 @@ class MochaPlugin extends CiPlugin {
737
1174
  return
738
1175
  }
739
1176
 
740
- const status = getJasmineStatus(result.status)
1177
+ if (state.currentResult?.id === result.id) {
1178
+ state.currentResult = undefined
1179
+ }
1180
+ if (test.willRetry) {
1181
+ this.#finishWebdriverioJasmineRetry(test, result)
1182
+ return
1183
+ }
1184
+ if (test._ddShouldWaitForHitProbe) {
1185
+ delete test._ddShouldWaitForHitProbe
1186
+ state.pendingTestFinishes++
1187
+ const finishTest = () => {
1188
+ this.#completeWebdriverioJasmineTest(test, result)
1189
+ state.pendingTestFinishes--
1190
+ if (state.pendingTestFinishes === 0) {
1191
+ const callbacks = state.pendingTestFinishCallbacks
1192
+ state.pendingTestFinishCallbacks = []
1193
+ for (const callback of callbacks) {
1194
+ callback()
1195
+ }
1196
+ }
1197
+ }
1198
+ return this.waitForDiBreakpointHits().then(finishTest, finishTest)
1199
+ }
1200
+
1201
+ this.#completeWebdriverioJasmineTest(test, result)
1202
+ }
1203
+
1204
+ /**
1205
+ * Finalizes the last reported attempt for one WebdriverIO Jasmine test.
1206
+ *
1207
+ * @param {object} test
1208
+ * @param {WebdriverioJasmineResult} result
1209
+ * @returns {void}
1210
+ */
1211
+ #completeWebdriverioJasmineTest (test, result) {
1212
+ const state = this._webdriverioJasmineState
1213
+
1214
+ const status = getJasmineStatus(test.reportedStatus || result.status)
741
1215
  const error = getJasmineError(result)
742
1216
  if (error) {
743
1217
  test.span.setTag('error', error)
744
1218
  }
745
- testFinishCh.publish({ span: test.span, status })
746
- state.tests.delete(result.id)
747
- if (state.currentResult?.id === result.id) {
748
- state.currentResult = undefined
1219
+ if (!test.isEarlyFlakeDetection || status !== 'skip') {
1220
+ test.statuses.push(status)
1221
+ }
1222
+ const hasFailedAllRetries = test.attempt > 0 &&
1223
+ (test.isAttemptToFix || test.isAtr || test.isEarlyFlakeDetection) &&
1224
+ test.statuses.every(testStatus => testStatus === 'fail')
1225
+ const isSkipped = status === 'skip'
1226
+ const attemptToFixPassed = !isSkipped && test.isAttemptToFix &&
1227
+ test.statuses.every(testStatus => testStatus === 'pass')
1228
+ const attemptToFixFailed = !isSkipped && test.isAttemptToFix && !attemptToFixPassed
1229
+ let finalStatus = status
1230
+ if (isSkipped || (!test.isAttemptToFix && (test.isDisabled || test.isQuarantined))) {
1231
+ finalStatus = 'skip'
1232
+ } else if (test.isAttemptToFix) {
1233
+ finalStatus = attemptToFixPassed ? 'pass' : 'fail'
1234
+ } else if (test.isEarlyFlakeDetection && status !== 'skip' && !test.hasFinalHookFailure) {
1235
+ finalStatus = test.statuses.includes('pass') ? 'pass' : 'fail'
749
1236
  }
1237
+ testFinishCh.publish({
1238
+ attemptToFixFailed,
1239
+ attemptToFixPassed,
1240
+ earlyFlakeAbortReason: test.earlyFlakeAbortReason,
1241
+ finalStatus,
1242
+ hasBeenRetried: !test.isAttemptToFix && !test.isEarlyFlakeDetection && test.attempt > 0,
1243
+ hasFailedAllRetries,
1244
+ isAttemptToFixRetry: test.isAttemptToFix && test.attempt > 0,
1245
+ isAtrRetry: test.isAtr && test.attempt > 0,
1246
+ isLastRetry: true,
1247
+ status,
1248
+ ...test.currentStore,
1249
+ })
1250
+ state.completedTestStatuses.set(result.id, test.runnerStatus || result.status)
1251
+ state.tests.delete(result.id)
750
1252
 
1253
+ const suiteStatus = test.isQuarantined && !test.isAttemptToFix ? 'pass' : finalStatus
751
1254
  const previousStatus = state.suiteStatuses.get(test.testSuiteAbsolutePath)
752
- if (status === 'fail' || !previousStatus || previousStatus === 'skip') {
753
- state.suiteStatuses.set(test.testSuiteAbsolutePath, status)
1255
+ if (suiteStatus === 'fail' || !previousStatus || previousStatus === 'skip') {
1256
+ state.suiteStatuses.set(test.testSuiteAbsolutePath, suiteStatus)
754
1257
  }
755
1258
  }
756
1259
 
@@ -765,28 +1268,28 @@ class MochaPlugin extends CiPlugin {
765
1268
  */
766
1269
  #finishWebdriverioJasmineWorker (context) {
767
1270
  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,
1271
+ const reportWorker = onDone => {
1272
+ const results = []
1273
+ const reportedFiles = new Set()
1274
+ for (const [file, status] of state.suiteStatuses) {
1275
+ const error = state.suiteErrors.get(file)
1276
+ const result = { file, status }
1277
+ if (error) {
1278
+ result.error = {
1279
+ message: error.message,
1280
+ stack: error.stack,
1281
+ }
777
1282
  }
1283
+ results.push(result)
1284
+ reportedFiles.add(file)
778
1285
  }
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' })
1286
+ for (const spec of state.specs) {
1287
+ const file = normalizeJasmineFile(spec)
1288
+ if (!reportedFiles.has(file)) {
1289
+ results.push({ file, status: 'skip' })
1290
+ }
786
1291
  }
787
- }
788
1292
 
789
- const waitForWorker = onDone => {
790
1293
  sendWebdriverioWorkerMessage({
791
1294
  origin: 'datadog',
792
1295
  name: SUITE_FINISH,
@@ -797,6 +1300,14 @@ class MochaPlugin extends CiPlugin {
797
1300
  }
798
1301
  }, () => workerFinishCh.publish({ onDone }))
799
1302
  }
1303
+
1304
+ const waitForWorker = onDone => {
1305
+ if (state?.pendingTestFinishes) {
1306
+ state.pendingTestFinishCallbacks.push(() => reportWorker(onDone))
1307
+ } else {
1308
+ reportWorker(onDone)
1309
+ }
1310
+ }
800
1311
  context.resolveCallback = waitForWorker
801
1312
  context.rejectCallback = waitForWorker
802
1313
  }