dd-trace 5.43.0 → 5.45.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 (58) hide show
  1. package/package.json +4 -4
  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 +77 -17
  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/sdk/track_event.js +7 -0
  29. package/packages/dd-trace/src/appsec/telemetry/common.js +6 -3
  30. package/packages/dd-trace/src/appsec/telemetry/index.js +28 -5
  31. package/packages/dd-trace/src/appsec/telemetry/user.js +9 -1
  32. package/packages/dd-trace/src/appsec/telemetry/waf.js +29 -9
  33. package/packages/dd-trace/src/appsec/waf/waf_manager.js +16 -7
  34. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -2
  35. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +3 -1
  36. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +4 -2
  37. package/packages/dd-trace/src/dogstatsd.js +94 -77
  38. package/packages/dd-trace/src/histogram.js +12 -23
  39. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  40. package/packages/dd-trace/src/llmobs/index.js +3 -0
  41. package/packages/dd-trace/src/llmobs/noop.js +3 -3
  42. package/packages/dd-trace/src/llmobs/plugins/base.js +1 -1
  43. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +2 -1
  44. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +196 -0
  45. package/packages/dd-trace/src/llmobs/sdk.js +2 -0
  46. package/packages/dd-trace/src/llmobs/span_processor.js +6 -0
  47. package/packages/dd-trace/src/llmobs/tagger.js +8 -2
  48. package/packages/dd-trace/src/llmobs/telemetry.js +108 -1
  49. package/packages/dd-trace/src/llmobs/writers/base.js +4 -0
  50. package/packages/dd-trace/src/llmobs/writers/spans/base.js +10 -1
  51. package/packages/dd-trace/src/plugin_manager.js +0 -3
  52. package/packages/dd-trace/src/plugins/ci_plugin.js +16 -26
  53. package/packages/dd-trace/src/plugins/database.js +4 -4
  54. package/packages/dd-trace/src/plugins/plugin.js +2 -0
  55. package/packages/dd-trace/src/plugins/util/test.js +62 -1
  56. package/packages/dd-trace/src/remote_config/manager.js +5 -0
  57. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +7 -6
  58. 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
  }
