dd-trace 5.99.1 → 5.100.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 (55) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/package.json +4 -4
  3. package/packages/datadog-instrumentations/src/cucumber.js +69 -5
  4. package/packages/datadog-instrumentations/src/express.js +3 -2
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/hono.js +15 -4
  7. package/packages/datadog-instrumentations/src/jest.js +84 -58
  8. package/packages/datadog-instrumentations/src/mocha/main.js +18 -22
  9. package/packages/datadog-instrumentations/src/mocha/utils.js +114 -96
  10. package/packages/datadog-instrumentations/src/mocha/worker.js +2 -2
  11. package/packages/datadog-instrumentations/src/path-to-regexp.js +44 -0
  12. package/packages/datadog-instrumentations/src/playwright.js +108 -18
  13. package/packages/datadog-instrumentations/src/router.js +53 -33
  14. package/packages/datadog-instrumentations/src/vitest.js +76 -30
  15. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -1
  16. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
  17. package/packages/datadog-plugin-bullmq/src/consumer.js +3 -2
  18. package/packages/datadog-plugin-bullmq/src/producer.js +25 -11
  19. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +32 -9
  20. package/packages/datadog-plugin-cypress/src/support.js +22 -21
  21. package/packages/datadog-plugin-grpc/src/client.js +1 -1
  22. package/packages/datadog-plugin-grpc/src/server.js +1 -1
  23. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -3
  24. package/packages/datadog-plugin-playwright/src/index.js +6 -0
  25. package/packages/datadog-plugin-router/src/index.js +13 -0
  26. package/packages/dd-trace/index.js +4 -3
  27. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  28. package/packages/dd-trace/src/baggage.js +10 -0
  29. package/packages/dd-trace/src/config/generated-config-types.d.ts +17 -41
  30. package/packages/dd-trace/src/config/index.js +6 -5
  31. package/packages/dd-trace/src/config/normalize-service.js +31 -0
  32. package/packages/dd-trace/src/config/supported-configurations.json +15 -32
  33. package/packages/dd-trace/src/debugger/config.js +1 -1
  34. package/packages/dd-trace/src/encode/0.4.js +1 -1
  35. package/packages/dd-trace/src/encode/tags-processors.js +3 -3
  36. package/packages/dd-trace/src/heap_snapshots.js +4 -4
  37. package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +2 -2
  38. package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -8
  39. package/packages/dd-trace/src/opentelemetry/span-helpers.js +170 -0
  40. package/packages/dd-trace/src/opentelemetry/span.js +14 -42
  41. package/packages/dd-trace/src/opentelemetry/tracer.js +11 -36
  42. package/packages/dd-trace/src/opentracing/propagation/text_map.js +31 -10
  43. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +42 -12
  44. package/packages/dd-trace/src/opentracing/span.js +3 -2
  45. package/packages/dd-trace/src/plugins/util/ci.js +119 -32
  46. package/packages/dd-trace/src/plugins/util/test.js +293 -27
  47. package/packages/dd-trace/src/profiling/ssi-heuristics.js +2 -2
  48. package/packages/dd-trace/src/propagation-hash/index.js +1 -1
  49. package/packages/dd-trace/src/proxy.js +3 -3
  50. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  51. package/packages/dd-trace/src/span_processor.js +1 -1
  52. package/packages/dd-trace/src/telemetry/telemetry.js +7 -5
  53. package/packages/dd-trace/src/tracer_metadata.js +1 -1
  54. package/vendor/dist/path-to-regexp/LICENSE +0 -21
  55. package/vendor/dist/path-to-regexp/index.js +0 -1
@@ -85,7 +85,6 @@
85
85
  "node-gyp-build","https://github.com/prebuild/node-gyp-build","['MIT']","['Mathias Buus']"
86
86
  "opentracing","https://github.com/opentracing/opentracing-javascript","['Apache-2.0']","['opentracing']"
87
87
  "oxc-parser","https://github.com/oxc-project/oxc","['MIT']","['Boshen and oxc contributors']"
88
- "path-to-regexp","https://github.com/pillarjs/path-to-regexp","['MIT']","['pillarjs']"
89
88
  "pprof-format","https://github.com/DataDog/pprof-format","['MIT']","['Datadog Inc.']"
90
89
  "protobufjs","https://github.com/protobufjs/protobuf.js","['BSD-3-Clause']","['Daniel Wirtz']"
91
90
  "queue-tick","https://github.com/mafintosh/queue-tick","['MIT']","['Mathias Buus']"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.99.1",
