dd-trace 5.92.0 → 5.94.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 (31) hide show
  1. package/package.json +15 -11
  2. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +23 -0
  3. package/packages/datadog-instrumentations/src/jest.js +118 -32
  4. package/packages/datadog-instrumentations/src/mocha/main.js +6 -0
  5. package/packages/datadog-instrumentations/src/mocha/utils.js +89 -5
  6. package/packages/datadog-instrumentations/src/playwright.js +10 -0
  7. package/packages/datadog-instrumentations/src/vitest.js +119 -0
  8. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -0
  9. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +12 -0
  10. package/packages/datadog-plugin-jest/src/index.js +6 -0
  11. package/packages/datadog-plugin-mocha/src/index.js +11 -0
  12. package/packages/datadog-plugin-playwright/src/index.js +9 -0
  13. package/packages/datadog-plugin-vitest/src/index.js +9 -0
  14. package/packages/datadog-webpack/index.js +187 -0
  15. package/packages/datadog-webpack/src/loader.js +27 -0
  16. package/packages/datadog-webpack/src/log.js +32 -0
  17. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +103 -32
  18. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +10 -21
  19. package/packages/dd-trace/src/config/supported-configurations.json +2 -2
  20. package/packages/dd-trace/src/crashtracking/index.js +7 -1
  21. package/packages/dd-trace/src/exporters/common/docker.js +1 -0
  22. package/packages/dd-trace/src/exporters/common/request.js +26 -17
  23. package/packages/dd-trace/src/opentracing/span.js +5 -0
  24. package/packages/dd-trace/src/plugin_manager.js +10 -7
  25. package/packages/dd-trace/src/plugins/util/test.js +76 -0
  26. package/packages/dd-trace/src/priority_sampler.js +6 -3
  27. package/packages/dd-trace/src/profiling/profiler.js +78 -47
  28. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -28
  29. package/packages/dd-trace/src/proxy.js +4 -3
  30. package/packages/dd-trace/src/tracer_metadata.js +10 -1
  31. package/webpack.js +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.92.0",
3
+ "version": "5.94.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -37,6 +37,8 @@
37
37
  "test:trace:guardrails:ci": "nyc -- npm run test:trace:guardrails",
38
38
  "test:esbuild": "mocha packages/datadog-esbuild/test/**/*.spec.js",
39
39
  "test:esbuild:ci": "nyc -- npm run test:esbuild",
40
+ "test:webpack": "mocha packages/datadog-webpack/test/**/*.spec.js",
41
+ "test:webpack:ci": "nyc -- npm run test:webpack",
40
42
  "test:instrumentations": "mocha \"packages/datadog-instrumentations/test/@(${PLUGINS}).spec.js\"",
41
43
  "test:instrumentations:ci": "yarn services && nyc -- npm run test:instrumentations",
42
44
  "test:instrumentations:misc": "mocha packages/datadog-instrumentations/test/*/**/*.spec.js",
@@ -67,6 +69,7 @@
67
69
  "test:integration:cypress": "mocha --timeout 60000 \"integration-tests/cypress/*.spec.js\"",
68
70
  "test:integration:debugger": "mocha --timeout 60000 \"integration-tests/debugger/*.spec.js\"",
69
71
  "test:integration:esbuild": "mocha --timeout 60000 \"integration-tests/esbuild/*.spec.js\"",
72
+ "test:integration:webpack": "mocha --timeout 60000 \"integration-tests/webpack/*.spec.js\"",
70
73
  "test:integration:openfeature": "mocha --timeout 60000 \"integration-tests/openfeature/*.spec.js\"",
71
74
  "test:integration:jest": "mocha --timeout 60000 \"integration-tests/jest/*.spec.js\"",
72
75
  "test:integration:mocha": "mocha --timeout 60000 \"integration-tests/mocha/*.spec.js\"",
@@ -110,6 +113,7 @@
110
113
  "ci/**/*",
111
114
  "cypress/**/*",
112
115
  "esbuild.js",
116
+ "webpack.js",
113
117
  "ext/**/*",
