dd-trace 5.106.0 → 5.108.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 (105) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +9 -11
  3. package/packages/datadog-core/src/storage.js +47 -48
  4. package/packages/datadog-esbuild/index.js +6 -1
  5. package/packages/datadog-instrumentations/src/ai.js +12 -3
  6. package/packages/datadog-instrumentations/src/body-parser.js +5 -2
  7. package/packages/datadog-instrumentations/src/connect.js +3 -2
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
  9. package/packages/datadog-instrumentations/src/cucumber.js +7 -0
  10. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
  11. package/packages/datadog-instrumentations/src/express-session.js +12 -11
  12. package/packages/datadog-instrumentations/src/express.js +24 -20
  13. package/packages/datadog-instrumentations/src/fastify.js +18 -6
  14. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
  15. package/packages/datadog-instrumentations/src/http/client.js +9 -12
  16. package/packages/datadog-instrumentations/src/http/server.js +30 -16
  17. package/packages/datadog-instrumentations/src/http2/client.js +15 -12
  18. package/packages/datadog-instrumentations/src/http2/server.js +15 -8
  19. package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
  20. package/packages/datadog-instrumentations/src/jest.js +143 -73
  21. package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
  23. package/packages/datadog-instrumentations/src/multer.js +3 -2
  24. package/packages/datadog-instrumentations/src/mysql2.js +34 -0
  25. package/packages/datadog-instrumentations/src/net.js +8 -6
  26. package/packages/datadog-instrumentations/src/openai.js +19 -7
  27. package/packages/datadog-instrumentations/src/pg.js +19 -0
  28. package/packages/datadog-instrumentations/src/router.js +12 -10
  29. package/packages/datadog-instrumentations/src/vitest.js +29 -4
  30. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
  31. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
  32. package/packages/datadog-plugin-cucumber/src/index.js +2 -0
  33. package/packages/datadog-plugin-cypress/src/support.js +31 -1
  34. package/packages/datadog-plugin-http/src/client.js +0 -3
  35. package/packages/datadog-plugin-http/src/server.js +11 -1
  36. package/packages/datadog-plugin-mocha/src/index.js +2 -0
  37. package/packages/datadog-plugin-pg/src/index.js +10 -0
  38. package/packages/dd-trace/src/aiguard/index.js +34 -15
  39. package/packages/dd-trace/src/aiguard/sdk.js +34 -3
  40. package/packages/dd-trace/src/aiguard/tags.js +6 -0
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +3 -2
  42. package/packages/dd-trace/src/appsec/iast/index.js +3 -2
  43. package/packages/dd-trace/src/appsec/rasp/ssrf.js +2 -1
  44. package/packages/dd-trace/src/appsec/reporter.js +1 -1
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
  46. package/packages/dd-trace/src/config/defaults.js +14 -0
  47. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -1
  48. package/packages/dd-trace/src/config/helper.js +1 -0
  49. package/packages/dd-trace/src/config/index.js +5 -9
  50. package/packages/dd-trace/src/config/parsers.js +8 -0
  51. package/packages/dd-trace/src/config/supported-configurations.json +20 -6
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  53. package/packages/dd-trace/src/datastreams/writer.js +1 -2
  54. package/packages/dd-trace/src/debugger/config.js +1 -1
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
  56. package/packages/dd-trace/src/debugger/index.js +1 -2
  57. package/packages/dd-trace/src/dogstatsd.js +2 -3
  58. package/packages/dd-trace/src/encode/0.4.js +49 -41
  59. package/packages/dd-trace/src/encode/agentless-json.js +5 -1
  60. package/packages/dd-trace/src/encode/tags-processors.js +14 -0
  61. package/packages/dd-trace/src/exporters/agent/index.js +1 -2
  62. package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
  63. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
  64. package/packages/dd-trace/src/exporters/common/request.js +26 -0
  65. package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
  66. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
  67. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
  68. package/packages/dd-trace/src/llmobs/sdk.js +4 -1
  69. package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
  70. package/packages/dd-trace/src/llmobs/tagger.js +5 -3
  71. package/packages/dd-trace/src/llmobs/util.js +54 -0
  72. package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
  73. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  74. package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
  75. package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
  76. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
  77. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
  78. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
  79. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +3 -2
  80. package/packages/dd-trace/src/opentracing/span.js +23 -18
  81. package/packages/dd-trace/src/opentracing/tracer.js +16 -12
  82. package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
  83. package/packages/dd-trace/src/priority_sampler.js +6 -5
  84. package/packages/dd-trace/src/profiling/config.js +3 -2
  85. package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
  86. package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
  87. package/packages/dd-trace/src/proxy.js +13 -10
  88. package/packages/dd-trace/src/remote_config/index.js +1 -2
  89. package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
  90. package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
  91. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
  92. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
  93. package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
  94. package/packages/dd-trace/src/span_format.js +33 -25
  95. package/packages/dd-trace/src/span_stats.js +1 -1
  96. package/packages/dd-trace/src/startup-log.js +1 -2
  97. package/packages/dd-trace/src/telemetry/send-data.js +1 -1
  98. package/packages/dd-trace/src/tracer.js +1 -1
  99. package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
  100. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  101. package/vendor/dist/protobufjs/index.js +1 -1
  102. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  103. package/vendor/dist/shell-quote/index.js +1 -1
  104. package/packages/dd-trace/src/agent/url.js +0 -28
  105. package/scripts/preinstall.js +0 -34
