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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.43.0",
3
+ "version": "5.45.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -85,7 +85,7 @@
85
85
  },
86
86
  "dependencies": {
87
87
  "@datadog/libdatadog": "^0.5.0",
88
- "@datadog/native-appsec": "8.5.0",
88
+ "@datadog/native-appsec": "8.5.1",
89
89
  "@datadog/native-iast-rewriter": "2.8.0",
90
90
  "@datadog/native-iast-taint-tracking": "3.3.0",
91
91
  "@datadog/native-metrics": "^3.1.0",
@@ -95,7 +95,7 @@
95
95
  "@opentelemetry/api": ">=1.0.0 <1.9.0",
96
96
  "@opentelemetry/core": "^1.14.0",
97
97
  "crypto-randomuuid": "^1.0.0",
98
- "dc-polyfill": "^0.1.4",
98
+ "dc-polyfill": "0.1.6",
99
99
  "ignore": "^5.2.4",
100
100
  "import-in-the-middle": "1.13.1",
101
101
  "istanbul-lib-coverage": "3.2.0",
@@ -125,7 +125,7 @@
125
125
  "@msgpack/msgpack": "^3.0.0-beta3",
126
126
  "@stylistic/eslint-plugin-js": "^3.0.1",
127
127
  "@types/node": "^16.0.0",
128
- "application-config-path": "^1.0.0",
128
+ "application-config-path": "^0.1.1",
129
129
  "autocannon": "^4.5.2",
130
130
  "aws-sdk": "^2.1446.0",
131
131
  "axios": "^1.8.2",
@@ -73,6 +73,7 @@ let isEarlyFlakeDetectionFaulty = false
73
73
  let isFlakyTestRetriesEnabled = false
74
74
  let isKnownTestsEnabled = false
75
75
  let isTestManagementTestsEnabled = false
76
+ let testManagementAttemptToFixRetries = 0
76
77
  let testManagementTests = {}
77
78
  let numTestRetries = 0
78
79
  let knownTests = []
@@ -121,10 +122,10 @@ function isNewTest (testSuite, testName) {
121
122
  }
122
123
 
123
124
  function getTestProperties (testSuite, testName) {
124
- const { disabled, quarantined } =
125
+ const { attempt_to_fix: attemptToFix, disabled, quarantined } =
125
126
  testManagementTests?.cucumber?.suites?.[testSuite]?.tests?.[testName]?.properties || {}
126
127
 
127
- return { disabled, quarantined }
128
+ return { attemptToFix, disabled, quarantined }
128
129
  }
129
130
 
130
131
  function getTestStatusFromRetries (testStatuses) {
@@ -303,22 +304,42 @@ function wrapRun (pl, isLatestVersion) {
303
304
  }
304
305
  let isNew = false
305
306
  let isEfdRetry = false
307
+ let isAttemptToFix = false
308
+ let isAttemptToFixRetry = false
309
+ let hasFailedAllRetries = false
310
+ let hasPassedAllRetries = false
306
311
  let isDisabled = false
307
312
  let isQuarantined = false
308
- if (isKnownTestsEnabled && status !== 'skip') {
309
- const numRetries = numRetriesByPickleId.get(this.pickle.id)
310
313
 
311
- isNew = numRetries !== undefined
312
- isEfdRetry = numRetries > 0
313
- }
314
314
  if (isTestManagementTestsEnabled) {
315
315
  const testSuitePath = getTestSuitePath(testFileAbsolutePath, process.cwd())
316
316
  const testProperties = getTestProperties(testSuitePath, this.pickle.name)
317
+ const numRetries = numRetriesByPickleId.get(this.pickle.id)
318
+ isAttemptToFix = testProperties.attemptToFix
319
+ isAttemptToFixRetry = isAttemptToFix && numRetries > 0
317
320
  isDisabled = testProperties.disabled
318
- if (!isDisabled) {
319
- isQuarantined = testProperties.quarantined
321
+ isQuarantined = testProperties.quarantined
322
+
323
+ if (isAttemptToFixRetry) {
324
+ const statuses = lastStatusByPickleId.get(this.pickle.id)
325
+ if (statuses.length === testManagementAttemptToFixRetries + 1) {
326
+ const { pass, fail } = statuses.reduce((acc, status) => {
327
+ acc[status]++
328
+ return acc
329
+ }, { pass: 0, fail: 0 })
330
+ hasFailedAllRetries = fail === testManagementAttemptToFixRetries + 1
331
+ hasPassedAllRetries = pass === testManagementAttemptToFixRetries + 1
332
+ }
320
333
  }
321
334
  }
335
+
336
+ if (isKnownTestsEnabled && status !== 'skip' && !isAttemptToFix) {
337
+ const numRetries = numRetriesByPickleId.get(this.pickle.id)
338
+
339
+ isNew = numRetries !== undefined
340
+ isEfdRetry = numRetries > 0
341
+ }
342
+
322
343
  const attemptAsyncResource = numAttemptToAsyncResource.get(numAttempt)
323
344
 
324
345
  const error = getErrorFromCucumberResult(result)
@@ -334,6 +355,10 @@ function wrapRun (pl, isLatestVersion) {
334
355
  isNew,
335
356
  isEfdRetry,
336
357
  isFlakyRetry: numAttempt > 0,
358
+ isAttemptToFix,
359
+ isAttemptToFixRetry,
360
+ hasFailedAllRetries,
361
+ hasPassedAllRetries,
337
362
  isDisabled,
338
363
  isQuarantined
339
364
  })
@@ -426,6 +451,7 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin
426
451
  numTestRetries = configurationResponse.libraryConfig?.flakyTestRetriesCount
427
452
  isKnownTestsEnabled = configurationResponse.libraryConfig?.isKnownTestsEnabled
428
453
  isTestManagementTestsEnabled = configurationResponse.libraryConfig?.isTestManagementEnabled
454
+ testManagementAttemptToFixRetries = configurationResponse.libraryConfig?.testManagementAttemptToFixRetries
429
455
 
430
456
  if (isKnownTestsEnabled) {
431
457
  const knownTestsResponse = await getChannelPromise(knownTestsCh)
@@ -576,22 +602,25 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
576
602
  }
577
603
 
578
604
  let isNew = false
605
+ let isAttemptToFix = false
579
606
  let isDisabled = false
580
607
  let isQuarantined = false
581
608
 
582
- if (isKnownTestsEnabled) {
583
- isNew = isNewTest(testSuitePath, pickle.name)
584
- if (isNew) {
585
- numRetriesByPickleId.set(pickle.id, 0)
586
- }
587
- }
588
609
  if (isTestManagementTestsEnabled) {
589
610
  const testProperties = getTestProperties(testSuitePath, pickle.name)
611
+ isAttemptToFix = testProperties.attemptToFix
590
612
  isDisabled = testProperties.disabled
591
- if (isDisabled) {
613
+ isQuarantined = testProperties.quarantined
614
+ // If attempt to fix is enabled, we run even if the test is disabled
615
+ if (!isAttemptToFix && isDisabled) {
592
616
  this.options.dryRun = true
593
- } else {
594
- isQuarantined = testProperties.quarantined
617
+ }
618
+ }
619
+
620
+ if (isKnownTestsEnabled && !isAttemptToFix) {
621
+ isNew = isNewTest(testSuitePath, pickle.name)
622
+ if (isNew) {
623
+ numRetriesByPickleId.set(pickle.id, 0)
595
624
  }
596
625
  }
597
626
  // TODO: for >=11 we could use `runTestCaseResult` instead of accumulating results in `lastStatusByPickleId`
@@ -599,6 +628,15 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
599
628
 
600
629
  const testStatuses = lastStatusByPickleId.get(pickle.id)
601
630
  const lastTestStatus = testStatuses[testStatuses.length - 1]
631
+
632
+ // New tests should not be marked as attempt to fix, so EFD + Attempt to fix should not be enabled at the same time
633
+ if (isAttemptToFix && lastTestStatus !== 'skip') {
634
+ for (let retryIndex = 0; retryIndex < testManagementAttemptToFixRetries; retryIndex++) {
635
+ numRetriesByPickleId.set(pickle.id, retryIndex + 1)
636
+ runTestCaseResult = await runTestCaseFunction.apply(this, arguments)
637
+ }
638
+ }
639
+
602
640
  // If it's a new test and it hasn't been skipped, we run it again
603
641
  if (isEarlyFlakeDetectionEnabled && lastTestStatus !== 'skip' && isNew) {
604
642
  for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
@@ -608,7 +646,7 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
608
646
  }
609
647
  let testStatus = lastTestStatus
610
648
  let shouldBePassedByEFD = false
611
- let shouldBePassedByQuarantine = false
649
+ let shouldBePassedByTestManagement = false
612
650
  if (isNew && isEarlyFlakeDetectionEnabled) {
613
651
  /**
614
652
  * If Early Flake Detection (EFD) is enabled the logic is as follows:
@@ -625,9 +663,9 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
625
663
  }
626
664
  }
627
665
 
628
- if (isTestManagementTestsEnabled && isQuarantined) {
666
+ if (isTestManagementTestsEnabled && (isDisabled || isQuarantined)) {
629
667
  this.success = true
630
- shouldBePassedByQuarantine = true
668
+ shouldBePassedByTestManagement = true
631
669
  }
632
670
 
633
671
  if (!pickleResultByFile[testFileAbsolutePath]) {
@@ -661,8 +699,8 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
661
699
  return shouldBePassedByEFD
662
700
  }
663
701
 
664
- if (isNewerCucumberVersion && isTestManagementTestsEnabled && isQuarantined) {
665
- return shouldBePassedByQuarantine
702
+ if (isNewerCucumberVersion && isTestManagementTestsEnabled && (isQuarantined || isDisabled)) {
703
+ return shouldBePassedByTestManagement
666
704
  }
667
705
 
668
706
  return runTestCaseResult
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { addHook } = require('./helpers/instrument')
4
+
5
+ // Empty hook just to make the plugin load.
6
+ // TODO: Add version range when the module is released on npm.
7
+ addHook({ name: 'dd-trace-api' }, api => api)
@@ -43,6 +43,7 @@ module.exports = {
43
43
  couchbase: () => require('../couchbase'),
44
44
  crypto: () => require('../crypto'),
45
45
  cypress: () => require('../cypress'),
46
+ 'dd-trace-api': () => require('../dd-trace-api'),
46
47
  dns: () => require('../dns'),
47
48
  elasticsearch: () => require('../elasticsearch'),
48
49
  express: () => require('../express'),
@@ -158,7 +158,7 @@ for (const packageName of names) {
158
158
  }
159
159
 
160
160
  function matchVersion (version, ranges) {
161
- return !version || (ranges && ranges.some(range => satisfies(version, range)))
161
+ return !version || !ranges || ranges.some(range => satisfies(version, range))
162
162
  }
163
163
 
164
164
  function getVersion (moduleBaseDir) {
@@ -13,7 +13,9 @@ const {
13
13
  addEfdStringToTestName,
14
14
  removeEfdStringFromTestName,
15
15
  getIsFaultyEarlyFlakeDetection,
16
- JEST_WORKER_LOGS_PAYLOAD_CODE
16
+ JEST_WORKER_LOGS_PAYLOAD_CODE,
17
+ addAttemptToFixStringToTestName,
18
+ removeAttemptToFixStringFromTestName
17
19
  } = require('../../dd-trace/src/plugins/util/test')
18
20
  const {
19
21
  getFormattedJestTestParameters,
@@ -73,6 +75,7 @@ let hasFilteredSkippableSuites = false
73
75
  let isKnownTestsEnabled = false
74
76
  let isTestManagementTestsEnabled = false
75
77
  let testManagementTests = {}
78
+ let testManagementAttemptToFixRetries = 0
76
79
 
77
80
  const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
78
81
 
@@ -80,6 +83,7 @@ const asyncResources = new WeakMap()
80
83
  const originalTestFns = new WeakMap()
81
84
  const retriedTestsToNumAttempts = new Map()
82
85
  const newTestsTestStatuses = new Map()
86
+ const attemptToFixRetriedTestsStatuses = new Map()
83
87
 
84
88
  const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200
85
89
 
@@ -110,7 +114,7 @@ function getTestEnvironmentOptions (config) {
110
114
  return {}
111
115
  }
112
116
 
113
- function getEfdStats (testStatuses) {
117
+ function getTestStats (testStatuses) {
114
118
  return testStatuses.reduce((acc, testStatus) => {
115
119
  acc[testStatus]++
116
120
  return acc
@@ -169,6 +173,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
169
173
  if (this.isTestManagementTestsEnabled) {
170
174
  try {
171
175
  const hasTestManagementTests = !!testManagementTests.jest
176
+ testManagementAttemptToFixRetries = this.testEnvironmentOptions._ddTestManagementAttemptToFixRetries
172
177
  this.testManagementTestsForThisSuite = hasTestManagementTests
173
178
  ? this.getTestManagementTestsForSuite(testManagementTests.jest.suites?.[this.testSuite]?.tests)
174
179
  : this.getTestManagementTestsForSuite(this.testEnvironmentOptions._ddTestManagementTests)
@@ -213,9 +218,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
213
218
  if (this.testManagementTestsForThisSuite) {
214
219
  return this.testManagementTestsForThisSuite
215
220
  }
216
- // TODO - ADD ATTEMPT_TO_FIX tests
217
221
  if (!testManagementTests) {
218
222
  return {
223
+ attemptToFix: [],
219
224
  disabled: [],
220
225
  quarantined: []
221
226
  }
@@ -228,14 +233,19 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
228
233
  }
229
234
 
230
235
  const result = {
236
+ attemptToFix: [],
231
237
  disabled: [],
232
238
  quarantined: []
233
239
  }
234
240
 
235
241
  Object.entries(testManagementTestsForSuite).forEach(([testName, { properties }]) => {
242
+ if (properties?.attempt_to_fix) {
243
+ result.attemptToFix.push(testName)
244
+ }
236
245
  if (properties?.disabled) {
237
246
  result.disabled.push(testName)
238
- } else if (properties?.quarantined) {
247
+ }
248
+ if (properties?.quarantined) {
239
249
  result.quarantined.push(testName)
240
250
  }
241
251
  })
@@ -243,11 +253,30 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
243
253
  return result
244
254
  }
245
255
 
246
- // Add the `add_test` event we don't have the test object yet, so
256
+ // Generic function to handle test retries
257
+ retryTest (testName, retryCount, addRetryStringToTestName, retryType, event) {
258
+ // Retrying snapshots has proven to be problematic, so we'll skip them for now
259
+ // We'll still detect new tests, but we won't retry them.
260
+ // TODO: do not bail out of retrying tests for the whole test suite
261
+ if (this.getHasSnapshotTests()) {
262
+ log.warn(`${retryType} is disabled for suites with snapshots`)
263
+ return
264
+ }
265
+
266
+ for (let retryIndex = 0; retryIndex < retryCount; retryIndex++) {
267
+ if (this.global.test) {
268
+ this.global.test(addRetryStringToTestName(testName, retryIndex), event.fn, event.timeout)
269
+ } else {
270
+ log.error(`${retryType} could not retry test because global.test is undefined`)
271
+ }
272
+ }
273
+ }
274
+
275
+ // At the `add_test` event we don't have the test object yet, so we can't use it
247
276
  getTestNameFromAddTestEvent (event, state) {
248
277
  const describeSuffix = getJestTestName(state.currentDescribeBlock)
249
278
  const fullTestName = describeSuffix ? `${describeSuffix} ${event.testName}` : event.testName
250
- return removeEfdStringFromTestName(fullTestName)
279
+ return removeAttemptToFixStringFromTestName(removeEfdStringFromTestName(fullTestName))
251
280
  }
252
281
 
253
282
  async handleTestEvent (event, state) {
@@ -273,15 +302,31 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
273
302
  if (event.name === 'test_start') {
274
303
  let isNewTest = false
275
304
  let numEfdRetry = null
305
+ let numOfAttemptsToFixRetries = null
276
306
  const testParameters = getTestParametersString(this.nameToParams, event.test.name)
277
307
  // Async resource for this test is created here
278
308
  // It is used later on by the test_done handler
279
309
  const asyncResource = new AsyncResource('bound-anonymous-fn')
280
310
  asyncResources.set(event.test, asyncResource)
281
311
  const testName = getJestTestName(event.test)
312
+ const originalTestName = removeEfdStringFromTestName(removeAttemptToFixStringFromTestName(testName))
313
+
314
+ let isAttemptToFix = false
315
+ let isDisabled = false
316
+ let isQuarantined = false
317
+ if (this.isTestManagementTestsEnabled) {
318
+ isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(originalTestName)
319
+ isDisabled = this.testManagementTestsForThisSuite?.disabled?.includes(originalTestName)
320
+ isQuarantined = this.testManagementTestsForThisSuite?.quarantined?.includes(originalTestName)
321
+ if (isAttemptToFix) {
322
+ numOfAttemptsToFixRetries = retriedTestsToNumAttempts.get(originalTestName)
323
+ retriedTestsToNumAttempts.set(originalTestName, numOfAttemptsToFixRetries + 1)
324
+ } else if (isDisabled) {
325
+ event.test.mode = 'skip'
326
+ }
327
+ }
282
328
 
283
329
  if (this.isKnownTestsEnabled) {
284
- const originalTestName = removeEfdStringFromTestName(testName)
285
330
  isNewTest = retriedTestsToNumAttempts.has(originalTestName)
286
331
  if (isNewTest) {
287
332
  numEfdRetry = retriedTestsToNumAttempts.get(originalTestName)
@@ -289,16 +334,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
289
334
  }
290
335
  }
291
336
 
292
- if (this.isTestManagementTestsEnabled) {
293
- const isDisabled = this.testManagementTestsForThisSuite?.disabled?.includes(testName)
294
- if (isDisabled) {
295
- event.test.mode = 'skip'
296
- }
297
- }
298
337
  const isJestRetry = event.test?.invocations > 1
299
338
  asyncResource.runInAsyncScope(() => {
300
339
  testStartCh.publish({
301
- name: removeEfdStringFromTestName(testName),
340
+ name: originalTestName,
302
341
  suite: this.testSuite,
303
342
  testSourceFile: this.testSourceFile,
304
343
  displayName: this.displayName,
@@ -306,34 +345,46 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
306
345
  frameworkVersion: jestVersion,
307
346
  isNew: isNewTest,
308
347
  isEfdRetry: numEfdRetry > 0,
309
- isJestRetry
348
+ isAttemptToFix,
349
+ isAttemptToFixRetry: numOfAttemptsToFixRetries > 0,
350
+ isJestRetry,
351
+ isDisabled,
352
+ isQuarantined
310
353
  })
311
354
  originalTestFns.set(event.test, event.test.fn)
312
355
  event.test.fn = asyncResource.bind(event.test.fn)
313
356
  })
314
357
  }
358
+
315
359
  if (event.name === 'add_test') {
360
+ const originalTestName = this.getTestNameFromAddTestEvent(event, state)
361
+
362
+ const isSkipped = event.mode === 'todo' || event.mode === 'skip'
363
+ if (this.isTestManagementTestsEnabled) {
364
+ const isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(originalTestName)
365
+ if (isAttemptToFix && !isSkipped && !retriedTestsToNumAttempts.has(originalTestName)) {
366
+ retriedTestsToNumAttempts.set(originalTestName, 0)
367
+ this.retryTest(
368
+ event.testName,
369
+ testManagementAttemptToFixRetries,
370
+ addAttemptToFixStringToTestName,
371
+ 'Test Management (Attempt to Fix)',
372
+ event
373
+ )
374
+ }
375
+ }
316
376
  if (this.isKnownTestsEnabled) {
317
- const testName = this.getTestNameFromAddTestEvent(event, state)
318
- const isNew = !this.knownTestsForThisSuite?.includes(testName)
319
- const isSkipped = event.mode === 'todo' || event.mode === 'skip'
320
- if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testName)) {
321
- retriedTestsToNumAttempts.set(testName, 0)
377
+ const isNew = !this.knownTestsForThisSuite?.includes(originalTestName)
378
+ if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(originalTestName)) {
379
+ retriedTestsToNumAttempts.set(originalTestName, 0)
322
380
  if (this.isEarlyFlakeDetectionEnabled) {
323
- // Retrying snapshots has proven to be problematic, so we'll skip them for now
324
- // We'll still detect new tests, but we won't retry them.
325
- // TODO: do not bail out of EFD with the whole test suite
326
- if (this.getHasSnapshotTests()) {
327
- log.warn('Early flake detection is disabled for suites with snapshots')
328
- return
329
- }
330
- for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
331
- if (this.global.test) {
332
- this.global.test(addEfdStringToTestName(event.testName, retryIndex), event.fn, event.timeout)
333
- } else {
334
- log.error('Early flake detection could not retry test because global.test is undefined')
335
- }
336
- }
381
+ this.retryTest(
382
+ event.testName,
383
+ earlyFlakeDetectionNumRetries,
384
+ addEfdStringToTestName,
385
+ 'Early flake detection',
386
+ event
387
+ )
337
388
  }
338
389
  }
339
390
  }
@@ -346,6 +397,32 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
346
397
  // restore in case it is retried
347
398
  event.test.fn = originalTestFns.get(event.test)
348
399
 
400
+ let attemptToFixPassed = false
401
+ let failedAllTests = false
402
+ if (this.isTestManagementTestsEnabled) {
403
+ const testName = getJestTestName(event.test)
404
+ const originalTestName = removeAttemptToFixStringFromTestName(testName)
405
+ const isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(originalTestName)
406
+ if (isAttemptToFix) {
407
+ if (attemptToFixRetriedTestsStatuses.has(originalTestName)) {
408
+ attemptToFixRetriedTestsStatuses.get(originalTestName).push(status)
409
+ } else {
410
+ attemptToFixRetriedTestsStatuses.set(originalTestName, [status])
411
+ }
412
+ const testStatuses = attemptToFixRetriedTestsStatuses.get(originalTestName)
413
+ // Check if this is the last attempt to fix.
414
+ // If it is, we'll set the failedAllTests flag to true if all the tests failed
415
+ // If all tests passed, we'll set the attemptToFixPassed flag to true
416
+ if (testStatuses.length === testManagementAttemptToFixRetries + 1) {
417
+ if (testStatuses.every(status => status === 'fail')) {
418
+ failedAllTests = true
419
+ } else if (testStatuses.every(status => status === 'pass')) {
420
+ attemptToFixPassed = true
421
+ }
422
+ }
423
+ }
424
+ }
425
+
349
426
  // We'll store the test statuses of the retries
350
427
  if (this.isKnownTestsEnabled) {
351
428
  const testName = getJestTestName(event.test)
@@ -359,12 +436,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
359
436
  }
360
437
  }
361
438
  }
362
- let isQuarantined = false
363
-
364
- if (this.isTestManagementTestsEnabled) {
365
- const testName = getJestTestName(event.test)
366
- isQuarantined = this.testManagementTestsForThisSuite?.quarantined?.includes(testName)
367
- }
368
439
 
369
440
  const promises = {}
370
441
  const numRetries = this.global[RETRY_TIMES]
@@ -399,7 +470,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
399
470
  testFinishCh.publish({
400
471
  status,
401
472
  testStartLine: getTestLineStart(event.test.asyncError, this.testSuite),
402
- isQuarantined
473
+ attemptToFixPassed,
474
+ failedAllTests
403
475
  })
404
476
  })
405
477
 
@@ -552,6 +624,7 @@ function cliWrapper (cli, jestVersion) {
552
624
  earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
553
625
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
554
626
  isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
627
+ testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries
555
628
  }
556
629
  } catch (err) {
557
630
  log.error('Jest library configuration error', err)
@@ -710,7 +783,7 @@ function cliWrapper (cli, jestVersion) {
710
783
  if (isEarlyFlakeDetectionEnabled) {
711
784
  let numFailedTestsToIgnore = 0
712
785
  for (const testStatuses of newTestsTestStatuses.values()) {
713
- const { pass, fail } = getEfdStats(testStatuses)
786
+ const { pass, fail } = getTestStats(testStatuses)
714
787
  if (pass > 0) { // as long as one passes, we'll consider the test passed
715
788
  numFailedTestsToIgnore += fail
716
789
  }
@@ -725,29 +798,41 @@ function cliWrapper (cli, jestVersion) {
725
798
  const failedTests = result
726
799
  .results
727
800
  .testResults.flatMap(({ testResults, testFilePath: testSuiteAbsolutePath }) => (
728
- testResults.map(({ fullName: testName, status }) => ({ testName, testSuiteAbsolutePath, status }))
801
+ testResults.map(({ fullName: testName, status }) => (
802
+ { testName, testSuiteAbsolutePath, status }
803
+ ))
729
804
  ))
730
805
  .filter(({ status }) => status === 'failed')
731
806
 
732
807
  let numFailedQuarantinedTests = 0
808
+ let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
733
809
 
734
810
  for (const { testName, testSuiteAbsolutePath } of failedTests) {
735
811
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, result.globalConfig.rootDir)
736
- const isQuarantined = testManagementTests
812
+ const originalName = removeAttemptToFixStringFromTestName(testName)
813
+ const testManagementTest = testManagementTests
737
814
  ?.jest
738
815
  ?.suites
739
816
  ?.[testSuite]
740
817
  ?.tests
741
- ?.[testName]
818
+ ?.[originalName]
742
819
  ?.properties
743
- ?.quarantined
744
- if (isQuarantined) {
820
+ // This uses `attempt_to_fix` because this is always the main process and it's not formatted in camelCase
821
+ if (testManagementTest?.attempt_to_fix && (testManagementTest?.quarantined || testManagementTest?.disabled)) {
822
+ numFailedQuarantinedOrDisabledAttemptedToFixTests++
823
+ } else if (testManagementTest?.quarantined) {
745
824
  numFailedQuarantinedTests++
746
825
  }
747
826
  }
748
827
 
749
828
  // If every test that failed was quarantined, we'll consider the suite passed
750
- if (numFailedQuarantinedTests !== 0 && result.results.numFailedTests === numFailedQuarantinedTests) {
829
+ // Note that if a test is attempted to fix,
830
+ // it's considered quarantined both if it's disabled and if it's quarantined (it'll run but its status is ignored)
831
+ if (
832
+ (numFailedQuarantinedOrDisabledAttemptedToFixTests !== 0 || numFailedQuarantinedTests !== 0) &&
833
+ result.results.numFailedTests ===
834
+ numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
835
+ ) {
751
836
  result.results.success = true
752
837
  }
753
838
  }
@@ -947,6 +1032,7 @@ addHook({
947
1032
  _ddIsKnownTestsEnabled,
948
1033
  _ddIsTestManagementTestsEnabled,
949
1034
  _ddTestManagementTests,
1035
+ _ddTestManagementAttemptToFixRetries,
950
1036
  ...restOfTestEnvironmentOptions
951
1037
  } = testEnvironmentOptions
952
1038
 
@@ -30,7 +30,9 @@ const {
30
30
  newTests,
31
31
  testsQuarantined,
32
32
  getTestFullName,
33
- getRunTestsWrapper
33
+ getRunTestsWrapper,
34
+ testsAttemptToFix,
35
+ testsStatuses
34
36
  } = require('./utils')
35
37
 
36
38
  require('./common')
@@ -138,16 +140,26 @@ function getOnEndHandler (isParallel) {
138
140
  }
139
141
  }
140
142
 
143
+ // We substract the errors of attempt to fix tests (quarantined or disabled) from the total number of failures
141
144
  // We subtract the errors from quarantined tests from the total number of failures
142
145
  if (config.isTestManagementTestsEnabled) {
143
146
  let numFailedQuarantinedTests = 0
147
+ let numFailedRetriedQuarantinedOrDisabledTests = 0
148
+ for (const test of testsAttemptToFix) {
149
+ const testName = getTestFullName(test)
150
+ const testProperties = getTestProperties(test, config.testManagementTests)
151
+ if (isTestFailed(test) && (testProperties.isQuarantined || testProperties.isDisabled)) {
152
+ const numFailedTests = testsStatuses.get(testName).filter(status => status === 'fail').length
153
+ numFailedRetriedQuarantinedOrDisabledTests += numFailedTests
154
+ }
155
+ }
144
156
  for (const test of testsQuarantined) {
145
157
  if (isTestFailed(test)) {
146
158
  numFailedQuarantinedTests++
147
159
  }
148
160
  }
149
- this.stats.failures -= numFailedQuarantinedTests
150
- this.failures -= numFailedQuarantinedTests
161
+ this.stats.failures -= numFailedQuarantinedTests + numFailedRetriedQuarantinedOrDisabledTests
162
+ this.failures -= numFailedQuarantinedTests + numFailedRetriedQuarantinedOrDisabledTests
151
163
  }
152
164
 
153
165
  if (status === 'fail') {
@@ -193,6 +205,7 @@ function getExecutionConfiguration (runner, isParallel, onFinishRequest) {
193
205
  if (err) {
194
206
  config.testManagementTests = {}
195
207
  config.isTestManagementTestsEnabled = false
208
+ config.testManagementAttemptToFixRetries = 0
196
209
  } else {
197
210
  config.testManagementTests = receivedTestManagementTests
198
211
  }
@@ -260,6 +273,7 @@ function getExecutionConfiguration (runner, isParallel, onFinishRequest) {
260
273
  config.earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
261
274
  config.isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
262
275
  config.isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
276
+ config.testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries
263
277
  // ITR and auto test retries are not supported in parallel mode yet
264
278
  config.isSuitesSkippingEnabled = !isParallel && libraryConfig.isSuitesSkippingEnabled
265
279
  config.isFlakyTestRetriesEnabled = !isParallel && libraryConfig.isFlakyTestRetriesEnabled
@@ -401,7 +415,7 @@ addHook({
401
415
 
402
416
  this.on('test', getOnTestHandler(true))
403
417
 
404
- this.on('test end', getOnTestEndHandler())
418
+ this.on('test end', getOnTestEndHandler(config))
405
419
 
406
420
  this.on('retry', getOnTestRetryHandler())
407
421
 
@@ -637,6 +651,8 @@ addHook({
637
651
  if (config.isTestManagementTestsEnabled) {
638
652
  const testSuiteTestManagementTests = config.testManagementTests?.mocha?.suites?.[testPath] || {}
639
653
  newWorkerArgs._ddIsTestManagementTestsEnabled = true
654
+ // TODO: attempt to fix does not work in parallel mode yet
655
+ // newWorkerArgs._ddTestManagementAttemptToFixRetries = config.testManagementAttemptToFixRetries
640
656
  newWorkerArgs._ddTestManagementTests = {
641
657
  mocha: {
642
658
  suites: {