dd-trace 5.42.0 → 5.44.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 (60) hide show
  1. package/package.json +5 -5
  2. package/packages/datadog-instrumentations/src/cucumber.js +61 -23
  3. package/packages/datadog-instrumentations/src/dd-trace-api.js +7 -0
  4. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  5. package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
  6. package/packages/datadog-instrumentations/src/jest.js +134 -48
  7. package/packages/datadog-instrumentations/src/mocha/main.js +20 -4
  8. package/packages/datadog-instrumentations/src/mocha/utils.js +89 -30
  9. package/packages/datadog-instrumentations/src/mocha/worker.js +3 -1
  10. package/packages/datadog-instrumentations/src/playwright.js +97 -17
  11. package/packages/datadog-instrumentations/src/router.js +1 -0
  12. package/packages/datadog-instrumentations/src/tedious.js +13 -10
  13. package/packages/datadog-instrumentations/src/vitest.js +96 -21
  14. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  15. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +69 -20
  16. package/packages/datadog-plugin-cypress/src/support.js +39 -10
  17. package/packages/datadog-plugin-google-cloud-vertexai/src/index.js +8 -186
  18. package/packages/datadog-plugin-google-cloud-vertexai/src/tracing.js +186 -0
  19. package/packages/datadog-plugin-google-cloud-vertexai/src/utils.js +19 -0
  20. package/packages/datadog-plugin-jest/src/index.js +38 -5
  21. package/packages/datadog-plugin-mocha/src/index.js +28 -5
  22. package/packages/datadog-plugin-playwright/src/index.js +22 -2
  23. package/packages/datadog-plugin-tedious/src/index.js +14 -9
  24. package/packages/datadog-plugin-vitest/src/index.js +46 -14
  25. package/packages/dd-trace/src/appsec/blocking.js +2 -0
  26. package/packages/dd-trace/src/appsec/graphql.js +3 -1
  27. package/packages/dd-trace/src/appsec/reporter.js +13 -8
  28. package/packages/dd-trace/src/appsec/telemetry/common.js +6 -2
  29. package/packages/dd-trace/src/appsec/telemetry/index.js +20 -4
  30. package/packages/dd-trace/src/appsec/telemetry/waf.js +74 -8
  31. package/packages/dd-trace/src/appsec/waf/waf_manager.js +16 -7
  32. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -2
  33. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +3 -1
  34. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +4 -2
  35. package/packages/dd-trace/src/config.js +4 -0
  36. package/packages/dd-trace/src/dogstatsd.js +3 -3
  37. package/packages/dd-trace/src/encode/0.4.js +108 -12
  38. package/packages/dd-trace/src/encode/0.5.js +7 -1
  39. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  40. package/packages/dd-trace/src/exporters/agent/writer.js +3 -2
  41. package/packages/dd-trace/src/format.js +34 -32
  42. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  43. package/packages/dd-trace/src/llmobs/noop.js +3 -3
  44. package/packages/dd-trace/src/llmobs/plugins/base.js +1 -1
  45. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +2 -1
  46. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +196 -0
  47. package/packages/dd-trace/src/llmobs/sdk.js +5 -3
  48. package/packages/dd-trace/src/llmobs/span_processor.js +6 -0
  49. package/packages/dd-trace/src/llmobs/tagger.js +8 -2
  50. package/packages/dd-trace/src/llmobs/telemetry.js +82 -1
  51. package/packages/dd-trace/src/llmobs/writers/spans/base.js +10 -1
  52. package/packages/dd-trace/src/log/index.js +1 -13
  53. package/packages/dd-trace/src/log/utils.js +16 -0
  54. package/packages/dd-trace/src/plugin_manager.js +0 -3
  55. package/packages/dd-trace/src/plugins/ci_plugin.js +16 -26
  56. package/packages/dd-trace/src/plugins/database.js +4 -4
  57. package/packages/dd-trace/src/plugins/plugin.js +2 -0
  58. package/packages/dd-trace/src/plugins/util/test.js +62 -1
  59. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +20 -20
  60. package/packages/dd-trace/src/telemetry/send-data.js +5 -1
@@ -9,6 +9,7 @@ const testPassCh = channel('ci:vitest:test:pass')
9
9
  const testErrorCh = channel('ci:vitest:test:error')
10
10
  const testSkipCh = channel('ci:vitest:test:skip')
11
11
  const isNewTestCh = channel('ci:vitest:test:is-new')
12
+ const isAttemptToFixCh = channel('ci:vitest:test:is-attempt-to-fix')
12
13
  const isDisabledCh = channel('ci:vitest:test:is-disabled')