@@ -86,7 +86,10 @@ const CHILD_MESSAGE_CALL = 1
86
86
 
87
87
  // Maximum time we'll wait for the tracer to flush
88
88
  const FLUSH_TIMEOUT = 10_000
89
+ const JEST_SESSION_STATE = Symbol.for('dd-trace:jest:session')
90
+ const JEST_BAIL_REPORTER_PATH = require.resolve('./jest/bail-reporter')
89
91
  const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
92
+ const jestSessionState = globalThis[JEST_SESSION_STATE] || (globalThis[JEST_SESSION_STATE] = {})
90
93
 
91
94
  // https://github.com/jestjs/jest/blob/41f842a46bb2691f828c3a5f27fc1d6290495b82/packages/jest-circus/src/types.ts#L9C8-L9C54
92
95
  const RETRY_TIMES = Symbol.for('RETRY_TIMES')
@@ -166,6 +169,7 @@ const MINIMUM_JEST_COVERAGE_BACKFILL_VERSION = '>=28.0.0'
166
169
  const atrSuppressedErrors = new Map()
167
170
  let hasWarnedDeprecatedJestVersion = false
168
171
  let isJestCoverageBackfillSupported = false
172
+ let hasFinishedTestSession = false
169
173
 
170
174
  // Track quarantined tests whose errors were suppressed, keyed by "suite › testName"
171
175
  const quarantinedFailingTests = new Set()
@@ -1218,6 +1222,25 @@ function getCoverageBackfillRequire (CoverageReporter) {
1218
1222
  return require
1219
1223
  }
1220
1224
 
1225
+ function addDatadogBailReporter (globalConfig) {
1226
+ if (!globalConfig.bail) return globalConfig
1227
+
1228
+ const reporters = globalConfig.reporters || [['default', {}]]
1229
+ for (const [reporter] of reporters) {
1230
+ if (reporter === JEST_BAIL_REPORTER_PATH) {
1231
+ return globalConfig
1232
+ }
1233
+ }
1234
+
1235
+ return {
1236
+ ...globalConfig,
1237
+ reporters: [
1238
+ ...reporters,
1239
+ [JEST_BAIL_REPORTER_PATH, {}],
1240
+ ],
1241
+ }
1242
+ }
1243
+
1221
1244
  function getTestContexts (tests) {
1222
1245
  if (!tests?.length) return
1223
1246
 
@@ -1283,6 +1306,115 @@ function applySkippedCoverageToJestCoverageMap (coverageMap, rootDir) {
1283
1306
  )
1284
1307
  }
1285
1308
 