3
+ "version": "5.100.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "lint:fix": "node scripts/check_licenses.js && node scripts/check-no-coverage-artifacts.js && eslint . --concurrency=auto --max-warnings 0 --fix",
23
23
  "lint:inspect": "npx @eslint/config-inspector@latest",
24
24
  "lint:codeowners": "codeowners-audit",
25
- "lint:codeowners:ci": "codeowners-audit --glob='**/*.spec.js'",
25
+ "lint:codeowners:ci": "codeowners-audit --glob='**/*.spec.js' --glob='benchmark/sirun/**'",
26
26
  "release:proposal": "node scripts/release/proposal",
27
27
  "services": "node ./scripts/install_plugin_modules && node packages/dd-trace/test/setup/services",
28
28
  "test": "echo '\nError: The root \"npm test\" command is intentionally disabled.\n\nInstead, run specific test suites:\n - npm run test:trace:core\n - npm run test:appsec\n - etc.\n\nOr run individual test files:\n npx mocha path/to/test.spec.js\n\nSee CONTRIBUTING.md (Testing section) for more details.\n' && exit 1",
@@ -192,12 +192,12 @@
192
192
  "bun": "1.3.13",
193
193
  "codeowners-audit": "^2.9.0",
194
194
  "eslint": "^9.39.2",
195
- "eslint-plugin-cypress": "^6.3.1",
195
+ "eslint-plugin-cypress": "^6.4.0",
196
196
  "eslint-plugin-import": "^2.32.0",
197
197
  "eslint-plugin-jsdoc": "^62.9.0",
198
198
  "eslint-plugin-mocha": "^11.2.0",
199
199
  "eslint-plugin-n": "^17.23.2",
200
- "eslint-plugin-promise": "^7.2.1",
200
+ "eslint-plugin-promise": "^7.3.0",
201
201
  "eslint-plugin-unicorn": "^64.0.0",
202
202
  "express": "^5.1.0",
203
203
  "glob": "^10.4.5",
@@ -12,6 +12,10 @@ const {
12
12
  getTestSuitePath,
13
13
  CUCUMBER_WORKER_TRACE_PAYLOAD_CODE,
14
14
  getIsFaultyEarlyFlakeDetection,
15
+ recordAttemptToFixExecution,
16
+ collectAttemptToFixExecutionsFromTraces,
17
+ logAttemptToFixTestExecution,
18
+ logTestOptimizationSummary,
15
19
  } = require('../../dd-trace/src/plugins/util/test')
16
20
  const satisfies = require('../../../vendor/dist/semifies')
17
21
  const { addHook, channel } = require('./helpers/instrument')
@@ -60,9 +64,12 @@ const atrStatusesByScenarioKey = new Map()
60
64
  const numRetriesByPickleId = new Map()
61
65
  const numAttemptToCtx = new Map()
62
66
  const newTestsByTestFullname = new Map()
67
+ const attemptToFixTestsByTestFullname = new Map()
63
68
  const modifiedTestsByPickleId = new Map()
64
69
  // Pickle IDs for tests that are genuinely new (not in known tests list).
65
70
  const newTestPickleIds = new Set()
71
+ const attemptToFixExecutions = new Map()
72
+ const loggedAttemptToFixTests = new Set()
66
73
 
67
74
  let eventDataCollector = null
68
75
  let pickleByFile = {}
@@ -154,6 +161,16 @@ function getTestStatusFromRetries (testStatuses) {
154
161
  return 'pass'
155
162
  }
156
163
 