13
14
  const isQuarantinedCh = channel('ci:vitest:test:is-quarantined')
14
15
 
@@ -30,7 +31,9 @@ const taskToStatuses = new WeakMap()
30
31
  const newTasks = new WeakSet()
31
32
  const disabledTasks = new WeakSet()
32
33
  const quarantinedTasks = new WeakSet()
34
+ const attemptToFixTasks = new WeakSet()
33
35
  let isRetryReasonEfd = false
36
+ let isRetryReasonAttemptToFix = false
34
37
  const switchedStatuses = new WeakSet()
35
38
  const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
36
39
 
@@ -53,6 +56,7 @@ function getProvidedContext () {
53
56
  _ddEarlyFlakeDetectionNumRetries: numRepeats,
54
57
  _ddIsKnownTestsEnabled: isKnownTestsEnabled,
55
58
  _ddIsTestManagementTestsEnabled: isTestManagementTestsEnabled,
59
+ _ddTestManagementAttemptToFixRetries: testManagementAttemptToFixRetries,
56
60
  _ddTestManagementTests: testManagementTests,
57
61
  _ddIsFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled
58
62
  } = globalThis.__vitest_worker__.providedContext
@@ -64,6 +68,7 @@ function getProvidedContext () {
64
68
  numRepeats,
65
69
  isKnownTestsEnabled,
66
70
  isTestManagementTestsEnabled,
71
+ testManagementAttemptToFixRetries,
67
72
  testManagementTests,
68
73
  isFlakyTestRetriesEnabled
69
74
  }
@@ -76,6 +81,7 @@ function getProvidedContext () {
76
81
  numRepeats: 0,
77
82
  isKnownTestsEnabled: false,
78
83
  isTestManagementTestsEnabled: false,
84
+ testManagementAttemptToFixRetries: 0,
79
85
  testManagementTests: {}
80
86
  }
81
87
  }
@@ -94,6 +100,10 @@ function isReporterPackageNewest (vitestPackage) {
94
100
  return vitestPackage.h?.name === 'BaseSequencer'
95
101
  }
96
102
 