114
118
  "index.d.ts",
115
119
  "index.js",
@@ -138,21 +142,21 @@
138
142
  "import-in-the-middle": "^3.0.0"
139
143
  },
140
144
  "optionalDependencies": {
141
- "@datadog/libdatadog": "0.8.1",
145
+ "@datadog/libdatadog": "0.9.2",
142
146
  "@datadog/native-appsec": "11.0.1",
143
147
  "@datadog/native-iast-taint-tracking": "4.1.0",
144
148
  "@datadog/native-metrics": "3.1.1",
145
- "@datadog/openfeature-node-server": "^1.1.0",
146
- "@datadog/pprof": "5.14.0",
149
+ "@datadog/openfeature-node-server": "^1.1.1",
150
+ "@datadog/pprof": "5.14.1",
147
151
  "@datadog/wasm-js-rewriter": "5.0.1",
148
152
  "@opentelemetry/api": ">=1.0.0 <1.10.0",
149
153
  "@opentelemetry/api-logs": "<1.0.0",
150
- "oxc-parser": "^0.118.0"
154
+ "oxc-parser": "^0.121.0"
151
155
  },
152
156
  "devDependencies": {
153
157
  "@actions/core": "^3.0.0",
154
158
  "@actions/github": "^9.0.0",
155
- "@babel/helpers": "^7.28.6",
159
+ "@babel/helpers": "^7.29.2",
156
160
  "@eslint/eslintrc": "^3.3.5",
157
161
  "@eslint/js": "^9.39.2",
158
162
  "@msgpack/msgpack": "^3.1.3",
@@ -165,12 +169,12 @@
165
169
  "axios": "^1.13.4",
166
170
  "benchmark": "^2.1.4",
167
171
  "body-parser": "^2.2.2",
168
- "bun": "1.3.10",
172
+ "bun": "1.3.11",
169
173
  "codeowners-audit": "^2.9.0",
170
174
  "eslint": "^9.39.2",
171
- "eslint-plugin-cypress": "^6.2.0",
175
+ "eslint-plugin-cypress": "^6.2.1",
172
176
  "eslint-plugin-import": "^2.32.0",
173
- "eslint-plugin-jsdoc": "^62.8.0",
177
+ "eslint-plugin-jsdoc": "^62.8.1",
174
178
  "eslint-plugin-mocha": "^11.2.0",
175
179
  "eslint-plugin-n": "^17.23.2",
176
180
  "eslint-plugin-promise": "^7.2.1",
@@ -194,11 +198,11 @@
194
198
  "retry": "^0.13.1",
195
199
  "semifies": "^1.0.0",
196
200
  "semver": "^7.7.2",
197
- "sinon": "^21.0.2",
201
+ "sinon": "^21.0.3",
198
202
  "tiktoken": "^1.0.21",
199
203
  "typescript": "^5.9.2",
200
204
  "workerpool": "^10.0.0",
201
- "yaml": "^2.8.0",
205
+ "yaml": "^2.8.3",
202
206
  "yarn-deduplicate": "^6.0.2"
203
207
  }
204
208
  }
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const Module = require('module')
3
4
  const dc = require('dc-polyfill')
4
5
 
5
6
  const log = require('../../../dd-trace/src/log')