1309
+ function getSessionFinishError (results) {
1310
+ const numFailedTestSuites = results?.numFailedTestSuites || 0
1311
+ const numFailedTests = results?.numFailedTests || 0
1312
+
1313
+ return new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
1314
+ }
1315
+
1316
+ function getTestSessionCoveragePayload (results, fallbackRootDir) {
1317
+ const payload = {}
1318
+ if (!shouldReportCodeCoverageLinesPct()) return payload
1319
+
1320
+ try {
1321
+ const coverageMap = results?.coverageMap || lastCoverageMap
1322
+ const coverageRootDir = lastCoverageMapRootDir ||
1323
+ repositoryRoot ||
1324
+ fallbackRootDir ||
1325
+ process.cwd()
1326
+ if (isSuitesSkipped) {
1327
+ applySkippedCoverageToJestCoverageMap(coverageMap, coverageRootDir)
1328
+ }
1329
+ payload.testCodeCoverageLinesTotal = getTestCoverageLinesPercentage(
1330
+ coverageMap,
1331
+ undefined,
1332
+ coverageRootDir
1333
+ )
1334
+ if (isTiaCoverageBackfillEnabled()) {
1335
+ payload.testSessionCoverageFiles = getExecutableFilesFromCoverage(coverageMap).map(({ filename, bitmap }) => ({
1336
+ filename: getTestSuitePath(filename, coverageRootDir),
1337
+ bitmap,
1338
+ }))
1339
+ }
1340
+ } catch {
1341
+ // ignore errors
1342
+ }
1343
+
1344
+ return payload
1345
+ }
1346
+
1347
+ function getNumBailFailures (results) {
1348
+ const numFailedTests = results?.numFailedTests || 0
1349
+ const numFailedSuites = results?.numRuntimeErrorTestSuites === undefined
1350
+ ? (numFailedTests === 0 ? results?.numFailedTestSuites || 0 : 0)
1351
+ : results.numRuntimeErrorTestSuites
1352
+
1353
+ return numFailedTests + numFailedSuites
1354
+ }
1355
+
1356
+ function shouldFinishBailTestSession (globalConfig, results) {
1357
+ return !!globalConfig?.bail && getNumBailFailures(results) >= globalConfig.bail
1358
+ }
1359
+
1360
+ async function waitForTestSessionFinish (payload) {
1361
+ if (!testSessionFinishCh.hasSubscribers || hasFinishedTestSession) return
1362
+
1363
+ hasFinishedTestSession = true
1364
+
1365
+ let timeoutId
1366
+
1367
+ const flushPromise = new Promise((resolve) => {
1368
+ payload.onDone = () => {
1369
+ clearTimeout(timeoutId)
1370
+ resolve()
1371
+ }
1372
+ })
1373
+
1374
+ const timeoutPromise = new Promise((resolve) => {
1375
+ timeoutId = realSetTimeout(() => {
1376
+ resolve('timeout')
1377
+ }, FLUSH_TIMEOUT)
1378
+ timeoutId.unref?.()
1379
+ })
1380
+
1381
+ testSessionFinishCh.publish(payload)
1382
+
1383
+ const waitingResult = await Promise.race([flushPromise, timeoutPromise])
1384
+
1385
+ if (waitingResult === 'timeout') {
1386
+ log.error('Timeout waiting for the tracer to flush')
1387
+ }
1388
+ }
1389
+
1390
+ function getTestSessionFinishPayload (status, error, extra = {}) {
1391
+ return {
1392
+ status,
1393
+ isSuitesSkipped,
1394
+ isSuitesSkippingEnabled,
1395
+ isCodeCoverageEnabled,
1396
+ isCoverageReportUploadEnabled,
1397
+ numSkippedSuites,
1398
+ hasUnskippableSuites,
1399
+ hasForcedToRunSuites,
1400
+ error,
1401
+ isEarlyFlakeDetectionEnabled,
1402
+ isEarlyFlakeDetectionFaulty,
1403
+ isTestManagementTestsEnabled,
1404
+ ...extra,
1405
+ }
1406
+ }
1407
+
1408
+ async function finishBailTestSession (results, fallbackRootDir) {
1409
+ await waitForTestSessionFinish(getTestSessionFinishPayload(
1410
+ 'fail',
1411
+ getSessionFinishError(results),
1412
+ getTestSessionCoveragePayload(results, fallbackRootDir)
1413
+ ))
1414
+ }
1415
+
1416
+ jestSessionState.finishBailTestSession = finishBailTestSession
1417
+
1286
1418
  function reporterDispatcherWrapper (reporterDispatcherPackage) {
1287
1419
  const ReporterDispatcher = reporterDispatcherPackage.default ?? reporterDispatcherPackage
1288
1420
  if (ReporterDispatcher?.prototype?.onRunComplete) {
@@ -1351,7 +1483,11 @@ function wrapCoverageReporter (CoverageReporter, hookMeta) {
1351
1483
  }
1352
1484
  lastCoverageMap = coverageMap
1353
1485
  lastCoverageMapRootDir = rootDir
1354
- return onRunComplete.call(this, coverageContexts, results)
1486
+ const result = await onRunComplete.call(this, coverageContexts, results)
1487
+ if (shouldFinishBailTestSession(this._globalConfig, results)) {
1488
+ await finishBailTestSession(results, rootDir)
1489
+ }
1490
+ return result
1355
1491
  })
1356
1492
  }
1357
1493
 
@@ -1462,12 +1598,12 @@ function getCliWrapper (isNewJestVersion) {
1462
1598
  )
1463
1599
  }