103
+ function isBaseSequencer (vitestPackage) {
104
+ return vitestPackage.b?.name === 'BaseSequencer'
105
+ }
106
+
97
107
  function getChannelPromise (channelToPublishTo) {
98
108
  return new Promise(resolve => {
99
109
  sessionAsyncResource.runInAsyncScope(() => {
@@ -172,6 +182,7 @@ function getSortWrapper (sort) {
172
182
  let isEarlyFlakeDetectionFaulty = false
173
183
  let isKnownTestsEnabled = false
174
184
  let isTestManagementTestsEnabled = false
185
+ let testManagementAttemptToFixRetries = 0
175
186
  let isDiEnabled = false
176
187
  let knownTests = {}
177
188
  let testManagementTests = {}
@@ -186,6 +197,7 @@ function getSortWrapper (sort) {
186
197
  isDiEnabled = libraryConfig.isDiEnabled
187
198
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
188
199
  isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
200
+ testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries
189
201
  }
190
202
  } catch (e) {
191
203
  isFlakyTestRetriesEnabled = false
@@ -258,6 +270,7 @@ function getSortWrapper (sort) {
258
270
  try {
259
271
  const workspaceProject = this.ctx.getCoreWorkspaceProject()
260
272
  workspaceProject._provided._ddIsTestManagementTestsEnabled = isTestManagementTestsEnabled
273
+ workspaceProject._provided._ddTestManagementAttemptToFixRetries = testManagementAttemptToFixRetries
261
274
  workspaceProject._provided._ddTestManagementTests = testManagementTests
262
275
  } catch (e) {
263
276
  log.warn('Could not send test management tests to workers so Test Management will not work.')
@@ -349,10 +362,24 @@ addHook({
349
362
  isKnownTestsEnabled,
350
363
  numRepeats,
351
364
  isTestManagementTestsEnabled,
365
+ testManagementAttemptToFixRetries,
352
366
  testManagementTests
353
367
  } = getProvidedContext()
354
368
 
355
369
  if (isTestManagementTestsEnabled) {
370
+ isAttemptToFixCh.publish({
371
+ testManagementTests,
372
+ testSuiteAbsolutePath: task.file.filepath,
373
+ testName,
374
+ onDone: (isAttemptToFix) => {
375
+ if (isAttemptToFix) {
376
+ isRetryReasonAttemptToFix = task.repeats !== testManagementAttemptToFixRetries
377
+ task.repeats = testManagementAttemptToFixRetries
378
+ attemptToFixTasks.add(task)
379
+ taskToStatuses.set(task, [])
380
+ }
381
+ }
382
+ })
356
383
  isDisabledCh.publish({
357
384
  testManagementTests,
358
385
  testSuiteAbsolutePath: task.file.filepath,
@@ -360,7 +387,10 @@ addHook({
360
387
  onDone: (isTestDisabled) => {
361
388
  if (isTestDisabled) {
362
389
  disabledTasks.add(task)
363
- task.mode = 'skip'
390
+ if (!attemptToFixTasks.has(task)) {
391
+ // we only actually skip if the test is not being attempted to be fixed
392
+ task.mode = 'skip'
393
+ }
364
394
  }
365
395
  }
366
396
  })
@@ -372,7 +402,7 @@ addHook({
372
402
  testSuiteAbsolutePath: task.file.filepath,
373
403
  testName,
374
404
  onDone: (isNew) => {
375
- if (isNew) {
405
+ if (isNew && !attemptToFixTasks.has(task)) {
376
406
  if (isEarlyFlakeDetectionEnabled) {
377
407
  isRetryReasonEfd = task.repeats !== numRepeats
378
408
  task.repeats = numRepeats
@@ -392,19 +422,28 @@ addHook({
392
422
  shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => function (task) {
393
423
  const { isEarlyFlakeDetectionEnabled, isTestManagementTestsEnabled } = getProvidedContext()
394
424
 
395
- if (isEarlyFlakeDetectionEnabled && taskToStatuses.has(task)) {
396
- const statuses = taskToStatuses.get(task)
397
- // If the test has passed at least once, we consider it passed
398
- if (statuses.includes('pass')) {
425
+ if (isTestManagementTestsEnabled) {
426
+ const isAttemptingToFix = attemptToFixTasks.has(task)
427
+ const isDisabled = disabledTasks.has(task)
428
+ const isQuarantined = quarantinedTasks.has(task)
429
+
430
+ if (isAttemptingToFix && (isDisabled || isQuarantined)) {
399
431
  if (task.result.state === 'fail') {
400
432
  switchedStatuses.add(task)
401
433
  }
402
434
  task.result.state = 'pass'
435
+ } else if (isQuarantined) {
436
+ task.result.state = 'pass'
403
437
  }
404
438
  }
405
439
 
406
- if (isTestManagementTestsEnabled) {
407
- if (quarantinedTasks.has(task)) {
440
+ if (isEarlyFlakeDetectionEnabled && taskToStatuses.has(task) && !attemptToFixTasks.has(task)) {
441
+ const statuses = taskToStatuses.get(task)
442
+ // If the test has passed at least once, we consider it passed
443
+ if (statuses.includes('pass')) {
444
+ if (task.result.state === 'fail') {
445
+ switchedStatuses.add(task)
446
+ }
408
447
  task.result.state = 'pass'
409
448
  }
410
449
  }
@@ -477,6 +516,8 @@ addHook({
477
516
  }
478
517
 
479
518
  const lastExecutionStatus = task.result.state
519
+ const shouldFlipStatus = isEarlyFlakeDetectionEnabled || attemptToFixTasks.has(task)
520
+ const statuses = taskToStatuses.get(task)
480
521
 
481
522
  // These clauses handle task.repeats, whether EFD is enabled or not
482
523
  // The only thing that EFD does is to forcefully pass the test if it has passed at least once
@@ -497,15 +538,21 @@ addHook({
497
538
  testPassCh.publish({ task })
498
539
  })
499
540
  }
500
- if (isEarlyFlakeDetectionEnabled) {
501
- const statuses = taskToStatuses.get(task)
541
+ if (shouldFlipStatus) {
502
542
  statuses.push(lastExecutionStatus)
503
543
  // If we don't "reset" the result.state to "pass", once a repetition fails,
504
544
  // vitest will always consider the test as failed, so we can't read the actual status
545
+ // This means that we change vitest's behavior:
546
+ // if the last attempt passes, vitest would consider the test as failed
547
+ // but after this change, it will consider the test as passed
505
548
  task.result.state = 'pass'
506
549
  }
507
550
  }
508
551
  } else if (numRepetition === task.repeats) {
552
+ if (shouldFlipStatus) {
553
+ statuses.push(lastExecutionStatus)
554
+ }
555
+
509
556
  const asyncResource = taskToAsync.get(task)
510
557
  if (lastExecutionStatus === 'fail') {
511
558
  const testError = task.result?.errors?.[0]
@@ -528,8 +575,11 @@ addHook({
528
575
  testSuiteAbsolutePath: task.file.filepath,
529
576
  isRetry: numAttempt > 0 || numRepetition > 0,
530
577
  isRetryReasonEfd,
578
+ isRetryReasonAttemptToFix: isRetryReasonAttemptToFix && numRepetition > 0,
531
579
  isNew,
532
580
  mightHitProbe: isDiEnabled && numAttempt > 0,
581
+ isAttemptToFix: attemptToFixTasks.has(task),
582
+ isDisabled: disabledTasks.has(task),
533
583
  isQuarantined
534
584
  })
535
585
  })
@@ -544,6 +594,8 @@ addHook({
544
594
  }
545
595
  const result = await onAfterTryTask.apply(this, arguments)
546
596
 
597
+ const { testManagementAttemptToFixRetries } = getProvidedContext()
598
+
547
599
  const status = getVitestTestStatus(task, retryCount)
548
600
  const asyncResource = taskToAsync.get(task)
549
601
 
@@ -553,10 +605,18 @@ addHook({
553
605
  await waitForHitProbe()
554
606
  }
555
607
 
608
+ let attemptToFixPassed = false
609
+ if (attemptToFixTasks.has(task)) {
610
+ const statuses = taskToStatuses.get(task)
611
+ if (statuses.length === testManagementAttemptToFixRetries && statuses.every(status => status === 'pass')) {
612
+ attemptToFixPassed = true
613
+ }
614
+ }
615
+
556
616
  if (asyncResource) {
557
617
  // We don't finish here because the test might fail in a later hook (afterEach)
558
618
  asyncResource.runInAsyncScope(() => {
559
- testFinishTimeCh.publish({ status, task })
619
+ testFinishTimeCh.publish({ status, task, attemptToFixPassed })
560
620
  })
561
621
  }
562
622
 
@@ -615,11 +675,22 @@ addHook({
615
675
 
616
676
  addHook({
617
677
  name: 'vitest',
618
- versions: ['>=3.0.0'],
678
+ versions: ['>=3.0.9'],
679
+ filePattern: 'dist/chunks/coverage.*'
680
+ }, (coveragePackage) => {
681
+ if (isBaseSequencer(coveragePackage)) {
682
+ shimmer.wrap(coveragePackage.b.prototype, 'sort', getSortWrapper)
683
+ }
684
+ return coveragePackage
685
+ })
686
+
687
+ addHook({
688
+ name: 'vitest',
689
+ versions: ['>=3.0.0 <3.0.9'],
619
690
  filePattern: 'dist/chunks/resolveConfig.*'
620
- }, (randomSequencerPackage) => {
621
- shimmer.wrap(randomSequencerPackage.B.prototype, 'sort', getSortWrapper)
622
- return randomSequencerPackage
691
+ }, (resolveConfigPackage) => {
692
+ shimmer.wrap(resolveConfigPackage.B.prototype, 'sort', getSortWrapper)
693
+ return resolveConfigPackage
623
694
  })
624
695
 
625
696
  // Can't specify file because compiled vitest includes hashes in their files
@@ -650,15 +721,11 @@ addHook({
650
721
  // From >=3.0.1, the first arguments changes from a string to an object containing the filepath
651
722
  const testSuiteAbsolutePath = testPaths[0]?.filepath || testPaths[0]
652
723
 
653
- const { isEarlyFlakeDetectionEnabled, isFlakyTestRetriesEnabled } = getProvidedContext()
654
-
655
724
  const testSuiteAsyncResource = new AsyncResource('bound-anonymous-fn')
656
725
  testSuiteAsyncResource.runInAsyncScope(() => {
657
726
  testSuiteStartCh.publish({
658
727
  testSuiteAbsolutePath,
659
- frameworkVersion,
660
- isFlakyTestRetriesEnabled,
661
- isEarlyFlakeDetectionEnabled
728
+ frameworkVersion
662
729
  })
663
730
  })
664
731
  const startTestsResponse = await startTests.apply(this, arguments)
@@ -700,11 +767,19 @@ addHook({
700
767
  testError = errors[0]
701
768
  }
702
769
 
770
+ let hasFailedAllRetries = false
771
+ if (attemptToFixTasks.has(task)) {
772
+ const statuses = taskToStatuses.get(task)
773
+ if (statuses.every(status => status === 'fail')) {
774
+ hasFailedAllRetries = true
775
+ }
776
+ }
777
+
703
778
  if (testAsyncResource) {
704
779
  const isRetry = task.result?.retryCount > 0
705
780
  // `duration` is the duration of all the retries, so it can't be used if there are retries
706
781
  testAsyncResource.runInAsyncScope(() => {
707
- testErrorCh.publish({ duration: !isRetry ? duration : undefined, error: testError })
782
+ testErrorCh.publish({ duration: !isRetry ? duration : undefined, error: testError, hasFailedAllRetries })
708
783
  })
709
784
  }
710
785
  if (errors?.length) {
@@ -30,7 +30,10 @@ const {
30
30
  TEST_RETRY_REASON,
31
31
  TEST_MANAGEMENT_ENABLED,
32
32
  TEST_MANAGEMENT_IS_QUARANTINED,
33
- TEST_MANAGEMENT_IS_DISABLED
33
+ TEST_MANAGEMENT_IS_DISABLED,
34
+ TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX,
35
+ TEST_HAS_FAILED_ALL_RETRIES,
36
+ TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED
34
37
  } = require('../../dd-trace/src/plugins/util/test')
35
38
  const { RESOURCE_NAME } = require('../../../ext/tags')
36
39
  const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants')
@@ -328,6 +331,10 @@ class CucumberPlugin extends CiPlugin {
328
331
  isNew,
329
332
  isEfdRetry,
330
333
  isFlakyRetry,
334
+ isAttemptToFix,
335
+ isAttemptToFixRetry,
336
+ hasFailedAllRetries,
337
+ hasPassedAllRetries,
331
338
  isDisabled,
332
339
  isQuarantined
333
340
  }) => {
@@ -358,6 +365,22 @@ class CucumberPlugin extends CiPlugin {
358
365
  span.setTag(TEST_IS_RETRY, 'true')
359
366
  }
360
367
 
368
+ if (hasFailedAllRetries) {
369
+ span.setTag(TEST_HAS_FAILED_ALL_RETRIES, 'true')
370
+ }
371
+
372
+ if (isAttemptToFix) {
373
+ span.setTag(TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX, 'true')
374
+ }
375
+
376
+ if (isAttemptToFixRetry) {
377
+ span.setTag(TEST_IS_RETRY, 'true')
378
+ span.setTag(TEST_RETRY_REASON, 'attempt_to_fix')
379
+ if (hasPassedAllRetries) {
380
+ span.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'true')
381
+ }
382
+ }
383
+
361
384
  if (isDisabled) {
362
385
  span.setTag(TEST_MANAGEMENT_IS_DISABLED, 'true')
363
386
  }
@@ -37,9 +37,10 @@ const {
37
37
  TEST_MANAGEMENT_IS_QUARANTINED,
38
38
  TEST_MANAGEMENT_ENABLED,
39
39
  TEST_MANAGEMENT_IS_DISABLED,
40
- DD_CAPABILITIES_EARLY_FLAKE_DETECTION,
41
- DD_CAPABILITIES_AUTO_TEST_RETRIES,
42
- DD_CAPABILITIES_TEST_IMPACT_ANALYSIS
40
+ TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX,
41
+ TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED,
42
+ TEST_HAS_FAILED_ALL_RETRIES,
43
+ getLibraryCapabilitiesTags
43
44
  } = require('../../dd-trace/src/plugins/util/test')
44
45
  const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
45
46
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
@@ -64,7 +65,8 @@ const {
64
65
  GIT_COMMIT_SHA,
65
66
  GIT_BRANCH,
66
67
  CI_PROVIDER_NAME,
67
- CI_WORKSPACE_PATH
68
+ CI_WORKSPACE_PATH,
69
+ GIT_COMMIT_MESSAGE
68
70
  } = require('../../dd-trace/src/plugins/util/tags')
69
71
  const {
70
72
  OS_VERSION,
@@ -201,7 +203,8 @@ class CypressPlugin {
201
203
  [RUNTIME_VERSION]: runtimeVersion,
202
204
  [GIT_BRANCH]: branch,
203
205
  [CI_PROVIDER_NAME]: ciProviderName,
204
- [CI_WORKSPACE_PATH]: repositoryRoot
206
+ [CI_WORKSPACE_PATH]: repositoryRoot,
207
+ [GIT_COMMIT_MESSAGE]: commitMessage
205
208
  } = this.testEnvironmentMetadata
206
209
 
207
210
  this.repositoryRoot = repositoryRoot
@@ -217,9 +220,11 @@ class CypressPlugin {
217
220
  runtimeName,
218
221
  runtimeVersion,
219
222
  branch,
220
- testLevel: 'test'
223
+ testLevel: 'test',
224
+ commitMessage
221
225
  }
222
226
  this.finishedTestsByFile = {}
227
+ this.testStatuses = {}
223
228
 
224
229
  this.isTestsSkipped = false
225
230
  this.isSuitesSkippingEnabled = false
@@ -233,6 +238,8 @@ class CypressPlugin {
233
238
  this.hasUnskippableSuites = false
234
239
  this.unskippableSuites = []
235
240
  this.knownTests = []
241
+ this.isTestManagementTestsEnabled = false
242
+ this.testManagementAttemptToFixRetries = 0
236
243
  }
237
244
 
238
245
  // Init function returns a promise that resolves with the Cypress configuration
@@ -261,7 +268,8 @@ class CypressPlugin {
261
268
  isFlakyTestRetriesEnabled,
262
269
  flakyTestRetriesCount,
263
270
  isKnownTestsEnabled,
264
- isTestManagementEnabled
271
+ isTestManagementEnabled,
272
+ testManagementAttemptToFixRetries
265
273
  }
266
274
  } = libraryConfigurationResponse
267
275
  this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
@@ -273,17 +281,22 @@ class CypressPlugin {
273
281
  this.cypressConfig.retries.runMode = flakyTestRetriesCount
274
282
  }
275
283
  this.isTestManagementTestsEnabled = isTestManagementEnabled
284
+ this.testManagementAttemptToFixRetries = testManagementAttemptToFixRetries
276
285
  }
277
286
  return this.cypressConfig
278
287
  })
279
288
  return this.libraryConfigurationPromise
280
289
  }
281
290
 
291
+ getTestSuiteProperties (testSuite) {
292
+ return this.testManagementTests?.cypress?.suites?.[testSuite]?.tests || {}
293
+ }
294
+
282
295
  getTestProperties (testSuite, testName) {
283
- const { disabled: isDisabled, quarantined: isQuarantined } =
284
- this.testManagementTests?.cypress?.suites?.[testSuite]?.tests?.[testName]?.properties || {}
296
+ const { attempt_to_fix: isAttemptToFix, disabled: isDisabled, quarantined: isQuarantined } =
297
+ this.getTestSuiteProperties(testSuite)?.[testName]?.properties || {}
285
298
 
286
- return { isDisabled, isQuarantined }
299
+ return { isAttemptToFix, isDisabled, isQuarantined }
287
300
  }
288
301
 
289
302
  getTestSuiteSpan ({ testSuite, testSuiteAbsolutePath }) {
@@ -312,7 +325,7 @@ class CypressPlugin {
312
325
  })
313
326
  }
314
327
 
315
- getTestSpan ({ testName, testSuite, isUnskippable, isForcedToRun, testSourceFile }) {
328
+ getTestSpan ({ testName, testSuite, isUnskippable, isForcedToRun, testSourceFile, isDisabled, isQuarantined }) {
316
329
  const testSuiteTags = {
317
330
  [TEST_COMMAND]: this.command,
318
331
  [TEST_COMMAND]: this.command,
@@ -357,6 +370,14 @@ class CypressPlugin {
357
370
  testSpanMetadata[TEST_ITR_FORCED_RUN] = 'true'
358
371
  }
359
372
 
373
+ if (isDisabled) {
374
+ testSpanMetadata[TEST_MANAGEMENT_IS_DISABLED] = 'true'
375
+ }
376
+
377
+ if (isQuarantined) {
378
+ testSpanMetadata[TEST_MANAGEMENT_IS_QUARANTINED] = 'true'
379
+ }
380
+
360
381
  this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'test', { hasCodeOwners: !!codeOwners })
361
382
 
362
383
  return this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test`, {
@@ -459,11 +480,10 @@ class CypressPlugin {
459
480
  [TEST_SESSION_NAME]: testSessionName
460
481
  }
461
482
  }
483
+ const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id)
462
484
  metadataTags.test = {
463
485
  ...metadataTags.test,
464
- [DD_CAPABILITIES_TEST_IMPACT_ANALYSIS]: this.isSuitesSkippingEnabled ? 'true' : 'false',
465
- [DD_CAPABILITIES_EARLY_FLAKE_DETECTION]: this.isEarlyFlakeDetectionEnabled ? 'true' : 'false',
466
- [DD_CAPABILITIES_AUTO_TEST_RETRIES]: this.isFlakyTestRetriesEnabled ? 'true' : 'false'
486
+ ...libraryCapabilitiesTags
467
487
  }
468
488
 
469
489
  this.tracer._tracer._exporter.addMetadataTags(metadataTags)
@@ -691,7 +711,10 @@ class CypressPlugin {
691
711
  isEarlyFlakeDetectionEnabled: this.isEarlyFlakeDetectionEnabled,
692
712
  knownTestsForSuite: this.knownTestsByTestSuite?.[testSuite] || [],
693
713
  earlyFlakeDetectionNumRetries: this.earlyFlakeDetectionNumRetries,
694
- isKnownTestsEnabled: this.isKnownTestsEnabled
714
+ isKnownTestsEnabled: this.isKnownTestsEnabled,
715
+ isTestManagementEnabled: this.isTestManagementTestsEnabled,
716
+ testManagementAttemptToFixRetries: this.testManagementAttemptToFixRetries,
717
+ testManagementTests: this.getTestSuiteProperties(testSuite)
695
718
  }
696
719
 
697
720
  if (this.testSuiteSpan) {
@@ -707,8 +730,7 @@ class CypressPlugin {
707
730
  })
708
731
  const isUnskippable = this.unskippableSuites.includes(testSuite)
709
732
  const isForcedToRun = shouldSkip && isUnskippable
710
- const { isDisabled, isQuarantined } = this.getTestProperties(testSuite, testName)
711
-
733
+ const { isAttemptToFix, isDisabled, isQuarantined } = this.getTestProperties(testSuite, testName)
712
734
  // skip test
713
735
  if (shouldSkip && !isUnskippable) {
714
736
  this.skippedTests.push(test)
@@ -718,7 +740,7 @@ class CypressPlugin {
718
740
 
719
741
  // TODO: I haven't found a way to trick cypress into ignoring a test
720
742
  // The way we'll implement quarantine in cypress is by skipping the test altogether
721
- if (isDisabled || isQuarantined) {
743
+ if (!isAttemptToFix && (isDisabled || isQuarantined)) {
722
744
  return { shouldSkip: true }
723
745
  }
724
746
 
@@ -727,7 +749,9 @@ class CypressPlugin {
727
749
  testName,
728
750
  testSuite,
729
751
  isUnskippable,
730
- isForcedToRun
752
+ isForcedToRun,
753
+ isDisabled,
754
+ isQuarantined
731
755
  })
732
756
  }
733
757
 
@@ -747,7 +771,8 @@ class CypressPlugin {
747
771
  testSuiteAbsolutePath,
748
772
  testName,
749
773
  isNew,
750
- isEfdRetry
774
+ isEfdRetry,
775
+ isAttemptToFix
751
776
  } = test
752
777
  if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) {
753
778
  const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
@@ -770,6 +795,14 @@ class CypressPlugin {
770
795
  const testStatus = CYPRESS_STATUS_TO_TEST_STATUS[state]
771
796
  this.activeTestSpan.setTag(TEST_STATUS, testStatus)
772
797
 
798
+ // Save the test status to know if it has passed all retries
799
+ if (!this.testStatuses[testName]) {
800
+ this.testStatuses[testName] = [testStatus]
801
+ } else {
802
+ this.testStatuses[testName].push(testStatus)
803
+ }
804
+ const testStatuses = this.testStatuses[testName]
805
+
773
806
  if (error) {
774
807
  this.activeTestSpan.setTag('error', error)
775
808
  }
@@ -786,6 +819,22 @@ class CypressPlugin {
786
819
  this.activeTestSpan.setTag(TEST_RETRY_REASON, 'efd')
787
820
  }
788
821
  }
822
+ if (isAttemptToFix) {
823
+ this.activeTestSpan.setTag(TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX, 'true')
824
+ if (testStatuses.length > 1) {
825
+ this.activeTestSpan.setTag(TEST_IS_RETRY, 'true')
826
+ this.activeTestSpan.setTag(TEST_RETRY_REASON, 'attempt_to_fix')
827
+ }
828
+ const isLastAttempt = testStatuses.length === this.testManagementAttemptToFixRetries + 1
829
+ if (isLastAttempt) {
830
+ if (testStatuses.every(status => status === 'fail')) {
831
+ this.activeTestSpan.setTag(TEST_HAS_FAILED_ALL_RETRIES, 'true')
832
+ } else if (testStatuses.every(status => status === 'pass')) {
833
+ this.activeTestSpan.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'true')
834
+ }
835
+ }
836
+ }
837
+
789
838
  const finishedTest = {
790
839
  testName,
791
840
  testStatus,
@@ -4,6 +4,9 @@ let isKnownTestsEnabled = false
4
4
  let knownTestsForSuite = []
5
5
  let suiteTests = []
6
6
  let earlyFlakeDetectionNumRetries = 0
7
+ let isTestManagementEnabled = false
8
+ let testManagementAttemptToFixRetries = 0
9
+ let testManagementTests = {}
7
10
  // We need to grab the original window as soon as possible,
8
11
  // in case the test changes the origin. If the test does change the origin,
9
12
  // any call to `cy.window()` will result in a cross origin error.
@@ -23,31 +26,53 @@ function isNewTest (test) {
23
26
  return !knownTestsForSuite.includes(test.fullTitle())
24
27
  }
25
28
 
26
- function retryTest (test, suiteTests) {
27
- for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
29
+ function getTestProperties (testName) {
30
+ // We neeed to do it in this way because of compatibility with older versions as '?' is not supported in older versions of Cypress
31
+ const properties = testManagementTests[testName] && testManagementTests[testName].properties || {};
32
+
33
+ const { attempt_to_fix: isAttemptToFix, disabled: isDisabled, quarantined: isQuarantined } = properties;
34
+
35
+ return { isAttemptToFix, isDisabled, isQuarantined };
36
+ }
37
+
38
+ function retryTest (test, suiteTests, numRetries, tags) {
39
+ for (let retryIndex = 0; retryIndex < numRetries; retryIndex++) {
28
40
  const clonedTest = test.clone()
29
41
  // TODO: signal in framework logs that this is a retry.
30
42
  // TODO: Change it so these tests are allowed to fail.
31
43
  // TODO: figure out if reported duration is skewed.
32
44
  suiteTests.unshift(clonedTest)
33
- clonedTest._ddIsNew = true
34
- clonedTest._ddIsEfdRetry = true
45
+ tags.forEach(tag => {
46
+ clonedTest[tag] = true
47
+ })
35
48
  }
36
49
  }
37
50
 
38
51
 
39
52
  const oldRunTests = Cypress.mocha.getRunner().runTests
40
53
  Cypress.mocha.getRunner().runTests = function (suite, fn) {
41
- if (!isKnownTestsEnabled) {
54
+ if (!isKnownTestsEnabled && !isTestManagementEnabled) {
42
55
  return oldRunTests.apply(this, arguments)
43
56
  }
44
57
  // We copy the new tests at the beginning of the suite run (runTests), so that they're run
45
58
  // multiple times.
46
59
  suite.tests.forEach(test => {
47
- if (!test._ddIsNew && !test.isPending() && isNewTest(test)) {
48
- test._ddIsNew = true
49
- if (isEarlyFlakeDetectionEnabled) {
50
- retryTest(test, suite.tests)
60
+ const testName = test.fullTitle()
61
+
62
+ const { isAttemptToFix } = getTestProperties(testName)
63
+
64
+ if (isTestManagementEnabled) {
65
+ if (isAttemptToFix && !test.isPending()) {
66
+ test._ddIsAttemptToFix = true
67
+ retryTest(test, suite.tests, testManagementAttemptToFixRetries, ['_ddIsAttemptToFix'])
68
+ }
69
+ }
70
+ if (isKnownTestsEnabled) {
71
+ if (!test._ddIsNew && !test.isPending() && isNewTest(test)) {
72
+ test._ddIsNew = true
73
+ if (isEarlyFlakeDetectionEnabled && !isAttemptToFix) {
74
+ retryTest(test, suite.tests, earlyFlakeDetectionNumRetries, ['_ddIsNew', '_ddIsEfdRetry'])
75
+ }
51
76
  }
52
77
  }
53
78
  })
@@ -80,6 +105,9 @@ before(function () {
80
105
  isKnownTestsEnabled = suiteConfig.isKnownTestsEnabled
81
106
  knownTestsForSuite = suiteConfig.knownTestsForSuite
82
107
  earlyFlakeDetectionNumRetries = suiteConfig.earlyFlakeDetectionNumRetries
108
+ isTestManagementEnabled = suiteConfig.isTestManagementEnabled
109
+ testManagementAttemptToFixRetries = suiteConfig.testManagementAttemptToFixRetries
110
+ testManagementTests = suiteConfig.testManagementTests
83
111
  }
84
112
  })
85
113
  })
@@ -104,7 +132,8 @@ afterEach(function () {
104
132
  state: currentTest.state,
105
133
  error: currentTest.err,
106
134
  isNew: currentTest._ddIsNew,
107
- isEfdRetry: currentTest._ddIsEfdRetry
135
+ isEfdRetry: currentTest._ddIsEfdRetry,
136
+ isAttemptToFix: currentTest._ddIsAttemptToFix
108
137
  }
109
138
  try {
110
139
  testInfo.testSourceLine = Cypress.mocha.getRunner().currentRunnable.invocationDetails.line