dd-trace 3.47.0 → 3.49.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 (86) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +1 -32
  3. package/ci/init.js +1 -4
  4. package/index.d.ts +21 -0
  5. package/package.json +7 -6
  6. package/packages/datadog-instrumentations/src/amqplib.js +1 -1
  7. package/packages/datadog-instrumentations/src/child_process.js +150 -0
  8. package/packages/datadog-instrumentations/src/cucumber.js +12 -12
  9. package/packages/datadog-instrumentations/src/express.js +20 -0
  10. package/packages/datadog-instrumentations/src/grpc/client.js +56 -36
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -2
  12. package/packages/datadog-instrumentations/src/jest.js +149 -11
  13. package/packages/datadog-instrumentations/src/mocha.js +142 -16
  14. package/packages/datadog-instrumentations/src/mongoose.js +23 -10
  15. package/packages/datadog-instrumentations/src/next.js +17 -3
  16. package/packages/datadog-instrumentations/src/playwright.js +41 -9
  17. package/packages/datadog-plugin-amqplib/src/consumer.js +10 -1
  18. package/packages/datadog-plugin-amqplib/src/producer.js +14 -1
  19. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +107 -1
  20. package/packages/datadog-plugin-child_process/src/index.js +91 -0
  21. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +125 -0
  22. package/packages/datadog-plugin-cucumber/src/index.js +16 -11
  23. package/packages/datadog-plugin-cypress/src/plugin.js +52 -23
  24. package/packages/datadog-plugin-grpc/src/client.js +16 -2
  25. package/packages/datadog-plugin-http/src/client.js +1 -1
  26. package/packages/datadog-plugin-jest/src/index.js +43 -6
  27. package/packages/datadog-plugin-kafkajs/src/consumer.js +16 -0
  28. package/packages/datadog-plugin-mocha/src/index.js +47 -17
  29. package/packages/datadog-plugin-playwright/src/index.js +19 -5
  30. package/packages/datadog-plugin-rhea/src/consumer.js +11 -1
  31. package/packages/datadog-plugin-rhea/src/producer.js +11 -0
  32. package/packages/dd-trace/src/appsec/addresses.js +2 -0
  33. package/packages/dd-trace/src/appsec/api_security_sampler.js +16 -3
  34. package/packages/dd-trace/src/appsec/channels.js +2 -1
  35. package/packages/dd-trace/src/appsec/iast/analyzers/command-injection-analyzer.js +1 -1
  36. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +7 -28
  37. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +10 -6
  38. package/packages/dd-trace/src/appsec/iast/context/context-plugin.js +90 -0
  39. package/packages/dd-trace/src/appsec/iast/context/kafka-ctx-plugin.js +14 -0
  40. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +12 -1
  41. package/packages/dd-trace/src/appsec/iast/index.js +4 -4
  42. package/packages/dd-trace/src/appsec/iast/overhead-controller.js +1 -1
  43. package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +1 -0
  44. package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +10 -0
  45. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +53 -0
  46. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +10 -46
  47. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +13 -9
  48. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +47 -0
  49. package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +3 -1
  50. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +29 -2
  51. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  52. package/packages/dd-trace/src/appsec/index.js +17 -2
  53. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
  54. package/packages/dd-trace/src/appsec/remote_config/index.js +1 -0
  55. package/packages/dd-trace/src/appsec/rule_manager.js +2 -2
  56. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +83 -0
  57. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +25 -6
  58. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +2 -0
  59. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +83 -41
  60. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +30 -8
  61. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -1
  62. package/packages/dd-trace/src/ci-visibility/{intelligent-test-runner/get-itr-configuration.js → requests/get-library-configuration.js} +18 -6
  63. package/packages/dd-trace/src/config.js +22 -9
  64. package/packages/dd-trace/src/datastreams/processor.js +6 -0
  65. package/packages/dd-trace/src/datastreams/writer.js +2 -5
  66. package/packages/dd-trace/src/dogstatsd.js +3 -5
  67. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +5 -3
  68. package/packages/dd-trace/src/exporters/common/request.js +21 -3
  69. package/packages/dd-trace/src/format.js +25 -1
  70. package/packages/dd-trace/src/noop/span.js +1 -0
  71. package/packages/dd-trace/src/opentelemetry/span.js +9 -2
  72. package/packages/dd-trace/src/opentracing/span.js +38 -0
  73. package/packages/dd-trace/src/opentracing/span_context.js +12 -6
  74. package/packages/dd-trace/src/opentracing/tracer.js +2 -1
  75. package/packages/dd-trace/src/plugins/ci_plugin.js +25 -9
  76. package/packages/dd-trace/src/plugins/index.js +1 -0
  77. package/packages/dd-trace/src/plugins/util/git.js +6 -0
  78. package/packages/dd-trace/src/plugins/util/test.js +53 -8
  79. package/packages/dd-trace/src/profiling/config.js +22 -22
  80. package/packages/dd-trace/src/proxy.js +31 -23
  81. package/packages/dd-trace/src/span_processor.js +5 -1
  82. package/packages/dd-trace/src/telemetry/index.js +6 -0
  83. package/packages/dd-trace/src/telemetry/logs/index.js +2 -2
  84. package/packages/dd-trace/src/telemetry/send-data.js +0 -3
  85. package/packages/datadog-instrumentations/src/child-process.js +0 -29
  86. package/packages/dd-trace/src/plugins/util/exec.js +0 -34