@@ -176,6 +182,7 @@ function getSortWrapper (sort) {
176
182
  let isEarlyFlakeDetectionFaulty = false
177
183
  let isKnownTestsEnabled = false
178
184
  let isTestManagementTestsEnabled = false
185
+ let testManagementAttemptToFixRetries = 0
179
186
  let isDiEnabled = false
180
187
  let knownTests = {}
181
188
  let testManagementTests = {}
@@ -190,6 +197,7 @@ function getSortWrapper (sort) {
190
197
  isDiEnabled = libraryConfig.isDiEnabled
191
198
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
192
199
  isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
200
+ testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries
193
201
  }
194
202
  } catch (e) {
195
203
  isFlakyTestRetriesEnabled = false
@@ -262,6 +270,7 @@ function getSortWrapper (sort) {
262
270
  try {
263
271
  const workspaceProject = this.ctx.getCoreWorkspaceProject()
264
272
  workspaceProject._provided._ddIsTestManagementTestsEnabled = isTestManagementTestsEnabled
273
+ workspaceProject._provided._ddTestManagementAttemptToFixRetries = testManagementAttemptToFixRetries
265
274
  workspaceProject._provided._ddTestManagementTests = testManagementTests
266
275
  } catch (e) {
267
276
  log.warn('Could not send test management tests to workers so Test Management will not work.')
@@ -353,10 +362,24 @@ addHook({
353
362
  isKnownTestsEnabled,
354
363
  numRepeats,
355
364
  isTestManagementTestsEnabled,
365
+ testManagementAttemptToFixRetries,
356
366
  testManagementTests
357
367
  } = getProvidedContext()
358
368
 
359
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
+ })
360
383
  isDisabledCh.publish({
361
384
  testManagementTests,
362
385
  testSuiteAbsolutePath: task.file.filepath,
@@ -364,7 +387,10 @@ addHook({
364
387
  onDone: (isTestDisabled) => {
365
388
  if (isTestDisabled) {
366
389
  disabledTasks.add(task)
367
- 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
+ }
368
394
  }
369
395
  }
370
396
  })
@@ -376,7 +402,7 @@ addHook({
376
402
  testSuiteAbsolutePath: task.file.filepath,
377
403
  testName,
378
404
  onDone: (isNew) => {
379
- if (isNew) {
405
+ if (isNew && !attemptToFixTasks.has(task)) {
380
406
  if (isEarlyFlakeDetectionEnabled) {
381
407
  isRetryReasonEfd = task.repeats !== numRepeats
382
408
  task.repeats = numRepeats
@@ -396,19 +422,28 @@ addHook({
396
422
  shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => function (task) {
397
423
  const { isEarlyFlakeDetectionEnabled, isTestManagementTestsEnabled } = getProvidedContext()
398
424
 
399
- if (isEarlyFlakeDetectionEnabled && taskToStatuses.has(task)) {
400
- const statuses = taskToStatuses.get(task)
401
- // If the test has passed at least once, we consider it passed
402
- 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)) {
403
431
  if (task.result.state === 'fail') {
404
432
  switchedStatuses.add(task)
405
433
  }
406
434
  task.result.state = 'pass'
435
+ } else if (isQuarantined) {
436
+ task.result.state = 'pass'
407
437
  }
408
438
  }
409
439
 
410
- if (isTestManagementTestsEnabled) {
411
- 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
+ }
412
447
  task.result.state = 'pass'
413
448
  }
414
449
  }
@@ -481,6 +516,8 @@ addHook({
481
516
  }
482
517
 
483
518
  const lastExecutionStatus = task.result.state
519
+ const shouldFlipStatus = isEarlyFlakeDetectionEnabled || attemptToFixTasks.has(task)
520
+ const statuses = taskToStatuses.get(task)
484
521
 
485
522
  // These clauses handle task.repeats, whether EFD is enabled or not
486
523
  // The only thing that EFD does is to forcefully pass the test if it has passed at least once
@@ -501,15 +538,21 @@ addHook({
501
538
  testPassCh.publish({ task })
502
539
  })
503
540
  }
504
- if (isEarlyFlakeDetectionEnabled) {
505
- const statuses = taskToStatuses.get(task)
541
+ if (shouldFlipStatus) {
506
542
  statuses.push(lastExecutionStatus)
507
543
  // If we don't "reset" the result.state to "pass", once a repetition fails,
508
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
509
548
  task.result.state = 'pass'
510
549
  }
511
550
  }
512
551
  } else if (numRepetition === task.repeats) {
552
+ if (shouldFlipStatus) {
553
+ statuses.push(lastExecutionStatus)
554
+ }
555
+
513
556
  const asyncResource = taskToAsync.get(task)
514
557
  if (lastExecutionStatus === 'fail') {
515
558
  const testError = task.result?.errors?.[0]
@@ -532,8 +575,11 @@ addHook({
532
575
  testSuiteAbsolutePath: task.file.filepath,
533
576
  isRetry: numAttempt > 0 || numRepetition > 0,
534
577
  isRetryReasonEfd,
578
+ isRetryReasonAttemptToFix: isRetryReasonAttemptToFix && numRepetition > 0,
535
579
  isNew,
536
580
  mightHitProbe: isDiEnabled && numAttempt > 0,
581
+ isAttemptToFix: attemptToFixTasks.has(task),
582
+ isDisabled: disabledTasks.has(task),
537
583
  isQuarantined
538
584
  })
539
585
  })
@@ -548,6 +594,8 @@ addHook({
548
594
  }
549
595
  const result = await onAfterTryTask.apply(this, arguments)
550
596
 
597
+ const { testManagementAttemptToFixRetries } = getProvidedContext()
598
+
551
599
  const status = getVitestTestStatus(task, retryCount)
552
600
  const asyncResource = taskToAsync.get(task)
553
601
 
@@ -557,10 +605,18 @@ addHook({
557
605
  await waitForHitProbe()
558
606
  }
559
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
+
560
616
  if (asyncResource) {
561
617
  // We don't finish here because the test might fail in a later hook (afterEach)
562
618
  asyncResource.runInAsyncScope(() => {
563
- testFinishTimeCh.publish({ status, task })
619
+ testFinishTimeCh.publish({ status, task, attemptToFixPassed })
564
620
  })
565
621
  }
566
622
 
@@ -665,15 +721,11 @@ addHook({
665
721
  // From >=3.0.1, the first arguments changes from a string to an object containing the filepath
666
722
  const testSuiteAbsolutePath = testPaths[0]?.filepath || testPaths[0]
667
723
 
668
- const { isEarlyFlakeDetectionEnabled, isFlakyTestRetriesEnabled } = getProvidedContext()
669
-
670
724
  const testSuiteAsyncResource = new AsyncResource('bound-anonymous-fn')
671
725
  testSuiteAsyncResource.runInAsyncScope(() => {
672
726
  testSuiteStartCh.publish({
673
727
  testSuiteAbsolutePath,
674
- frameworkVersion,
675
- isFlakyTestRetriesEnabled,
676
- isEarlyFlakeDetectionEnabled
728
+ frameworkVersion
677
729
  })
678
730
  })
679
731
  const startTestsResponse = await startTests.apply(this, arguments)
@@ -715,11 +767,19 @@ addHook({
715
767
  testError = errors[0]
716
768
  }
717
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
+
718
778
  if (testAsyncResource) {
719
779
  const isRetry = task.result?.retryCount > 0
720
780
  // `duration` is the duration of all the retries, so it can't be used if there are retries
721
781
  testAsyncResource.runInAsyncScope(() => {
722
- testErrorCh.publish({ duration: !isRetry ? duration : undefined, error: testError })
782
+ testErrorCh.publish({ duration: !isRetry ? duration : undefined, error: testError, hasFailedAllRetries })
723
783
  })
724
784
  }
725
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