164
+ function getTestStatusFromAttemptToFixExecutions (testStatuses) {
165
+ if (testStatuses.every(status => status === 'pass')) {
166
+ return 'pass'
167
+ }
168
+ if (testStatuses.every(status => status === 'skip')) {
169
+ return 'skip'
170
+ }
171
+ return 'fail'
172
+ }
173
+
157
174
  function getErrorFromCucumberResult (cucumberResult) {
158
175
  if (!cucumberResult.message) {
159
176
  return
@@ -247,9 +264,8 @@ function getFinalStatus ({
247
264
  }) {
248
265
  // Note that intermediate executions DO NOT report a final status tag
249
266
 
250
- // If the test is quarantined or disabled, regardless of its actual execution result or active retry features,
251
- // the final status of its last execution should be reported as 'skip'.
252
- if (isQuarantined || isDisabled || status === 'skip') {
267
+ // If the test is quarantined or disabled, its final status is skip unless attempt-to-fix takes precedence.
268
+ if (status === 'skip' || (!isLastAttemptToFix && (isQuarantined || isDisabled))) {
253
269
  return 'skip'
254
270
  }
255
271
 
@@ -282,6 +298,7 @@ function wrapRun (pl, isLatestVersion, version) {
282
298
  let numAttempt = 0
283
299
 
284
300
  const testFileAbsolutePath = this.pickle.uri
301
+ const testSuitePath = getTestSuitePath(testFileAbsolutePath, process.cwd())
285
302
 
286
303
  const testSourceLine = this.gherkinDocument?.feature?.location?.line
287
304
 
@@ -293,6 +310,9 @@ function wrapRun (pl, isLatestVersion, version) {
293
310
  }
294
311
  const ctx = testStartPayload
295
312
  numAttemptToCtx.set(numAttempt, ctx)
313
+ if (isTestManagementTestsEnabled && getTestProperties(testSuitePath, this.pickle.name).attemptToFix) {
314
+ logAttemptToFixTestExecution(testSuitePath, this.pickle.name, loggedAttemptToFixTests)
315
+ }
296
316
  testStartCh.runStores(ctx, () => {})
297
317
  const promises = {}
298
318
  try {
@@ -441,6 +461,16 @@ function wrapRun (pl, isLatestVersion, version) {
441
461
 
442
462
  const error = getErrorFromCucumberResult(result)
443
463
 
464
+ if (isAttemptToFix) {
465
+ recordAttemptToFixExecution(attemptToFixExecutions, {
466
+ testSuite: testSuitePath,
467
+ testName: this.pickle.name,
468
+ status,
469
+ isDisabled,
470
+ isQuarantined,
471
+ })
472
+ }
473
+
444
474
  if (promises.hitBreakpointPromise) {
445
475
  await promises.hitBreakpointPromise
446
476
  }
@@ -661,6 +691,7 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin
661
691
  }
662
692
 
663
693
  atrStatusesByScenarioKey.clear()
694
+ attemptToFixTestsByTestFullname.clear()
664
695
  sessionStartCh.publish({ command, frameworkVersion })
665
696
 
666
697
  if (!errorSkippableRequest && skippedSuites.length) {
@@ -701,6 +732,8 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin
701
732
  isTestManagementTestsEnabled,
702
733
  isParallel,
703
734
  })
735
+ logTestOptimizationSummary({ attemptToFixExecutions })
736
+ loggedAttemptToFixTests.clear()
704
737
  eventDataCollector = null
705
738
  return success
706
739
  }
@@ -811,6 +844,7 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
811
844
  let testStatus = lastTestStatus
812
845
  let shouldBePassedByEFD = false
813
846
  let shouldBePassedByTestManagement = false
847
+ let shouldBeFailedByAttemptToFix = false
814
848
  if ((isNew || isModified) && isEarlyFlakeDetectionEnabled) {
815
849
  /**
816
850
  * If Early Flake Detection (EFD) is enabled the logic is as follows:
@@ -827,7 +861,15 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
827
861
  }
828
862
  }
829
863
 
830
- if (isTestManagementTestsEnabled && (isDisabled || isQuarantined)) {
864
+ if (isAttemptToFix && testStatuses.length === testManagementAttemptToFixRetries + 1) {
865
+ testStatus = getTestStatusFromAttemptToFixExecutions(testStatuses)
866
+ if (testStatus === 'fail') {
867
+ this.success = false
868
+ shouldBeFailedByAttemptToFix = true
869
+ }
870
+ }
871
+
872
+ if (isTestManagementTestsEnabled && !isAttemptToFix && (isDisabled || isQuarantined)) {
831
873
  this.success = true
832
874
  shouldBePassedByTestManagement = true
833
875
  }
@@ -863,10 +905,14 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
863
905
  return shouldBePassedByEFD
864
906
  }
865
907
 
866
- if (isNewerCucumberVersion && isTestManagementTestsEnabled && (isQuarantined || isDisabled)) {
908
+ if (isNewerCucumberVersion && isTestManagementTestsEnabled && !isAttemptToFix && (isQuarantined || isDisabled)) {
867
909
  return shouldBePassedByTestManagement
868
910
  }
869
911
 
912
+ if (isNewerCucumberVersion && isAttemptToFix && shouldBeFailedByAttemptToFix) {
913
+ return false
914
+ }
915
+
870
916
  return runTestCaseResult
871
917
  }
872
918
  }
@@ -882,6 +928,7 @@ function getWrappedParseWorkerMessage (parseWorkerMessageFunction, isNewVersion)
882
928
  if (Array.isArray(message)) {
883
929
  const [messageCode, payload] = message
884
930
  if (messageCode === CUCUMBER_WORKER_TRACE_PAYLOAD_CODE) {
931
+ collectAttemptToFixExecutionsFromTraces(payload, attemptToFixExecutions)
885
932
  workerReportTraceCh.publish(payload)
886
933
  return
887
934
  }
@@ -962,6 +1009,23 @@ function getWrappedParseWorkerMessage (parseWorkerMessageFunction, isNewVersion)
962
1009
  // we only push to `finished` if the retries have finished
963
1010
  finished.push(newTestFinalStatus)
964
1011
  }
1012
+ } else if (
1013
+ isTestManagementTestsEnabled &&
1014
+ getTestProperties(getTestSuitePath(testFileAbsolutePath, process.cwd()), pickle.name).attemptToFix
1015
+ ) {
1016
+ const testFullname = `${pickle.uri}:${pickle.name}`
1017
+ let testStatuses = attemptToFixTestsByTestFullname.get(testFullname)
1018
+ if (testStatuses) {
1019
+ testStatuses.push(status)
1020
+ } else {
1021
+ testStatuses = [status]
1022
+ attemptToFixTestsByTestFullname.set(testFullname, testStatuses)
1023
+ }
1024
+
1025
+ if (status === 'skip' || testStatuses.length === testManagementAttemptToFixRetries + 1) {
1026
+ finished.push(getTestStatusFromAttemptToFixExecutions(testStatuses))
1027
+ attemptToFixTestsByTestFullname.delete(testFullname)
1028
+ }
965
1029
  } else {
966
1030
  // TODO: can we get error message?
967
1031
  const finished = pickleResultByFile[testFileAbsolutePath]
@@ -3,6 +3,7 @@
3
3
  const shimmer = require('../../datadog-shimmer')
4
4
  const { createWrapRouterMethod } = require('./router')
5
5
  const { addHook, channel, tracingChannel } = require('./helpers/instrument')
6
+ const { getCompileToRegexp } = require('./path-to-regexp')
6
7
  const {
7
8
  setRouterMountPath,
8
9
  markAppMounted,
@@ -26,8 +27,6 @@ function wrapHandle (handle) {
26
27
  }
27
28
  }
28
29
 
29
- const wrapRouterMethod = createWrapRouterMethod('express')
30
-
31
30
  const responseJsonChannel = channel('datadog:express:response:json:start')
32
31
 
33
32
  function wrapResponseJson (json) {
@@ -163,6 +162,8 @@ addHook({ name: 'express', versions: ['>=4'], file: 'lib/express.js' }, express
163
162
  // It would otherwise produce spans for router and express, and so duplicating them.
164
163
  // We now fall back to router instrumentation
165
164
  addHook({ name: 'express', versions: ['4'], file: 'lib/express.js' }, express => {
165
+ const wrapRouterMethod = createWrapRouterMethod('express', getCompileToRegexp())
166
+
166
167
  shimmer.wrap(express.Router, 'use', wrapRouterMethod)
167
168
  shimmer.wrap(express.Router, 'route', wrapRouterMethod)
168
169
 
@@ -118,6 +118,7 @@ module.exports = {
118
118
  passport: () => require('../passport'),
119
119
  'passport-http': () => require('../passport-http'),
120
120
  'passport-local': () => require('../passport-local'),
121
+ 'path-to-regexp': () => require('../path-to-regexp'),
121
122
  pg: () => require('../pg'),
122
123
  pino: () => require('../pino'),
123
124
  'pino-pretty': () => require('../pino'),
@@ -14,9 +14,15 @@ const enterChannel = channel('apm:hono:middleware:enter')
14
14
  const exitChannel = channel('apm:hono:middleware:exit')
15
15
  const finishChannel = channel('apm:hono:middleware:finish')
16
16
 
17
+ // `app.request()` and non-node adapters call `app.fetch` without an `incoming`
18
+ // IncomingMessage; the APM `web` helpers depend on one, so the wrappers below
19
+ // skip publishing whenever it is missing.
17
20
  function wrapFetch (fetch) {
18
21
  return function (request, env, executionCtx) {
19
- handleChannel.publish({ req: env.incoming })
22
+ const req = env?.incoming
23
+ if (req) {
24
+ handleChannel.publish({ req })
25
+ }
20
26
  return fetch.apply(this, arguments)
21
27
  }
22
28
  }
@@ -31,8 +37,10 @@ function wrapCompose (compose) {
31
37
 
32
38
  const instrumentedOnError = (...args) => {
33
39
  const [error, context] = args
34
- const req = context.env.incoming
35
- errorChannel.publish({ req, error })
40
+ const req = context.env?.incoming
41
+ if (req) {
42
+ errorChannel.publish({ req, error })
43
+ }
36
44
  return onError(...args)
37
45
  }
38
46
 
@@ -62,7 +70,10 @@ function wrapMiddleware (middleware, route) {
62
70
  middleware,
63
71
  (middleware) =>
64
72
  function (context, next) {
65
- const req = context.env.incoming
73
+ const req = context.env?.incoming
74
+ if (!req) {
75
+ return middleware.apply(this, arguments)
76
+ }
66
77
  routeChannel.publish({ req, route })
67
78
  enterChannel.publish({ req, name, route })
68
79
  if (typeof next === 'function') {
@@ -21,6 +21,9 @@ const {
21
21
  isModifiedTest,
22
22
  DYNAMIC_NAME_RE,
23
23
  collectDynamicNamesFromTraces,
24
+ recordAttemptToFixExecution,
25
+ logAttemptToFixTestExecution,
26
+ logTestOptimizationSummary,
24
27
  } = require('../../dd-trace/src/plugins/util/test')
25
28
  const {
26
29
  SEED_SUFFIX_RE,
@@ -107,6 +110,7 @@ const wrappedWorkerChannels = new WeakMap()
107
110
  // New tests whose names contain likely dynamic data (timestamps, UUIDs, etc.)
108
111
  // Populated in-process for runInBand, and via worker-report:trace for parallel mode.
109
112
  const newTestsWithDynamicNames = new Set()
113
+ const loggedAttemptToFixTests = new Set()
110
114
  const testSuiteMockedFiles = new Map()
111
115
  const testsToBeRetried = new Set()
112
116
  // Per-test: how many EFD retries were determined after the first execution.
@@ -193,64 +197,88 @@ function getTestStats (testStatuses) {
193
197
  }
194
198
 
195
199
  /**
196
- * @param {string[]} efdNames
197
- * @param {string[]} quarantineNames
198
- * @param {number} totalCount
199
- */
200
- /**
201
- * Renders a truncated bullet list from an array of items.
200
+ * Formats the ignored-failure section for the Test Optimization summary.
202
201
  *
203
- * @param {Array<{ text: string, suffix?: string }>} items
202
+ * @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
204
203
  * @returns {string}
205
204
  */
206
- function formatList (items) {
205
+ function formatIgnoredFailuresSummary (ignoredFailures) {
206
+ if (!ignoredFailures) return ''
207
+
208
+ const items = []
209
+
210
+ for (const n of ignoredFailures.efdNames) {
211
+ items.push({ text: n, suffix: 'Early Flake Detection' })
212
+ }
213
+ for (const n of ignoredFailures.quarantineNames) {
214
+ items.push({ text: n, suffix: 'Quarantine' })
215
+ }
216
+
217
+ if (items.length === 0 || ignoredFailures.totalCount <= 0) return ''
218
+
207
219
  const shown = items.slice(0, MAX_IGNORED_TEST_NAMES)
208
220
  const more = items.length - shown.length
209
221
  const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
210
- return shown.map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`).join('\n') + moreSuffix
222
+ const formattedItems = shown
223
+ .map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`)
224
+ .join('\n') + moreSuffix
225
+
226
+ return `${ignoredFailures.totalCount} test failure(s) were ignored. Exit code set to 0.\n\n${formattedItems}`
211
227
  }
212
228
 
213
229
  /**
214
- * Logs a single "Datadog Test Optimization" summary at session end,
215
- * combining all relevant sections (ignored failures, dynamic names).
230
+ * Logs a single "Datadog Test Optimization" summary at session end.
216
231
  *
217
232
  * @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
218
233
  */
219
- function logSessionSummary (ignoredFailures) {
220
- const sections = []
234
+ function logSessionSummary (ignoredFailures, attemptToFixExecutions) {
235
+ logTestOptimizationSummary({
236
+ attemptToFixExecutions,
237
+ extraSections: [formatIgnoredFailuresSummary(ignoredFailures)],
238
+ newTestsWithDynamicNames,
239
+ })
240
+ loggedAttemptToFixTests.clear()
241
+ }
221
242
 
222
- if (ignoredFailures) {
223
- const items = []
224
- for (const n of ignoredFailures.efdNames) {
225
- items.push({ text: n, suffix: 'Early Flake Detection' })
226
- }
227
- for (const n of ignoredFailures.quarantineNames) {
228
- items.push({ text: n, suffix: 'Quarantine' })
229
- }
230
- sections.push(
231
- `${ignoredFailures.totalCount} test failure(s) were ignored. Exit code set to 0.\n\n` +
232
- formatList(items)
233
- )
234
- }
243
+ function getTestStatusFromJestResult (status) {
244
+ if (status === 'failed') return 'fail'
245
+ if (status === 'passed') return 'pass'
246
+ }
235
247
 
236
- if (newTestsWithDynamicNames.size > 0) {
237
- const items = [...newTestsWithDynamicNames].map(name => ({ text: name }))
238
- sections.push(
239
- `${newTestsWithDynamicNames.size} test(s) detected as new but their names contain ` +
240
- 'dynamic data (timestamps, UUIDs, etc.).\n' +
241
- 'Tests with changing names are always treated as new on every run, ' +
242
- 'causing unnecessary Early Flake Detection retries and preventing correct new test detection.\n' +
243
- 'Consider using stable, deterministic test names.\n\n' +
244
- formatList(items)
245
- )
246
- newTestsWithDynamicNames.clear()
248
+ function getAttemptToFixExecutionsFromJestResults (result) {
249
+ const executions = new Map()
250
+ const rootDir = result.globalConfig?.rootDir || process.cwd()
251
+
252
+ for (const { testResults, testFilePath } of result.results.testResults) {
253
+ const testSuite = getTestSuitePath(testFilePath, rootDir)
254
+ const testManagementTestsForSuite = testManagementTests
255
+ ?.jest
256
+ ?.suites
257
+ ?.[testSuite]
258
+ ?.tests
259
+ if (!testManagementTestsForSuite) continue
260
+
261
+ for (const { fullName, status } of testResults) {
262
+ const testName = testSuiteAbsolutePathsWithFastCheck.has(testFilePath)
263
+ ? fullName.replace(SEED_SUFFIX_RE, '')
264
+ : fullName
265
+ const testStatus = getTestStatusFromJestResult(status)
266
+ if (!testStatus) continue
267
+
268
+ const testManagementTest = testManagementTestsForSuite[testName]?.properties
269
+ if (!testManagementTest?.attempt_to_fix) continue
270
+
271
+ recordAttemptToFixExecution(executions, {
272
+ testSuite,
273
+ testName,
274
+ status: testStatus,
275
+ isDisabled: testManagementTest.disabled,
276
+ isQuarantined: testManagementTest.quarantined,
277
+ })
278
+ }
247
279
  }
248
280
 
249
- if (sections.length === 0) return
250
-
251
- const line = '-'.repeat(50)
252
- // eslint-disable-next-line no-console -- Intentional user-facing session summary
253
- console.warn(`\n${line}\nDatadog Test Optimization\n${line}\n${sections.join('\n\n')}\n`)
281
+ return executions
254
282
  }
255
283
 
256
284
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
@@ -556,6 +584,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
556
584
  }
557
585
  testContexts.set(event.test, ctx)
558
586
 
587
+ if (isAttemptToFix) {
588
+ logAttemptToFixTestExecution(this.testSuite, testName, loggedAttemptToFixTests)
589
+ }
590
+
559
591
  testStartCh.runStores(ctx, () => {
560
592
  let p = event.test.parent
561
593
  const hooks = []
@@ -795,7 +827,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
795
827
  // Only suppress on the final execution — not when ATR/EFD/ATF will retry the test.
796
828
  if (!event.test?.[ATR_RETRY_SUPPRESSION_FLAG] && !willBeRetriedByFailedTestReplay) {
797
829
  const quarantineCtx = testContexts.get(event.test)
798
- if (quarantineCtx?.isQuarantined && event.test.errors?.length) {
830
+ if (quarantineCtx?.isQuarantined && !quarantineCtx.isAttemptToFix && event.test.errors?.length) {
799
831
  quarantinedFailingTests.add(`${quarantineCtx.suite} › ${quarantineCtx.name}`)
800
832
  event.test.errors = []
801
833
  }
@@ -858,10 +890,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
858
890
  }
859
891
  if (event.name === 'run_finish') {
860
892
  for (const [test, errors] of atrSuppressedErrors) {
861
- // Do not restore errors for quarantined tests — they should stay suppressed
893
+ // Do not restore errors for non-ATF quarantined tests — they should stay suppressed
862
894
  // so Jest doesn't see the failure (prevents --bail from stopping the run).
863
895
  const ctx = testContexts.get(test)
864
- if (ctx?.isQuarantined) {
896
+ if (ctx?.isQuarantined && !ctx.isAttemptToFix) {
865
897
  const testName = getJestTestName(test, this.getShouldStripSeedFromTestName())
866
898
  quarantinedFailingTests.add(`${ctx.suite} › ${testName}`)
867
899
  } else {
@@ -984,7 +1016,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
984
1016
 
985
1017
  // If the test is quarantined, regardless of its actual execution result,
986
1018
  // the final status of its last execution should be reported as 'skip'.
987
- if (this.isTestManagementTestsEnabled &&
1019
+ if (!attemptToFixResult.isAttemptToFixEnabled &&
1020
+ this.isTestManagementTestsEnabled &&
988
1021
  this.testManagementTestsForThisSuite?.quarantined?.includes(testName)) {
989
1022
  return 'skip'
990
1023
  }
@@ -1315,7 +1348,6 @@ function getCliWrapper (isNewJestVersion) {
1315
1348
  }
1316
1349
 
1317
1350
  let numFailedQuarantinedTests = 0
1318
- let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
1319
1351
  let numSuppressedQuarantinedTests = 0
1320
1352
  if (isTestManagementTestsEnabled) {
1321
1353
  const failedTests = result
@@ -1343,11 +1375,7 @@ function getCliWrapper (isNewJestVersion) {
1343
1375
  ?.tests
1344
1376
  ?.[testName]
1345
1377
  ?.properties
1346
- // This uses `attempt_to_fix` because this is always the main process and it's not formatted in camelCase
1347
- if (testManagementTest?.attempt_to_fix && (testManagementTest?.quarantined || testManagementTest?.disabled)) {
1348
- numFailedQuarantinedOrDisabledAttemptedToFixTests++
1349
- quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
1350
- } else if (testManagementTest?.quarantined) {
1378
+ if (testManagementTest?.quarantined && !testManagementTest?.attempt_to_fix) {
1351
1379
  numFailedQuarantinedTests++
1352
1380
  quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
1353
1381
  }
@@ -1365,13 +1393,11 @@ function getCliWrapper (isNewJestVersion) {
1365
1393
  quarantinedFailingTests.clear()
1366
1394
 
1367
1395
  // If every test that failed was quarantined, we'll consider the suite passed
1368
- // Note that if a test is attempted to fix,
1369
- // it's considered quarantined both if it's disabled and if it's quarantined
1370
- // (it'll run but its status is ignored)
1396
+ // Attempt-to-fix tests ignore quarantine/disabled suppression and keep their framework result.
1371
1397
  // Skip if EFD block already flipped (to avoid logging twice)
1372
1398
  // Only use visible failures (from Jest results) for the flip check.
1373
1399
  // Suppressed quarantine failures are not in numFailedTests.
1374
- const visibleQuarantineFailures = numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1400
+ const visibleQuarantineFailures = numFailedQuarantinedTests
1375
1401
  if (
1376
1402
  !result.results.success &&
1377
1403
  !mustNotFlipSuccess &&
@@ -1406,7 +1432,7 @@ function getCliWrapper (isNewJestVersion) {
1406
1432
  (isEarlyFlakeDetectionEnabled || isTestManagementTestsEnabled)
1407
1433
  ) {
1408
1434
  const visibleIgnoredFailures =
1409
- numEfdFailedTestsToIgnore + numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1435
+ numEfdFailedTestsToIgnore + numFailedQuarantinedTests
1410
1436
  if (
1411
1437
  visibleIgnoredFailures !== 0 &&
1412
1438
  result.results.numFailedTests === visibleIgnoredFailures
@@ -1474,7 +1500,7 @@ function getCliWrapper (isNewJestVersion) {
1474
1500
  })
1475
1501
  }
1476
1502
 
1477
- logSessionSummary(ignoredFailuresSummary)
1503
+ logSessionSummary(ignoredFailuresSummary, getAttemptToFixExecutionsFromJestResults(result))
1478
1504
 
1479
1505
  numSkippedSuites = 0
1480
1506
 
@@ -14,8 +14,8 @@ const {
14
14
  mergeCoverage,
15
15
  resetCoverage,
16
16
  getIsFaultyEarlyFlakeDetection,
17
- collectDynamicNamesFromTraces,
18
- logDynamicNamesWarning,
17
+ collectTestOptimizationSummariesFromTraces,
18
+ logTestOptimizationSummary,
19
19
  } = require('../../../dd-trace/src/plugins/util/test')
20
20
 
21
21
  const {
@@ -34,9 +34,9 @@ const {
34
34
  testsQuarantined,
35
35
  getTestFullName,
36
36
  getRunTestsWrapper,
37
- testsAttemptToFix,
38
- testsStatuses,
39
37
  newTestsWithDynamicNames,
38
+ attemptToFixExecutions,
39
+ loggedAttemptToFixTests,
40
40
  } = require('./utils')
41
41
 
42
42
  require('./common')
@@ -146,26 +146,17 @@ function getOnEndHandler (isParallel) {
146
146
  }
147
147
  }
148
148
 
149
- // We substract the errors of attempt to fix tests (quarantined or disabled) from the total number of failures
150
- // We subtract the errors from quarantined tests from the total number of failures
149
+ // We subtract the errors from quarantined tests from the total number of failures.
150
+ // Attempt-to-fix tests ignore quarantine/disabled suppression and keep their framework result.
151
151
  if (config.isTestManagementTestsEnabled) {
152
152
  let numFailedQuarantinedTests = 0
153
- let numFailedRetriedQuarantinedOrDisabledTests = 0
154
- for (const test of testsAttemptToFix) {
155
- const testName = getTestFullName(test)
156
- const testProperties = getTestProperties(test, config.testManagementTests)
157
- if (isTestFailed(test) && (testProperties.isQuarantined || testProperties.isDisabled)) {
158
- const numFailedTests = testsStatuses.get(testName).filter(status => status === 'fail').length
159
- numFailedRetriedQuarantinedOrDisabledTests += numFailedTests
160
- }
161
- }
162
153
  for (const test of testsQuarantined) {
163
154
  if (isTestFailed(test)) {
164
155
  numFailedQuarantinedTests++
165
156
  }
166
157
  }
167
- this.stats.failures -= numFailedQuarantinedTests + numFailedRetriedQuarantinedOrDisabledTests
168
- this.failures -= numFailedQuarantinedTests + numFailedRetriedQuarantinedOrDisabledTests
158
+ this.stats.failures -= numFailedQuarantinedTests
159
+ this.failures -= numFailedQuarantinedTests
169
160
  }
170
161
 
171
162
  // Recompute status after EFD and quarantine adjustments have reduced failure counts
@@ -211,7 +202,8 @@ function getOnEndHandler (isParallel) {
211
202
  isParallel,
212
203
  })
213
204
 
214
- logDynamicNamesWarning(newTestsWithDynamicNames)
205
+ logTestOptimizationSummary({ attemptToFixExecutions, newTestsWithDynamicNames })
206
+ loggedAttemptToFixTests.clear()
215
207
  }
216
208
  }
217
209
 
@@ -467,9 +459,9 @@ addHook({
467
459
  this.on('retry', getOnTestRetryHandler(config))
468
460
 
469
461
  // If the hook passes, 'hook end' will be emitted. Otherwise, 'fail' will be emitted
470
- this.on('hook end', getOnHookEndHandler())
462
+ this.on('hook end', getOnHookEndHandler(config))
471
463
 
472
- this.on('fail', getOnFailHandler(true))
464
+ this.on('fail', getOnFailHandler(true, config))
473
465
 
474
466
  this.on('pending', getOnPendingHandler())
475
467
 
@@ -557,7 +549,10 @@ function onMessage (message) {
557
549
  if (Array.isArray(message)) {
558
550
  const [messageCode, payload] = message
559
551
  if (messageCode === MOCHA_WORKER_TRACE_PAYLOAD_CODE) {
560
- collectDynamicNamesFromTraces(payload, newTestsWithDynamicNames)
552
+ collectTestOptimizationSummariesFromTraces(payload, {
553
+ newTestsWithDynamicNames,
554
+ attemptToFixExecutions,
555
+ })
561
556
  workerReportTraceCh.publish(payload)
562
557
  }
563
558
  }
@@ -771,7 +766,8 @@ addHook({
771
766
  }
772
767
  }
773
768
  // `testsQuarantined` is filled in the worker process, so we need to use the test results to fill it here too.
774
- if (config.isTestManagementTestsEnabled && getTestProperties(test, config.testManagementTests).isQuarantined) {
769
+ const testProperties = getTestProperties(test, config.testManagementTests)
770
+ if (config.isTestManagementTestsEnabled && testProperties.isQuarantined && !testProperties.isAttemptToFix) {
775
771
  testsQuarantined.add(test)
776
772
  }
777
773
  }