@@ -11,6 +12,28 @@ const {
11
12
  const hooks = require('./hooks')
12
13
  const instrumentations = require('./instrumentations')
13
14
 
15
+ // register.js has now set up ritm (require-in-the-middle). In bundled
16
+ // environments (webpack, esbuild), Node.js built-in modules required by
17
+ // dd-trace internal modules (e.g. http from request.js) may have been loaded
18
+ // before ritm was active. The bundler's module cache then returns those
19
+ // pre-loaded modules for any subsequent require() calls, bypassing ritm.
20
+ // Re-requiring them via the real Module.prototype.require ensures ritm applies
21
+ // their instrumentation hooks.
22
+ //
23
+ // In regular Node.js, `module` is an instance of Module. In bundlers, the
24
+ // module wrapper object is a plain object (not a Module instance), so we use
25
+ // that to detect a bundled context and avoid unintended side-effects in
26
+ // normal Node.js (e.g. shimmer-wrapping http before ESM modules load).
27
+ if (!(module instanceof Module)) {
28
+ for (const name of ['http', 'https']) {
29
+ try {
30
+ Module.prototype.require.call(module, name)
31
+ } catch {
32
+ // Built-in not available in this environment, skip
33
+ }
34
+ }
35
+ }
36
+
14
37
  const CHANNEL = 'dd-trace:bundler:load'
15
38
 
16
39
  if (!dc.subscribe) {
@@ -15,6 +15,8 @@ const {
15
15
  JEST_WORKER_LOGS_PAYLOAD_CODE,
16
16
  getTestEndLine,
17
17
  isModifiedTest,
18
+ DYNAMIC_NAME_RE,
19
+ collectDynamicNamesFromTraces,
18
20
  } = require('../../dd-trace/src/plugins/util/test')
19
21
  const {
20
22
  SEED_SUFFIX_RE,
@@ -97,7 +99,10 @@ const originalHookFns = new WeakMap()
97
99
  const retriedTestsToNumAttempts = new Map()
98
100
  const newTestsTestStatuses = new Map()
99
101
  const attemptToFixRetriedTestsStatuses = new Map()
100
- const wrappedWorkers = new WeakSet()
102
+ const wrappedWorkerChannels = new WeakMap()
103
+ // New tests whose names contain likely dynamic data (timestamps, UUIDs, etc.)
104
+ // Populated in-process for runInBand, and via worker-report:trace for parallel mode.
105
+ const newTestsWithDynamicNames = new Set()
101
106
  const testSuiteMockedFiles = new Map()
102
107
  const testsToBeRetried = new Set()
103
108
  // Per-test: how many EFD retries were determined after the first execution.
@@ -154,25 +159,60 @@ function getTestStats (testStatuses) {
154
159
  * @param {string[]} quarantineNames
155
160
  * @param {number} totalCount
156
161
  */
157
- function logIgnoredFailuresSummary (efdNames, quarantineNames, totalCount) {
158
- const names = []
159
- for (const n of efdNames) {
160
- names.push({ name: n, reason: 'Early Flake Detection' })
162
+ /**
163
+ * Renders a truncated bullet list from an array of items.
164
+ *
165
+ * @param {Array<{ text: string, suffix?: string }>} items
166
+ * @returns {string}
167
+ */
168
+ function formatList (items) {
169
+ const shown = items.slice(0, MAX_IGNORED_TEST_NAMES)
170
+ const more = items.length - shown.length
171
+ const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
172
+ return shown.map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`).join('\n') + moreSuffix
173
+ }
174
+
175
+ /**
176
+ * Logs a single "Datadog Test Optimization" summary at session end,
177
+ * combining all relevant sections (ignored failures, dynamic names).
178
+ *
179
+ * @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
180
+ */
181
+ function logSessionSummary (ignoredFailures) {
182
+ const sections = []
183
+
184
+ if (ignoredFailures) {
185
+ const items = []
186
+ for (const n of ignoredFailures.efdNames) {
187
+ items.push({ text: n, suffix: 'Early Flake Detection' })
188
+ }
189
+ for (const n of ignoredFailures.quarantineNames) {
190
+ items.push({ text: n, suffix: 'Quarantine' })
191
+ }
192
+ sections.push(
193
+ `${ignoredFailures.totalCount} test failure(s) were ignored. Exit code set to 0.\n\n` +
194
+ formatList(items)
195
+ )
161
196
  }
162
- for (const n of quarantineNames) {
163
- names.push({ name: n, reason: 'Quarantine' })
197
+
198
+ if (newTestsWithDynamicNames.size > 0) {
199
+ const items = [...newTestsWithDynamicNames].map(name => ({ text: name }))
200
+ sections.push(
201
+ `${newTestsWithDynamicNames.size} test(s) detected as new but their names contain ` +
202
+ 'dynamic data (timestamps, UUIDs, etc.).\n' +
203
+ 'Tests with changing names are always treated as new on every run, ' +
204
+ 'causing unnecessary Early Flake Detection retries and preventing correct new test detection.\n' +
205
+ 'Consider using stable, deterministic test names.\n\n' +
206
+ formatList(items)
207
+ )
208
+ newTestsWithDynamicNames.clear()
164
209
  }
165
- const shown = names.slice(0, MAX_IGNORED_TEST_NAMES)
166
- const more = names.length - shown.length
167
- const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
168
- const list = shown.map(({ name, reason }) => ` • ${name} (${reason})`).join('\n')
210
+
211
+ if (sections.length === 0) return
212
+
169
213
  const line = '-'.repeat(50)
170
- // eslint-disable-next-line no-console -- Intentional user-facing message when exit code is flipped
171
- console.warn(
172
- `\n${line}\nDatadog Test Optimization\n${line}\n` +
173
- `${totalCount} test failure(s) were ignored. Exit code set to 0.\n\n` +
174
- `${list}${moreSuffix}\n`
175
- )
214
+ // eslint-disable-next-line no-console -- Intentional user-facing session summary
215
+ console.warn(`\n${line}\nDatadog Test Optimization\n${line}\n${sections.join('\n\n')}\n`)
176
216
  }
177
217
 
178
218
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
@@ -457,6 +497,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
457
497
  }
458
498
 
459
499
  const isJestRetry = event.test?.invocations > 1
500
+ const hasDynamicName = isNewTest && DYNAMIC_NAME_RE.test(testName)
460
501
  const ctx = {
461
502
  name: testName,
462
503
  suite: this.testSuite,
@@ -472,6 +513,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
472
513
  isDisabled,
473
514
  isQuarantined,
474
515
  isModified,
516
+ hasDynamicName,
475
517
  testSuiteAbsolutePath: this.testSuiteAbsolutePath,
476
518
  }
477
519
  testContexts.set(event.test, ctx)
@@ -564,6 +606,11 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
564
606
  if (!isAttemptToFix && this.isKnownTestsEnabled) {
565
607
  const isNew = !this.knownTestsForThisSuite.includes(testFullName)
566
608
  if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testFullName)) {
609
+ if (DYNAMIC_NAME_RE.test(testFullName)) {
610
+ // Populated directly for runInBand; for parallel workers the main process
611
+ // collects these from the TEST_HAS_DYNAMIC_NAME span tag via worker-report:trace.
612
+ newTestsWithDynamicNames.add(`${this.testSuite} › ${testFullName}`)
613
+ }
567
614
  retriedTestsToNumAttempts.set(testFullName, 0)
568
615
  if (this.isEarlyFlakeDetectionEnabled) {
569
616
  testsToBeRetried.add(testFullName)
@@ -722,7 +769,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
722
769
  error: formatJestError(originalError),
723
770
  shouldSetProbe,
724
771
  promises,
725
- finalStatus,
726
772
  })
727
773
  }
728
774
 
@@ -1333,13 +1379,7 @@ function getCliWrapper (isNewJestVersion) {
1333
1379
  })
1334
1380
  }
1335
1381
 
1336
- if (ignoredFailuresSummary) {
1337
- logIgnoredFailuresSummary(
1338
- ignoredFailuresSummary.efdNames,
1339
- ignoredFailuresSummary.quarantineNames,
1340
- ignoredFailuresSummary.totalCount
1341
- )
1342
- }
1382
+ logSessionSummary(ignoredFailuresSummary)
1343
1383
 
1344
1384
  numSkippedSuites = 0
1345
1385
 
@@ -1793,8 +1833,14 @@ addHook({
1793
1833
 
1794
1834
  function onMessageWrapper (onMessage) {
1795
1835
  return function () {
1796
- const [code, data] = arguments[0]
1836
+ const response = arguments[0]
1837
+ if (!Array.isArray(response)) {
1838
+ return onMessage.apply(this, arguments)
1839
+ }
1840
+
1841
+ const [code, data] = response
1797
1842
  if (code === JEST_WORKER_TRACE_PAYLOAD_CODE) { // datadog trace payload
1843
+ collectDynamicNamesFromTraces(data, newTestsWithDynamicNames)
1798
1844
  workerReportTraceCh.publish(data)
1799
1845
  return
1800
1846
  }
@@ -1855,15 +1901,40 @@ function sendWrapper (send) {
1855
1901
  }
1856
1902
  }
1857
1903
 
1904
+ function wrapWorkerChannel (worker) {
1905
+ const workerChannel = worker._child || worker._worker
1906
+ if (!workerChannel) return
1907
+
1908
+ shimmer.wrap(workerChannel, worker._child ? 'send' : 'postMessage', sendWrapper)
1909
+ }
1910
+
1911
+ function wrapWorker (worker) {
1912
+ // ChildProcessWorker uses _child (child_process), ExperimentalWorker uses _worker (worker_threads)
1913
+ const workerChannel = worker._child || worker._worker
1914
+ if (!workerChannel) return
1915
+
1916
+ wrapWorkerChannel(worker)
1917
+ shimmer.wrap(worker, '_onMessage', onMessageWrapper)
1918
+ workerChannel.removeAllListeners('message')
1919
+ workerChannel.on('message', worker._onMessage.bind(worker))
1920
+ }
1921
+
1858
1922
  function enqueueWrapper (enqueue) {
1859
1923
  return function () {
1860
1924
  shimmer.wrap(arguments[0], 'onStart', onStart => function (worker) {
1861
- if (worker && !wrappedWorkers.has(worker)) {
1862
- shimmer.wrap(worker._child, 'send', sendWrapper)
1863
- shimmer.wrap(worker, '_onMessage', onMessageWrapper)
1864
- worker._child.removeAllListeners('message')
1865
- worker._child.on('message', worker._onMessage.bind(worker))
1866
- wrappedWorkers.add(worker)
1925
+ if (worker) {
1926
+ const currentChannel = worker._child || worker._worker
1927
+ const previousChannel = wrappedWorkerChannels.get(worker)
1928
+ if (currentChannel !== previousChannel) {
1929
+ if (previousChannel) {
1930
+ // Worker restarted — only re-wrap the new child's send/postMessage
1931
+ wrapWorkerChannel(worker)
1932
+ } else {
1933
+ // First time seeing this worker — full setup
1934
+ wrapWorker(worker)
1935
+ }
1936
+ wrappedWorkerChannels.set(worker, currentChannel)
1937
+ }
1867
1938
  }
1868
1939
  return onStart.apply(this, arguments)
1869
1940
  })
@@ -1892,6 +1963,21 @@ addHook({
1892
1963
  return childProcessWorker
1893
1964
  })
1894
1965
 
1966
+ addHook({
1967
+ name: 'jest-worker',
1968
+ versions: ['>=24.9.0 <30.0.0'],
1969
+ file: 'build/workers/NodeThreadsWorker.js',
1970
+ }, (nodeThreadsWorker) => {
1971
+ const ExperimentalWorker = nodeThreadsWorker.default
1972
+ shimmer.wrap(ExperimentalWorker.prototype, 'send', sendWrapper)
1973
+ if (ExperimentalWorker.prototype._onMessage) {
1974
+ shimmer.wrap(ExperimentalWorker.prototype, '_onMessage', onMessageWrapper)
1975
+ } else if (ExperimentalWorker.prototype.onMessage) {
1976
+ shimmer.wrap(ExperimentalWorker.prototype, 'onMessage', onMessageWrapper)
1977
+ }
1978
+ return nodeThreadsWorker
1979
+ })
1980
+
1895
1981
  addHook({
1896
1982
  name: 'jest-worker',
1897
1983
  versions: ['>=30.0.0'],
@@ -14,6 +14,8 @@ const {
14
14
  mergeCoverage,
15
15
  resetCoverage,
16
16
  getIsFaultyEarlyFlakeDetection,
17
+ collectDynamicNamesFromTraces,
18
+ logDynamicNamesWarning,
17
19
  } = require('../../../dd-trace/src/plugins/util/test')
18
20
 
19
21
  const {
@@ -34,6 +36,7 @@ const {
34
36
  getRunTestsWrapper,
35
37
  testsAttemptToFix,
36
38
  testsStatuses,
39
+ newTestsWithDynamicNames,
37
40
  } = require('./utils')
38
41
 
39
42
  require('./common')
@@ -207,6 +210,8 @@ function getOnEndHandler (isParallel) {
207
210
  isTestManagementEnabled: config.isTestManagementTestsEnabled,
208
211
  isParallel,
209
212
  })
213
+
214
+ logDynamicNamesWarning(newTestsWithDynamicNames)
210
215
  }
211
216
  }
212
217
 
@@ -552,6 +557,7 @@ function onMessage (message) {
552
557
  if (Array.isArray(message)) {
553
558
  const [messageCode, payload] = message
554
559
  if (messageCode === MOCHA_WORKER_TRACE_PAYLOAD_CODE) {
560
+ collectDynamicNamesFromTraces(payload, newTestsWithDynamicNames)
555
561
  workerReportTraceCh.publish(payload)
556
562
  }
557
563
  }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { getTestSuitePath } = require('../../../dd-trace/src/plugins/util/test')
3
+ const { getTestSuitePath, DYNAMIC_NAME_RE } = require('../../../dd-trace/src/plugins/util/test')
4
4
  const { channel } = require('../helpers/instrument')
5
5
  const shimmer = require('../../../datadog-shimmer')
6
6
 
@@ -23,6 +23,7 @@ const testToStartLine = new WeakMap()
23
23
  const testFileToSuiteCtx = new Map()
24
24
  const wrappedFunctions = new WeakSet()
25
25
  const newTests = {}
26
+ const newTestsWithDynamicNames = new Set()
26
27
  const testsAttemptToFix = new Set()
27
28
  const testsQuarantined = new Set()
28
29
  const testsStatuses = new Map()
@@ -221,6 +222,10 @@ function getOnTestHandler (isMain) {
221
222
  testInfo.isDisabled = isDisabled
222
223
  testInfo.isQuarantined = isQuarantined
223
224
  testInfo.isModified = isModified
225
+ testInfo.hasDynamicName = isNew && DYNAMIC_NAME_RE.test(test.fullTitle())
226
+ if (testInfo.hasDynamicName) {
227
+ newTestsWithDynamicNames.add(`${getTestSuitePath(test.file, process.cwd())} › ${test.fullTitle()}`)
228
+ }
224
229
  // We want to store the result of the new tests
225
230
  if (isNew) {
226
231
  const testFullName = getTestFullName(test)
@@ -241,6 +246,44 @@ function getOnTestHandler (isMain) {
241
246
  }
242
247
  }
243
248
 
249
+ function getFinalStatus ({
250
+ status,
251
+ hasFailedAllRetries,
252
+ isFlakyTestRetriesEnabled,
253
+ isLastAtrAttempt,
254
+ isEfdRetry,
255
+ isLastEfdRetry,
256
+ isAttemptToFix,
257
+ isLastAttemptToFix,
258
+ attemptToFixPassed,
259
+ isQuarantined,
260
+ isDisabled,
261
+ }) {
262
+ // Note that intermediate executions DO NOT report a final status tag
263
+
264
+ // If the test is quarantined or disabled, regardless of its actual execution result or active retry features,
265
+ // the final status of its last execution should be reported as 'skip'.
266
+ if (isQuarantined || isDisabled) {
267
+ return 'skip'
268
+ }
269
+
270
+ const isAtrActive = isFlakyTestRetriesEnabled && !isAttemptToFix && !isEfdRetry
271
+
272
+ // When no retry feature is active, every execution is final
273
+ if (!isAtrActive && !isEfdRetry && !isAttemptToFix) {
274
+ return status
275
+ }
276
+ if (isAtrActive && isLastAtrAttempt) {
277
+ return hasFailedAllRetries ? 'fail' : 'pass'
278
+ }
279
+ if (isEfdRetry && isLastEfdRetry) {
280
+ return hasFailedAllRetries ? 'fail' : 'pass'
281
+ }
282
+ if (isAttemptToFix && isLastAttemptToFix) {
283
+ return attemptToFixPassed ? 'pass' : 'fail'
284
+ }
285
+ }
286
+
244
287
  function getOnTestEndHandler (config) {
245
288
  return async function (test) {
246
289
  const ctx = getTestContext(test)
@@ -271,6 +314,11 @@ function getOnTestEndHandler (config) {
271
314
 
272
315
  const isLastAttempt = testStatuses.length === config.testManagementAttemptToFixRetries + 1
273
316
  const isLastEfdRetry = testStatuses.length === config.earlyFlakeDetectionNumRetries + 1
317
+ const isLastAtrAttempt = getIsLastRetry(test) || (config.isFlakyTestRetriesEnabled && status === 'pass')
318
+
319
+ // Needed for the getFinalStatus call. This is because EFD does NOT tag as
320
+ // EFD retry the first run of the test. It only tags as retries the clones
321
+ const isEfdRetry = test._ddIsEfdRetry || (test._ddIsNew && config.isEarlyFlakeDetectionEnabled)
274
322
 
275
323
  if (test._ddIsAttemptToFix && isLastAttempt) {
276
324
  if (testStatuses.includes('fail')) {
@@ -299,8 +347,29 @@ function getOnTestEndHandler (config) {
299
347
  !test._ddIsAttemptToFix &&
300
348
  !test._ddIsEfdRetry
301
349
 
302
- // if there are afterEach to be run, we don't finish the test yet
303
- if (ctx && !getAfterEachHooks(test).length) {
350
+ const { isFlakyTestRetriesEnabled } = config
351
+ const { _ddIsAttemptToFix, _ddIsQuarantined, _ddIsDisabled } = test
352
+
353
+ const finalStatus = getFinalStatus({
354
+ status,
355
+ hasFailedAllRetries,
356
+ isFlakyTestRetriesEnabled,
357
+ isLastAtrAttempt,
358
+ isEfdRetry,
359
+ isLastEfdRetry,
360
+ isAttemptToFix: _ddIsAttemptToFix,
361
+ isLastAttemptToFix: isLastAttempt,
362
+ attemptToFixPassed,
363
+ isQuarantined: _ddIsQuarantined,
364
+ isDisabled: _ddIsDisabled,
365
+ })
366
+
367
+ // If there are afterEach to be run, we don't finish the test yet.
368
+ // Disabled tests (marked pending by us) are finished immediately without waiting for afterEach hooks.
369
+ // In older mocha versions, pending tests don't run afterEach hooks, so we can't rely on
370
+ // getOnHookEndHandler to finish the test. This mirrors Jest's approach where the skip handler
371
+ // directly sets finalStatus without waiting for hooks
372
+ if (ctx && (!getAfterEachHooks(test).length || test._ddIsDisabled)) {
304
373
  testFinishCh.publish({
305
374
  status,
306
375
  hasBeenRetried: isMochaRetry(test),
@@ -311,7 +380,10 @@ function getOnTestEndHandler (config) {
311
380
  isAttemptToFixRetry,
312
381
  isAtrRetry,
313
382
  ...ctx.currentStore,
383
+ finalStatus,
314
384
  })
385
+ } else if (ctx) { // if there is an afterEach to run, let's store the finalStatus for getOnHookEndHandler
386
+ ctx.finalStatus = finalStatus
315
387
  }
316
388
  }
317
389
  }
@@ -325,12 +397,15 @@ function getOnHookEndHandler () {
325
397
  if (isLastAfterEach) {
326
398
  const status = getTestStatus(test)
327
399
  const ctx = getTestContext(test)
328
- if (ctx) {
400
+ // Disabled tests are already finished in getOnTestEndHandler,
401
+ // skip to avoid double-publishing
402
+ if (ctx && !test._ddIsDisabled) {
329
403
  testFinishCh.publish({
330
404
  status,
331
405
  hasBeenRetried: isMochaRetry(test),
332
406
  isLastRetry: getIsLastRetry(test),
333
407
  ...ctx.currentStore,
408
+ finalStatus: ctx.finalStatus,
334
409
  })
335
410
  }
336
411
  }
@@ -356,7 +431,15 @@ function getOnFailHandler (isMain) {
356
431
  testContext.err = err
357
432
  errorCh.runStores(testContext, () => {})
358
433
  // if it's a hook and it has failed, 'test end' will not be called
359
- testFinishCh.publish({ status: 'fail', hasBeenRetried: isMochaRetry(test), ...testContext.currentStore })
434
+ // quarantined and disabled tests always report 'skip'
435
+ // as final status, even when hooks fail
436
+ const isSkippedByManagement = test._ddIsQuarantined || test._ddIsDisabled
437
+ testFinishCh.publish({
438
+ status: 'fail',
439
+ hasBeenRetried: isMochaRetry(test),
440
+ ...testContext.currentStore,
441
+ finalStatus: isSkippedByManagement ? 'skip' : 'fail',
442
+ })
360
443
  } else {
361
444
  testContext.err = err
362
445
  errorCh.runStores(testContext, () => {})
@@ -519,6 +602,7 @@ module.exports = {
519
602
  testFileToSuiteCtx,
520
603
  getRunTestsWrapper,
521
604
  newTests,
605
+ newTestsWithDynamicNames,
522
606
  testsQuarantined,
523
607
  testsAttemptToFix,
524
608
  testsStatuses,
@@ -8,6 +8,8 @@ const {
8
8
  getTestSuitePath,
9
9
  PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE,
10
10
  getIsFaultyEarlyFlakeDetection,
11
+ DYNAMIC_NAME_RE,
12
+ logDynamicNamesWarning,
11
13
  } = require('../../dd-trace/src/plugins/util/test')
12
14
  const log = require('../../dd-trace/src/log')
13
15
  const {
@@ -70,6 +72,7 @@ let isImpactedTestsEnabled = false
70
72
  let modifiedFiles = {}
71
73
  const quarantinedOrDisabledTestsAttemptToFix = []
72
74
  let quarantinedButNotAttemptToFixFqns = new Set()
75
+ const newTestsWithDynamicNames = new Set()
73
76
  let rootDir = ''
74
77
  let sessionProjects = []
75
78
 
@@ -381,6 +384,9 @@ function testEndHandler ({
381
384
 
382
385
  if (testStatuses.length === 0) {
383
386
  testsToTestStatuses.set(testFqn, [testStatus])
387
+ if (test._ddIsNew && DYNAMIC_NAME_RE.test(getTestFullname(test))) {
388
+ newTestsWithDynamicNames.add(`${getTestSuitePath(test._requireFile, rootDir)} › ${getTestFullname(test)}`)
389
+ }
384
390
  } else {
385
391
  testStatuses.push(testStatus)
386
392
  }
@@ -432,6 +438,7 @@ function testEndHandler ({
432
438
  error,
433
439
  extraTags: annotationTags,
434
440
  isNew: test._ddIsNew,
441
+ hasDynamicName: test._ddIsNew && DYNAMIC_NAME_RE.test(getTestFullname(test)),
435
442
  isAttemptToFix: test._ddIsAttemptToFix,
436
443
  isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
437
444
  isQuarantined: test._ddIsQuarantined,
@@ -784,6 +791,8 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
784
791
  }
785
792
  }
786
793
 
794
+ logDynamicNamesWarning(newTestsWithDynamicNames)
795
+
787
796
  const flushWait = new Promise(resolve => {
788
797
  onDone = resolve
789
798
  })
@@ -1281,6 +1290,7 @@ addHook({
1281
1290
  error,
1282
1291
  extraTags: annotationTags,
1283
1292
  isNew: test._ddIsNew,
1293
+ hasDynamicName: test._ddIsNew && DYNAMIC_NAME_RE.test(getTestFullname(test)),
1284
1294
  isRetry: retry > 0,
1285
1295
  isEfdRetry: test._ddIsEfdRetry,
1286
1296
  isAttemptToFix: test._ddIsAttemptToFix,