1464
1600
  return shimmer.wrap(cli, 'runCLI', runCLI => async function () {
1465
- let onDone
1466
1601
  if (!libraryConfigurationCh.hasSubscribers) {
1467
1602
  return runCLI.apply(this, arguments)
1468
1603
  }
1469
1604
 
1470
1605
  resetSuiteSkippingRunState()
1606
+ hasFinishedTestSession = false
1471
1607
 
1472
1608
  try {
1473
1609
  const { err, libraryConfig } = await getChannelPromise(libraryConfigurationCh, {
@@ -1576,7 +1712,6 @@ function getCliWrapper (isNewJestVersion) {
1576
1712
 
1577
1713
  const {
1578
1714
  results: {
1579
- coverageMap: resultCoverageMap,
1580
1715
  numFailedTestSuites,
1581
1716
  numFailedTests,
1582
1717
  numRuntimeErrorTestSuites = 0,
@@ -1591,36 +1726,6 @@ function getCliWrapper (isNewJestVersion) {
1591
1726
  const hasRunLevelFailure = runExecError != null || wasInterrupted === true
1592
1727
  const mustNotFlipSuccess = hasSuiteLevelFailures || hasRunLevelFailure
1593
1728
 
1594
- let testCodeCoverageLinesTotal
1595
- let testSessionCoverageFiles
1596
- const shouldReportTestSessionCoverage = isTiaCoverageBackfillEnabled()
1597
-
1598
- if (shouldReportCodeCoverageLinesPct()) {
1599
- try {
1600
- const coverageMap = resultCoverageMap || lastCoverageMap
1601
- const coverageRootDir = lastCoverageMapRootDir ||
1602
- repositoryRoot ||
1603
- result.globalConfig?.rootDir ||
1604
- process.cwd()
1605
- if (isSuitesSkipped) {
1606
- applySkippedCoverageToJestCoverageMap(coverageMap, coverageRootDir)
1607
- }
1608
- testCodeCoverageLinesTotal = getTestCoverageLinesPercentage(
1609
- coverageMap,
1610
- undefined,
1611
- coverageRootDir
1612
- )
1613
- if (shouldReportTestSessionCoverage) {
1614
- testSessionCoverageFiles = getExecutableFilesFromCoverage(coverageMap).map(({ filename, bitmap }) => ({
1615
- filename: getTestSuitePath(filename, coverageRootDir),
1616
- bitmap,
1617
- }))
1618
- }
1619
- } catch {
1620
- // ignore errors
1621
- }
1622
- }
1623
-
1624
1729
  /**
1625
1730
  * If Early Flake Detection (EFD) is enabled the logic is as follows:
1626
1731
  * - If all attempts for a test are failing, the test has failed and we will let the test process fail.
@@ -1774,46 +1879,9 @@ function getCliWrapper (isNewJestVersion) {
1774
1879
  error = new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
1775
1880
  }
1776
1881
 
1777
- let timeoutId
1778
-
1779
- // Pass the resolve callback to defer it to DC listener
1780
- const flushPromise = new Promise((resolve) => {
1781
- onDone = () => {
1782
- clearTimeout(timeoutId)
1783
- resolve()
1784
- }
1785
- })
1786
-
1787
- const timeoutPromise = new Promise((resolve) => {
1788
- timeoutId = realSetTimeout(() => {
1789
- resolve('timeout')
1790
- }, FLUSH_TIMEOUT)
1791
- timeoutId.unref?.()
1792
- })
1793
-
1794
- testSessionFinishCh.publish({
1795
- status,
1796
- isSuitesSkipped,
1797
- isSuitesSkippingEnabled,
1798
- isCodeCoverageEnabled,
1799
- isCoverageReportUploadEnabled,
1800
- testCodeCoverageLinesTotal,
1801
- testSessionCoverageFiles,
1802
- numSkippedSuites,
1803
- hasUnskippableSuites,
1804
- hasForcedToRunSuites,
1805
- error,
1806
- isEarlyFlakeDetectionEnabled,
1807
- isEarlyFlakeDetectionFaulty,
1808
- isTestManagementTestsEnabled,
1809
- onDone,
1810
- })
1811
-
1812
- const waitingResult = await Promise.race([flushPromise, timeoutPromise])
1813
-
1814
- if (waitingResult === 'timeout') {
1815
- log.error('Timeout waiting for the tracer to flush')
1816
- }
1882
+ await waitForTestSessionFinish(getTestSessionFinishPayload(status, error, {
1883
+ ...getTestSessionCoveragePayload(result.results, result.globalConfig?.rootDir),
1884
+ }))
1817
1885
 
1818
1886
  if (codeCoverageReportCh.hasSubscribers) {
1819
1887
  const rootDir = result.globalConfig?.rootDir || process.cwd()
@@ -2100,6 +2168,8 @@ function configureTestEnvironment (readConfigsResult) {
2100
2168
  log.warn("Jest's '--forceExit' flag has been passed. This may cause loss of data.")
2101
2169
  }
2102
2170
 
2171
+ readConfigsResult.globalConfig = addDatadogBailReporter(readConfigsResult.globalConfig)
2172
+
2103
2173
  if (isSuitesSkippingEnabled) {
2104
2174
  // If suite skipping is enabled, we pass `passWithNoTests` in case every test gets skipped.
2105
2175
  const globalConfig = {
@@ -24,6 +24,7 @@ const {
24
24
  collectTestOptimizationSummariesFromTraces,
25
25
  logTestOptimizationSummary,
26
26
  getTestOptimizationRequestResults,
27
+ isModifiedTest,
27
28
  } = require('../../../dd-trace/src/plugins/util/test')
28
29
 
29
30
  const {
@@ -39,6 +40,7 @@ const {
39
40
  getOnPendingHandler,
40
41
  testFileToSuiteCtx,
41
42
  newTests,
43
+ efdTests,
42
44
  testsQuarantined,
43
45
  getTestFullName,
44
46
  getRunTestsWrapper,
@@ -191,6 +193,23 @@ function getCoverageRootDir () {
191
193
  return config.repositoryRoot || process.cwd()
192
194
  }
193
195
 
196
+ /**
197
+ * Recomputes whether a parallel worker result belongs to a modified suite.
198
+ *
199
+ * In parallel mode, `_ddIsModified` is set on Mocha Test objects inside the worker.
200
+ * The main process receives `Test.prototype.serialize()` output for test events,
201
+ * and that fixed serialization drops custom properties. We still need modified-test
202
+ * bookkeeping in the main process for EFD failure suppression, so infer it again
203
+ * from the suite path.
204
+ *
205
+ * @param {string} testSuiteAbsolutePath
206
+ * @returns {boolean}
207
+ */
208
+ function isModifiedTestSuite (testSuiteAbsolutePath) {
209
+ const testPath = getTestSuitePath(testSuiteAbsolutePath, getCoverageRootDir())
210
+ return isModifiedTest(testPath, null, null, config.modifiedFiles, 'mocha')
211
+ }
212
+
194
213
  function shouldReportCodeCoverageLinesPct (hasBackfilledCoverage) {
195
214
  return !isSuitesSkipped || hasBackfilledCoverage
196
215
  }
@@ -252,12 +271,13 @@ function getOnEndHandler (isParallel) {
252
271
  * The rationale behind is the following: you may still be able to block your CI pipeline by gating
253
272
  * on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
254
273
  */
255
- for (const tests of Object.values(newTests)) {
256
- const failingNewTests = tests.filter(test => isTestFailed(test))
257
- const areAllNewTestsFailing = failingNewTests.length === tests.length
258
- if (failingNewTests.length && !areAllNewTestsFailing) {
259
- this.stats.failures -= failingNewTests.length
260
- this.failures -= failingNewTests.length
274
+ for (const tests of Object.values(efdTests)) {
275
+ const failingEfdTests = tests.filter(test => isTestFailed(test))
276
+ const areAllEfdTestsFailing = failingEfdTests.length === tests.length
277
+ const nonQuarantinedFailingEfdTests = failingEfdTests.filter(test => !testsQuarantined.has(test))
278
+ if (nonQuarantinedFailingEfdTests.length && !areAllEfdTestsFailing) {
279
+ this.stats.failures -= nonQuarantinedFailingEfdTests.length
280
+ this.failures -= nonQuarantinedFailingEfdTests.length
261
281
  }
262
282
  }
263
283
  }
@@ -1139,10 +1159,15 @@ addHook({
1139
1159
  .events
1140
1160
  .filter(event => event.eventName === 'test end')
1141
1161
  .map(event => event.data)
1162
+ const isModified = config.isImpactedTestsEnabled && isModifiedTestSuite(testSuiteAbsolutePath)
1142
1163
 
1143
1164
  for (const test of tests) {
1165
+ const testProperties = getTestProperties(test, config.testManagementTests)
1166
+ const isAttemptToFix = config.isTestManagementTestsEnabled && testProperties.isAttemptToFix
1167
+
1144
1168
  // `newTests` is filled in the worker process, so we need to use the test results to fill it here too.
1145
- if (config.isKnownTestsEnabled && isNewTest(test, config.knownTests)) {
1169
+ const isNew = config.isKnownTestsEnabled && isNewTest(test, config.knownTests)
1170
+ if (isNew) {
1146
1171
  const testFullName = getTestFullName(test)
1147
1172
  const tests = newTests[testFullName]
1148
1173
 
@@ -1152,8 +1177,18 @@ addHook({
1152
1177
  newTests[testFullName] = [test]
1153
1178
  }
1154
1179
  }
1180
+ // `efdTests` is filled in the worker process, so we need to use the test results to fill it here too.
1181
+ if (!isAttemptToFix && (isNew || isModified)) {
1182
+ const testFullName = getTestFullName(test)
1183
+ const tests = efdTests[testFullName]
1184
+
1185
+ if (tests) {
1186
+ tests.push(test)
1187
+ } else {
1188
+ efdTests[testFullName] = [test]
1189
+ }
1190
+ }
1155
1191
  // `testsQuarantined` is filled in the worker process, so we need to use the test results to fill it here too.
1156
- const testProperties = getTestProperties(test, config.testManagementTests)
1157
1192
  if (config.isTestManagementTestsEnabled && testProperties.isQuarantined && !testProperties.isAttemptToFix) {
1158
1193
  testsQuarantined.add(test)
1159
1194
  }
@@ -35,6 +35,7 @@ const testToStartLine = new WeakMap()
35
35
  const testFileToSuiteCtx = new Map()
36
36
  const wrappedFunctions = new WeakSet()
37
37
  const newTests = {}
38
+ const efdTests = {}
38
39
  const newTestsWithDynamicNames = new Set()
39
40
  const testsAttemptToFix = new Set()
40
41
  const testsQuarantined = new Set()
@@ -129,14 +130,61 @@ function wrapOriginalEfdTest (test, slowTestRetries) {
129
130
  })
130
131
  }
131
132
 
133
+ /**
134
+ * Disables Mocha's native retry mechanism for Datadog-managed clone retries.
135
+ * @param {{ retries?: (count: number) => void }} test
136
+ * @returns {void}
137
+ */
138
+ function disableMochaRetries (test) {
139
+ if (typeof test.retries === 'function') {
140
+ test.retries(0)
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Checks whether a runnable belongs to a Datadog-managed clone retry feature.
146
+ * @param {{
147
+ * _ddIsAttemptToFix?: boolean,
148
+ * _ddIsEfdRetry?: boolean,
149
+ * _ddIsModified?: boolean,
150
+ * _ddIsNew?: boolean
151
+ * }} test
152
+ * @param {{ isEarlyFlakeDetectionEnabled?: boolean }} config
153
+ * @returns {boolean}
154
+ */
155
+ function isDatadogManagedRetryTest (test, config) {
156
+ return test._ddIsAttemptToFix ||
157
+ test._ddIsEfdRetry ||
158
+ (config.isEarlyFlakeDetectionEnabled && (test._ddIsNew || test._ddIsModified))
159
+ }
160
+
161
+ /**
162
+ * Checks whether a runnable belongs to an Early Flake Detection execution.
163
+ * @param {{
164
+ * _ddIsAttemptToFix?: boolean,
165
+ * _ddIsEfdRetry?: boolean,
166
+ * _ddIsModified?: boolean,
167
+ * _ddIsNew?: boolean
168
+ * }} test
169
+ * @param {{ isEarlyFlakeDetectionEnabled?: boolean }} config
170
+ * @returns {boolean}
171
+ */
172
+ function isEarlyFlakeDetectionTest (test, config) {
173
+ return !test._ddIsAttemptToFix &&
174
+ config.isEarlyFlakeDetectionEnabled &&
175
+ (test._ddIsEfdRetry || test._ddIsNew || test._ddIsModified)
176
+ }
177
+
132
178
  function retryTest (test, numRetries, tags, slowTestRetries) {
133
179
  const suite = test.parent
134
180
  const isEfdRetry = tags.includes('_ddIsEfdRetry')
181
+ disableMochaRetries(test)
135
182
  if (isEfdRetry) {
136
183
  wrapOriginalEfdTest(test, slowTestRetries)
137
184
  }
138
185
  for (let retryIndex = 0; retryIndex < numRetries; retryIndex++) {
139
186
  const clonedTest = test.clone()
187
+ disableMochaRetries(clonedTest)
140
188
  suite.addTest(clonedTest)
141
189
  if (isEfdRetry) {
142
190
  clonedTest._ddEfdRetryIndex = retryIndex + 1
@@ -205,6 +253,21 @@ function getTestFullName (test) {
205
253
  return `mocha.${getTestSuitePath(test.file, process.cwd())}.${test.fullTitle()}`
206
254
  }
207
255
 
256
+ /**
257
+ * Records every attempt for a test grouped by its full test name.
258
+ * @param {Record<string, Array<{ file: string, fullTitle: () => string }>>} testsByFullName
259
+ * @param {{ file: string, fullTitle: () => string }} test
260
+ * @returns {void}
261
+ */
262
+ function recordTestAttempt (testsByFullName, test) {
263
+ const testFullName = getTestFullName(test)
264
+ if (testsByFullName[testFullName]) {
265
+ testsByFullName[testFullName].push(test)
266
+ } else {
267
+ testsByFullName[testFullName] = [test]
268
+ }
269
+ }
270
+
208
271
  function getTestStatus (test) {
209
272
  if (test.isPending()) {
210
273
  return 'skip'
@@ -230,14 +293,38 @@ function getTestContext (test) {
230
293
  return testToContext.get(key)
231
294
  }
232
295
 
296
+ /**
297
+ * Copies Test Management metadata from Mocha's original runnable to its native retry clone.
298
+ * @param {{
299
+ * _retriedTest?: {
300
+ * _ddIsDisabled?: boolean,
301
+ * _ddIsQuarantined?: boolean
302
+ * },
303
+ * _ddIsDisabled?: boolean,
304
+ * _ddIsQuarantined?: boolean
305
+ * }} test
306
+ */
307
+ function inheritDatadogPropertiesFromRetriedTest (test) {
308
+ const retriedTest = test._retriedTest
309
+ if (!retriedTest) return
310
+
311
+ if (retriedTest._ddIsDisabled) {
312
+ test._ddIsDisabled = true
313
+ }
314
+ if (retriedTest._ddIsQuarantined) {
315
+ test._ddIsQuarantined = true
316
+ }
317
+
318
+ if (test._ddIsQuarantined && !test._ddIsAttemptToFix) {
319
+ testsQuarantined.add(test)
320
+ }
321
+ }
322
+
233
323
  function runnableWrapper (RunnablePackage, libraryConfig) {
234
324
  shimmer.wrap(RunnablePackage.prototype, 'run', run => function (...args) {
235
325
  if (!testFinishCh.hasSubscribers) {
236
326
  return run.apply(this, args)
237
327
  }
238
- if (libraryConfig?.isFlakyTestRetriesEnabled) {
239
- this.retries(libraryConfig?.flakyTestRetriesCount)
240
- }
241
328
  // The reason why the wrapping logic is here is because we need to cover
242
329
  // `afterEach` and `beforeEach` hooks as well.
243
330
  // It can't be done in `getOnTestHandler` because it's only called for tests.
@@ -245,6 +332,7 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
245
332
  const isAfterEach = this.parent._afterEach.includes(this)
246
333
 
247
334
  const isTestHook = isBeforeEach || isAfterEach
335
+ const test = isTestHook ? this.ctx.currentTest : this
248
336
 
249
337
  // we restore the original user defined function
250
338
  if (wrappedFunctions.has(this.fn)) {
@@ -253,8 +341,20 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
253
341
  wrappedFunctions.delete(this.fn)
254
342
  }
255
343
 
344
+ if (isDatadogManagedRetryTest(test, libraryConfig)) {
345
+ disableMochaRetries(this)
346
+ if (typeof args[0] === 'function') {
347
+ const onRunnableFinished = args[0]
348
+ args[0] = function () {
349
+ disableMochaRetries(test)
350
+ return onRunnableFinished.apply(this, arguments)
351
+ }
352
+ }
353
+ } else if (libraryConfig?.isFlakyTestRetriesEnabled) {
354
+ this.retries(libraryConfig.flakyTestRetriesCount)
355
+ }
356
+
256
357
  if (isTestHook || this.type === 'test') {
257
- const test = isTestHook ? this.ctx.currentTest : this
258
358
  const ctx = getTestContext(test)
259
359
 
260
360
  if (ctx) {
@@ -288,6 +388,8 @@ function getOnTestHandler (isMain) {
288
388
  wrappedFunctions.delete(test.fn)
289
389
  }
290
390
 
391
+ inheritDatadogPropertiesFromRetriedTest(test)
392
+
291
393
  const {
292
394
  file: testSuiteAbsolutePath,
293
395
  title,
@@ -340,12 +442,10 @@ function getOnTestHandler (isMain) {
340
442
  }
341
443
  // We want to store the result of the new tests
342
444
  if (isNew) {
343
- const testFullName = getTestFullName(test)
344
- if (newTests[testFullName]) {
345
- newTests[testFullName].push(test)
346
- } else {
347
- newTests[testFullName] = [test]
348
- }
445
+ recordTestAttempt(newTests, test)
446
+ }
447
+ if (!isAttemptToFix && (isNew || isModified)) {
448
+ recordTestAttempt(efdTests, test)
349
449
  }
350
450
 
351
451
  if (!isAttemptToFix && isDisabled) {
@@ -371,13 +471,16 @@ function getFinalStatus ({
371
471
  hasPassedAnyEfdAttempt,
372
472
  isQuarantined,
373
473
  isDisabled,
474
+ isFinalAttempt,
374
475
  }) {
375
476
  // Note that intermediate executions DO NOT report a final status tag
376
477
 
377
- // Intermediate EFD and ATF executions must not carry a final status, regardless of quarantine/disabled state
478
+ // Intermediate executions must not carry a final status, regardless of quarantine/disabled state
479
+ const isExternalIntermediateExecution = !isEfdRetry && !isAttemptToFix && !isFinalAttempt
378
480
  const isIntermediateExecution =
379
481
  (isEfdRetry && !isLastEfdRetry) ||
380
- (isAttemptToFix && !isLastAttemptToFix)
482
+ (isAttemptToFix && !isLastAttemptToFix) ||
483
+ isExternalIntermediateExecution
381
484
  if (isIntermediateExecution) {
382
485
  return
383
486
  }
@@ -411,8 +514,7 @@ function getTestFinishInfo (test, status, config, error) {
411
514
 
412
515
  const testName = getTestFullName(test)
413
516
  if (
414
- config.isEarlyFlakeDetectionEnabled &&
415
- (test._ddIsNew || test._ddIsModified) &&
517
+ isEarlyFlakeDetectionTest(test, config) &&
416
518
  !test._ddIsEfdRetry &&
417
519
  !efdRetryCountByTestFullName.has(testName)
418
520
  ) {
@@ -434,8 +536,7 @@ function getTestFinishInfo (test, status, config, error) {
434
536
 
435
537
  // Needed for the getFinalStatus call. This is because EFD does NOT tag as
436
538
  // EFD retry the first run of the test. It only tags as retries the clones
437
- const isEfdRetry =
438
- test._ddIsEfdRetry || ((test._ddIsNew || test._ddIsModified) && config.isEarlyFlakeDetectionEnabled)
539
+ const isEfdRetry = isEarlyFlakeDetectionTest(test, config)
439
540
 
440
541
  if (test._ddIsAttemptToFix && isLastAttempt) {
441
542
  if (testStatuses.includes('fail')) {
@@ -463,6 +564,7 @@ function getTestFinishInfo (test, status, config, error) {
463
564
  const isAtrRetry = config.isFlakyTestRetriesEnabled &&
464
565
  !test._ddIsAttemptToFix &&
465
566
  !test._ddIsEfdRetry
567
+ const isFinalAttempt = status !== 'fail' || test._currentRetry >= test._retries
466
568
 
467
569
  const { isFlakyTestRetriesEnabled } = config
468
570
  const { _ddIsAttemptToFix, _ddIsQuarantined, _ddIsDisabled } = test
@@ -480,6 +582,7 @@ function getTestFinishInfo (test, status, config, error) {
480
582
  hasPassedAnyEfdAttempt: testStatuses.includes('pass'),
481
583
  isQuarantined: _ddIsQuarantined,
482
584
  isDisabled: _ddIsDisabled,
585
+ isFinalAttempt,
483
586
  })
484
587
 
485
588
  if (_ddIsAttemptToFix) {
@@ -661,7 +764,14 @@ function getOnTestRetryHandler (config) {
661
764
  config.isFlakyTestRetriesEnabled &&
662
765
  !test._ddIsAttemptToFix &&
663
766
  !test._ddIsEfdRetry
664
- testRetryCh.publish({ isFirstAttempt, err, willBeRetried, test, isAtrRetry, ...ctx.currentStore })
767
+ testRetryCh.publish({
768
+ isFirstAttempt,
769
+ err,
770
+ willBeRetried,
771
+ test,
772
+ isAtrRetry,
773
+ ...ctx.currentStore,
774
+ })
665
775
  }
666
776
  const key = getTestToContextKey(test)
667
777
  testToContext.delete(key)
@@ -796,6 +906,7 @@ module.exports = {
796
906
  testFileToSuiteCtx,
797
907
  getRunTestsWrapper,
798
908
  newTests,
909
+ efdTests,
799
910
  newTestsWithDynamicNames,
800
911
  testsQuarantined,
801
912
  testsAttemptToFix,
@@ -6,7 +6,8 @@ const { channel, addHook, AsyncResource } = require('./helpers/instrument')
6
6
  const multerReadCh = channel('datadog:multer:read:finish')
7
7
 
8
8
  function publishRequestBodyAndNext (req, res, next) {
9
- return shimmer.wrapFunction(next, next => function (...args) {
9
+ // Mirror next's name/arity so wrapCallback skips its per-call identity rewrite.
10
+ return shimmer.wrapCallback(next, original => function next (_error) {
10
11
  if (multerReadCh.hasSubscribers && req) {
11
12
  const abortController = new AbortController()
12
13
  const body = req.body
@@ -16,7 +17,7 @@ function publishRequestBodyAndNext (req, res, next) {
16
17
  if (abortController.signal.aborted) return
17
18
  }
18
19
 
19
- return next.apply(this, args)
20
+ return original.apply(this, arguments)
20
21
  })
21
22
  }
22
23