@@ -9,7 +9,9 @@ const {
9
9
  JEST_WORKER_COVERAGE_PAYLOAD_CODE,
10
10
  getTestLineStart,
11
11
  getTestSuitePath,
12
- getTestParametersString
12
+ getTestParametersString,
13
+ EFD_STRING,
14
+ removeEfdStringFromTestName
13
15
  } = require('../../dd-trace/src/plugins/util/test')
14
16
  const {
15
17
  getFormattedJestTestParameters,
@@ -37,11 +39,16 @@ const testRunFinishCh = channel('ci:jest:test:finish')
37
39
  const testErrCh = channel('ci:jest:test:err')
38
40
 
39
41
  const skippableSuitesCh = channel('ci:jest:test-suite:skippable')
40
- const jestItrConfigurationCh = channel('ci:jest:itr-configuration')
42
+ const libraryConfigurationCh = channel('ci:jest:library-configuration')
43
+ const knownTestsCh = channel('ci:jest:known-tests')
41
44
 
42
45
  const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
43
46
 
47
+ // Maximum time we'll wait for the tracer to flush
48
+ const FLUSH_TIMEOUT = 10000
49
+
44
50
  let skippableSuites = []
51
+ let knownTests = []
45
52
  let isCodeCoverageEnabled = false
46
53
  let isSuitesSkippingEnabled = false
47
54
  let isUserCodeCoverageEnabled = false
@@ -49,6 +56,8 @@ let isSuitesSkipped = false
49
56
  let numSkippedSuites = 0
50
57
  let hasUnskippableSuites = false
51
58
  let hasForcedToRunSuites = false
59
+ let isEarlyFlakeDetectionEnabled = false
60
+ let earlyFlakeDetectionNumRetries = 0
52
61
 
53
62
  const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
54
63
 
@@ -62,6 +71,7 @@ const specStatusToTestStatus = {
62
71
 
63
72
  const asyncResources = new WeakMap()
64
73
  const originalTestFns = new WeakMap()
74
+ const retriedTestsToNumAttempts = new Map()
65
75
 
66
76
  // based on https://github.com/facebook/jest/blob/main/packages/jest-circus/src/formatNodeAssertErrors.ts#L41
67
77
  function formatJestError (errors) {
@@ -90,6 +100,10 @@ function getTestEnvironmentOptions (config) {
90
100
  return {}
91
101
  }
92
102
 
103
+ function getEfdTestName (testName, numAttempt) {
104
+ return `${EFD_STRING} (#${numAttempt}): ${testName}`
105
+ }
106
+
93
107
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
94
108
  return class DatadogEnvironment extends BaseEnvironment {
95
109
  constructor (config, context) {
@@ -101,6 +115,44 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
101
115
  this.global._ddtrace = global._ddtrace
102
116
 
103
117
  this.testEnvironmentOptions = getTestEnvironmentOptions(config)
118
+
119
+ const repositoryRoot = this.testEnvironmentOptions._ddRepositoryRoot
120
+
121
+ if (repositoryRoot) {
122
+ this.testSourceFile = getTestSuitePath(context.testPath, repositoryRoot)
123
+ }
124
+
125
+ this.isEarlyFlakeDetectionEnabled = this.testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled
126
+
127
+ if (this.isEarlyFlakeDetectionEnabled) {
128
+ earlyFlakeDetectionNumRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionNumRetries
129
+ try {
130
+ this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
131
+ } catch (e) {
132
+ // If there has been an error parsing the tests, we'll disable Early Flake Deteciton
133
+ this.isEarlyFlakeDetectionEnabled = false
134
+ }
135
+ }
136
+ }
137
+
138
+ // Function that receives a list of known tests for a test service and
139
+ // returns the ones that belong to the current suite
140
+ getKnownTestsForSuite (knownTests) {
141
+ let knownTestsForSuite = knownTests
142
+ // If jest runs in band, the known tests are not serialized, so they're an array.
143
+ if (!Array.isArray(knownTests)) {
144
+ knownTestsForSuite = JSON.parse(knownTestsForSuite)
145
+ }
146
+ return knownTestsForSuite
147
+ .filter(test => test.includes(this.testSuite))
148
+ .map(test => test.replace(`jest.${this.testSuite}.`, '').trim())
149
+ }
150
+
151
+ // Add the `add_test` event we don't have the test object yet, so
152
+ // we use its describe block to get the full name
153
+ getTestNameFromAddTestEvent (event, state) {
154
+ const describeSuffix = getJestTestName(state.currentDescribeBlock)
155
+ return removeEfdStringFromTestName(`${describeSuffix} ${event.testName}`).trim()
104
156
  }
105
157
 
106
158
  async handleTestEvent (event, state) {
@@ -124,23 +176,56 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
124
176
  }
125
177
  }
126
178
  if (event.name === 'test_start') {
179
+ let isNewTest = false
180
+ let numEfdRetry = null
127
181
  const testParameters = getTestParametersString(this.nameToParams, event.test.name)
128
182
  // Async resource for this test is created here
129
183
  // It is used later on by the test_done handler
130
184
  const asyncResource = new AsyncResource('bound-anonymous-fn')
131
185
  asyncResources.set(event.test, asyncResource)
186
+ const testName = getJestTestName(event.test)
187
+
188
+ if (this.isEarlyFlakeDetectionEnabled) {
189
+ const originalTestName = removeEfdStringFromTestName(testName)
190
+ isNewTest = retriedTestsToNumAttempts.has(originalTestName)
191
+ if (isNewTest) {
192
+ numEfdRetry = retriedTestsToNumAttempts.get(originalTestName)
193
+ retriedTestsToNumAttempts.set(originalTestName, numEfdRetry + 1)
194
+ }
195
+ }
196
+
132
197
  asyncResource.runInAsyncScope(() => {
133
198
  testStartCh.publish({
134
- name: getJestTestName(event.test),
199
+ name: removeEfdStringFromTestName(testName),
135
200
  suite: this.testSuite,
201
+ testSourceFile: this.testSourceFile,
136
202
  runner: 'jest-circus',
137
203
  testParameters,
138
- frameworkVersion: jestVersion
204
+ frameworkVersion: jestVersion,
205
+ isNew: isNewTest,
206
+ isEfdRetry: numEfdRetry > 0
139
207
  })
140
208
  originalTestFns.set(event.test, event.test.fn)
141
209
  event.test.fn = asyncResource.bind(event.test.fn)
142
210
  })
143
211
  }
212
+ if (event.name === 'add_test') {
213
+ if (this.isEarlyFlakeDetectionEnabled) {
214
+ const testName = this.getTestNameFromAddTestEvent(event, state)
215
+ const isNew = !this.knownTestsForThisSuite?.includes(testName)
216
+ const isSkipped = event.mode === 'todo' || event.mode === 'skip'
217
+ if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testName)) {
218
+ retriedTestsToNumAttempts.set(testName, 0)
219
+ for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
220
+ if (this.global.test) {
221
+ this.global.test(getEfdTestName(event.testName, retryIndex), event.fn, event.timeout)
222
+ } else {
223
+ log.error('Early flake detection could not retry test because global.test is undefined')
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
144
229
  if (event.name === 'test_done') {
145
230
  const asyncResource = asyncResources.get(event.test)
146
231
  asyncResource.runInAsyncScope(() => {
@@ -164,6 +249,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
164
249
  testSkippedCh.publish({
165
250
  name: getJestTestName(event.test),
166
251
  suite: this.testSuite,
252
+ testSourceFile: this.testSourceFile,
167
253
  runner: 'jest-circus',
168
254
  frameworkVersion: jestVersion,
169
255
  testStartLine: getTestLineStart(event.test.asyncError, this.testSuite)
@@ -206,7 +292,7 @@ addHook({
206
292
  }
207
293
  // TODO: could we get the rootDir from each test?
208
294
  const [test] = shardedTests
209
- const rootDir = test && test.context && test.context.config && test.context.config.rootDir
295
+ const rootDir = test?.context?.config?.rootDir
210
296
 
211
297
  const jestSuitesToRun = getJestSuitesToRun(skippableSuites, shardedTests, rootDir || process.cwd())
212
298
 
@@ -234,24 +320,45 @@ function cliWrapper (cli, jestVersion) {
234
320
  const configurationPromise = new Promise((resolve) => {
235
321
  onDone = resolve
236
322
  })
237
- if (!jestItrConfigurationCh.hasSubscribers) {
323
+ if (!libraryConfigurationCh.hasSubscribers) {
238
324
  return runCLI.apply(this, arguments)
239
325
  }
240
326
 
241
327
  sessionAsyncResource.runInAsyncScope(() => {
242
- jestItrConfigurationCh.publish({ onDone })
328
+ libraryConfigurationCh.publish({ onDone })
243
329
  })
244
330
 
245
331
  try {
246
- const { err, itrConfig } = await configurationPromise
332
+ const { err, libraryConfig } = await configurationPromise
247
333
  if (!err) {
248
- isCodeCoverageEnabled = itrConfig.isCodeCoverageEnabled
249
- isSuitesSkippingEnabled = itrConfig.isSuitesSkippingEnabled
334
+ isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled
335
+ isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
336
+ isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
337
+ earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
250
338
  }
251
339
  } catch (err) {
252
340
  log.error(err)
253
341
  }
254
342
 
343
+ if (isEarlyFlakeDetectionEnabled) {
344
+ const knownTestsPromise = new Promise((resolve) => {
345
+ onDone = resolve
346
+ })
347
+
348
+ sessionAsyncResource.runInAsyncScope(() => {
349
+ knownTestsCh.publish({ onDone })
350
+ })
351
+
352
+ try {
353
+ const { err, knownTests: receivedKnownTests } = await knownTestsPromise
354
+ if (!err) {
355
+ knownTests = receivedKnownTests
356
+ }
357
+ } catch (err) {
358
+ log.error(err)
359
+ }
360
+ }
361
+
255
362
  if (isSuitesSkippingEnabled) {
256
363
  const skippableSuitesPromise = new Promise((resolve) => {
257
364
  onDone = resolve
@@ -311,6 +418,21 @@ function cliWrapper (cli, jestVersion) {
311
418
  status = 'fail'
312
419
  error = new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
313
420
  }
421
+ let timeoutId
422
+
423
+ // Pass the resolve callback to defer it to DC listener
424
+ const flushPromise = new Promise((resolve) => {
425
+ onDone = () => {
426
+ clearTimeout(timeoutId)
427
+ resolve()
428
+ }
429
+ })
430
+
431
+ const timeoutPromise = new Promise((resolve) => {
432
+ timeoutId = setTimeout(() => {
433
+ resolve('timeout')
434
+ }, FLUSH_TIMEOUT).unref()
435
+ })
314
436
 
315
437
  sessionAsyncResource.runInAsyncScope(() => {
316
438
  testSessionFinishCh.publish({
@@ -322,9 +444,16 @@ function cliWrapper (cli, jestVersion) {
322
444
  numSkippedSuites,
323
445
  hasUnskippableSuites,
324
446
  hasForcedToRunSuites,
325
- error
447
+ error,
448
+ isEarlyFlakeDetectionEnabled,
449
+ onDone
326
450
  })
327
451
  })
452
+ const waitingResult = await Promise.race([flushPromise, timeoutPromise])
453
+
454
+ if (waitingResult === 'timeout') {
455
+ log.error('Timeout waiting for the tracer to flush')
456
+ }
328
457
 
329
458
  numSkippedSuites = 0
330
459
 
@@ -438,10 +567,15 @@ function configureTestEnvironment (readConfigsResult) {
438
567
  // because `jestAdapterWrapper` runs in a different process. We have to go through `testEnvironmentOptions`
439
568
  configs.forEach(config => {
440
569
  config.testEnvironmentOptions._ddTestCodeCoverageEnabled = isCodeCoverageEnabled
570
+ config.testEnvironmentOptions._ddKnownTests = knownTests
441
571
  })
442
572
 
443
573
  isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
444
574
 
575
+ if (readConfigsResult.globalConfig.forceExit) {
576
+ log.warn("Jest's '--forceExit' flag has been passed. This may cause loss of data.")
577
+ }
578
+
445
579
  if (isCodeCoverageEnabled) {
446
580
  const globalConfig = {
447
581
  ...readConfigsResult.globalConfig,
@@ -498,6 +632,10 @@ addHook({
498
632
  _ddForcedToRun,
499
633
  _ddUnskippable,
500
634
  _ddItrCorrelationId,
635
+ _ddKnownTests,
636
+ _ddIsEarlyFlakeDetectionEnabled,
637
+ _ddEarlyFlakeDetectionNumRetries,
638
+ _ddRepositoryRoot,
501
639
  ...restOfTestEnvironmentOptions
502
640
  } = testEnvironmentOptions
503
641
 
@@ -11,7 +11,9 @@ const {
11
11
  mergeCoverage,
12
12
  getTestSuitePath,
13
13
  fromCoverageMapToCoverage,
14
- getCallSites
14
+ getCallSites,
15
+ addEfdStringToTestName,
16
+ removeEfdStringFromTestName
15
17
  } = require('../../dd-trace/src/plugins/util/test')
16
18
 
17
19
  const testStartCh = channel('ci:mocha:test:start')
@@ -20,7 +22,8 @@ const skipCh = channel('ci:mocha:test:skip')
20
22
  const testFinishCh = channel('ci:mocha:test:finish')
21
23
  const parameterizedTestCh = channel('ci:mocha:test:parameterize')
22
24
 
23
- const itrConfigurationCh = channel('ci:mocha:itr-configuration')
25
+ const libraryConfigurationCh = channel('ci:mocha:library-configuration')
26
+ const knownTestsCh = channel('ci:mocha:known-tests')
24
27
  const skippableSuitesCh = channel('ci:mocha:test-suite:skippable')
25
28
 
26
29
  const testSessionStartCh = channel('ci:mocha:session:start')
@@ -40,6 +43,7 @@ const testToAr = new WeakMap()
40
43
  const originalFns = new WeakMap()
41
44
  const testFileToSuiteAr = new Map()
42
45
  const testToStartLine = new WeakMap()
46
+ const newTests = {}
43
47
 
44
48
  // `isWorker` is true if it's a Mocha worker
45
49
  let isWorker = false
@@ -54,6 +58,10 @@ let skippedSuites = []
54
58
  const unskippableSuites = []
55
59
  let isForcedToRun = false
56
60
  let itrCorrelationId = ''
61
+ let isEarlyFlakeDetectionEnabled = false
62
+ let earlyFlakeDetectionNumRetries = 0
63
+ let isSuitesSkippingEnabled = false
64
+ let knownTests = []
57
65
 
58
66
  function getSuitesByTestFile (root) {
59
67
  const suitesByTestFile = {}
@@ -93,6 +101,26 @@ function isRetry (test) {
93
101
  return test._currentRetry !== undefined && test._currentRetry !== 0
94
102
  }
95
103
 
104
+ function getTestFullName (test) {
105
+ return `mocha.${getTestSuitePath(test.file, process.cwd())}.${removeEfdStringFromTestName(test.fullTitle())}`
106
+ }
107
+
108
+ function isNewTest (test) {
109
+ return !knownTests.includes(getTestFullName(test))
110
+ }
111
+
112
+ function retryTest (test) {
113
+ const originalTestName = test.title
114
+ const suite = test.parent
115
+ for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
116
+ const clonedTest = test.clone()
117
+ clonedTest.title = addEfdStringToTestName(originalTestName, retryIndex + 1)
118
+ suite.addTest(clonedTest)
119
+ clonedTest._ddIsNew = true
120
+ clonedTest._ddIsEfdRetry = true
121
+ }
122
+ }
123
+
96
124
  function getTestAsyncResource (test) {
97
125
  if (!test.fn) {
98
126
  return testToAr.get(test)
@@ -123,6 +151,19 @@ function mochaHook (Runner) {
123
151
 
124
152
  patched.add(Runner)
125
153
 
154
+ shimmer.wrap(Runner.prototype, 'runTests', runTests => function (suite, fn) {
155
+ if (isEarlyFlakeDetectionEnabled) {
156
+ // by the time we reach `this.on('test')`, it is too late. We need to add retries here
157
+ suite.tests.forEach(test => {
158
+ if (!test.isPending() && isNewTest(test)) {
159
+ test._ddIsNew = true
160
+ retryTest(test)
161
+ }
162
+ })
163
+ }
164
+ return runTests.apply(this, arguments)
165
+ })
166
+
126
167
  shimmer.wrap(Runner.prototype, 'run', run => function () {
127
168
  if (!testStartCh.hasSubscribers || isWorker) {
128
169
  return run.apply(this, arguments)
@@ -144,6 +185,24 @@ function mochaHook (Runner) {
144
185
  status = 'fail'
145
186
  }
146
187
 
188
+ if (isEarlyFlakeDetectionEnabled) {
189
+ /**
190
+ * If Early Flake Detection (EFD) is enabled the logic is as follows:
191
+ * - If all attempts for a test are failing, the test has failed and we will let the test process fail.
192
+ * - If just a single attempt passes, we will prevent the test process from failing.
193
+ * The rationale behind is the following: you may still be able to block your CI pipeline by gating
194
+ * on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
195
+ */
196
+ for (const tests of Object.values(newTests)) {
197
+ const failingNewTests = tests.filter(test => test.isFailed())
198
+ const areAllNewTestsFailing = failingNewTests.length === tests.length
199
+ if (failingNewTests.length && !areAllNewTestsFailing) {
200
+ this.stats.failures -= failingNewTests.length
201
+ this.failures -= failingNewTests.length
202
+ }
203
+ }
204
+ }
205
+
147
206
  if (status === 'fail') {
148
207
  error = new Error(`Failed tests: ${this.failures}.`)
149
208
  }
@@ -168,7 +227,8 @@ function mochaHook (Runner) {
168
227
  numSkippedSuites: skippedSuites.length,
169
228
  hasForcedToRunSuites: isForcedToRun,
170
229
  hasUnskippableSuites: !!unskippableSuites.length,
171
- error
230
+ error,
231
+ isEarlyFlakeDetectionEnabled
172
232
  })
173
233
  }))
174
234
 
@@ -253,8 +313,35 @@ function mochaHook (Runner) {
253
313
  const testStartLine = testToStartLine.get(test)
254
314
  const asyncResource = new AsyncResource('bound-anonymous-fn')
255
315
  testToAr.set(test.fn, asyncResource)
316
+
317
+ const {
318
+ file: testSuiteAbsolutePath,
319
+ title,
320
+ _ddIsNew: isNew,
321
+ _ddIsEfdRetry: isEfdRetry
322
+ } = test
323
+
324
+ const testInfo = {
325
+ testName: test.fullTitle(),
326
+ testSuiteAbsolutePath,
327
+ title,
328
+ isNew,
329
+ isEfdRetry,
330
+ testStartLine
331
+ }
332
+
333
+ // We want to store the result of the new tests
334
+ if (isNew) {
335
+ const testFullName = getTestFullName(test)
336
+ if (newTests[testFullName]) {
337
+ newTests[testFullName].push(test)
338
+ } else {
339
+ newTests[testFullName] = [test]
340
+ }
341
+ }
342
+
256
343
  asyncResource.runInAsyncScope(() => {
257
- testStartCh.publish({ test, testStartLine })
344
+ testStartCh.publish(testInfo)
258
345
  })
259
346
  })
260
347
 
@@ -323,10 +410,23 @@ function mochaHook (Runner) {
323
410
  })
324
411
 
325
412
  this.on('pending', (test) => {
413
+ const testStartLine = testToStartLine.get(test)
414
+ const {
415
+ file: testSuiteAbsolutePath,
416
+ title
417
+ } = test
418
+
419
+ const testInfo = {
420
+ testName: test.fullTitle(),
421
+ testSuiteAbsolutePath,
422
+ title,
423
+ testStartLine
424
+ }
425
+
326
426
  const asyncResource = getTestAsyncResource(test)
327
427
  if (asyncResource) {
328
428
  asyncResource.runInAsyncScope(() => {
329
- skipCh.publish(test)
429
+ skipCh.publish(testInfo)
330
430
  })
331
431
  } else {
332
432
  // if there is no async resource, the test has been skipped through `test.skip`
@@ -338,7 +438,7 @@ function mochaHook (Runner) {
338
438
  testToAr.set(test, skippedTestAsyncResource)
339
439
  }
340
440
  skippedTestAsyncResource.runInAsyncScope(() => {
341
- skipCh.publish(test)
441
+ skipCh.publish(testInfo)
342
442
  })
343
443
  }
344
444
  })
@@ -358,8 +458,8 @@ function mochaEachHook (mochaEach) {
358
458
  const [params] = arguments
359
459
  const { it, ...rest } = mochaEach.apply(this, arguments)
360
460
  return {
361
- it: function (name) {
362
- parameterizedTestCh.publish({ name, params })
461
+ it: function (title) {
462
+ parameterizedTestCh.publish({ title, params })
363
463
  it.apply(this, arguments)
364
464
  },
365
465
  ...rest
@@ -384,7 +484,7 @@ addHook({
384
484
  return run.apply(this, arguments)
385
485
  }
386
486
 
387
- if (!itrConfigurationCh.hasSubscribers || this.isWorker) {
487
+ if (!libraryConfigurationCh.hasSubscribers || this.isWorker) {
388
488
  if (this.isWorker) {
389
489
  isWorker = true
390
490
  }
@@ -425,21 +525,47 @@ addHook({
425
525
  global.run()
426
526
  }
427
527
 
428
- const onReceivedConfiguration = ({ err }) => {
528
+ const onReceivedKnownTests = ({ err, knownTests: receivedKnownTests }) => {
429
529
  if (err) {
430
- return global.run()
530
+ knownTests = []
531
+ isEarlyFlakeDetectionEnabled = false
532
+ } else {
533
+ knownTests = receivedKnownTests
534
+ }
535
+
536
+ if (isSuitesSkippingEnabled) {
537
+ skippableSuitesCh.publish({
538
+ onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites)
539
+ })
540
+ } else {
541
+ global.run()
431
542
  }
432
- if (!skippableSuitesCh.hasSubscribers) {
543
+ }
544
+
545
+ const onReceivedConfiguration = ({ err, libraryConfig }) => {
546
+ if (err || !skippableSuitesCh.hasSubscribers || !knownTestsCh.hasSubscribers) {
433
547
  return global.run()
434
548
  }
435
549
 
436
- skippableSuitesCh.publish({
437
- onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites)
438
- })
550
+ isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
551
+ isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
552
+ earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
553
+
554
+ if (isEarlyFlakeDetectionEnabled) {
555
+ knownTestsCh.publish({
556
+ onDone: mochaRunAsyncResource.bind(onReceivedKnownTests)
557
+ })
558
+ } else if (isSuitesSkippingEnabled) {
559
+ skippableSuitesCh.publish({
560
+ onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites)
561
+ })
562
+ } else {
563
+ global.run()
564
+ }
439
565
  }
440
566
 
441
567
  mochaRunAsyncResource.runInAsyncScope(() => {
442
- itrConfigurationCh.publish({
568
+ libraryConfigurationCh.publish({
443
569
  onDone: mochaRunAsyncResource.bind(onReceivedConfiguration)
444
570
  })
445
571
  })
@@ -79,21 +79,26 @@ addHook({
79
79
  })
80
80
 
81
81
  let callbackWrapped = false
82
- const lastArgumentIndex = arguments.length - 1
83
82
 
84
- if (typeof arguments[lastArgumentIndex] === 'function') {
85
- // is a callback, wrap it to execute finish()
86
- shimmer.wrap(arguments, lastArgumentIndex, originalCb => {
87
- return function () {
88
- finish()
83
+ const wrapCallbackIfExist = (args) => {
84
+ const lastArgumentIndex = args.length - 1
89
85
 
90
- return originalCb.apply(this, arguments)
91
- }
92
- })
86
+ if (typeof args[lastArgumentIndex] === 'function') {
87
+ // is a callback, wrap it to execute finish()
88
+ shimmer.wrap(args, lastArgumentIndex, originalCb => {
89
+ return function () {
90
+ finish()
91
+
92
+ return originalCb.apply(this, arguments)
93
+ }
94
+ })
93
95
 
94
- callbackWrapped = true
96
+ callbackWrapped = true
97
+ }
95
98
  }
96
99
 
100
+ wrapCallbackIfExist(arguments)
101
+
97
102
  return asyncResource.runInAsyncScope(() => {
98
103
  startCh.publish({
99
104
  filters,
@@ -106,8 +111,16 @@ addHook({
106
111
  if (!callbackWrapped) {
107
112
  shimmer.wrap(res, 'exec', originalExec => {
108
113
  return function wrappedExec () {
114
+ if (!callbackWrapped) {
115
+ wrapCallbackIfExist(arguments)
116
+ }
117
+
109
118
  const execResult = originalExec.apply(this, arguments)
110
119
 
120
+ if (callbackWrapped || typeof execResult?.then !== 'function') {
121
+ return execResult
122
+ }
123
+
111
124
  // wrap them method, wrap resolve and reject methods
112
125
  shimmer.wrap(execResult, 'then', originalThen => {
113
126
  return function wrappedThen () {
@@ -290,9 +290,23 @@ addHook({
290
290
  shimmer.massWrap(request.NextRequest.prototype, ['text', 'json'], function (originalMethod) {
291
291
  return async function wrappedJson () {
292
292
  const body = await originalMethod.apply(this, arguments)
293
- bodyParsedChannel.publish({
294
- body
295
- })
293
+
294
+ bodyParsedChannel.publish({ body })
295
+
296
+ return body
297
+ }
298
+ })
299
+
300
+ shimmer.wrap(request.NextRequest.prototype, 'formData', function (originalFormData) {
301
+ return async function wrappedFormData () {
302
+ const body = await originalFormData.apply(this, arguments)
303
+
304
+ let normalizedBody = body
305
+ if (typeof body.entries === 'function') {
306
+ normalizedBody = Object.fromEntries(body.entries())
307
+ }
308
+ bodyParsedChannel.publish({ body: normalizedBody })
309
+
296
310
  return body
297
311
  }
298
312
  })