dd-trace 5.85.0 → 5.87.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 (71) hide show
  1. package/index.d.ts +38 -4
  2. package/package.json +1 -1
  3. package/packages/datadog-core/src/storage.js +30 -12
  4. package/packages/datadog-instrumentations/src/cucumber.js +14 -0
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/http/client.js +119 -1
  7. package/packages/datadog-instrumentations/src/jest.js +135 -10
  8. package/packages/datadog-instrumentations/src/mocha/main.js +9 -0
  9. package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
  10. package/packages/datadog-instrumentations/src/mysql2.js +131 -64
  11. package/packages/datadog-instrumentations/src/playwright.js +8 -0
  12. package/packages/datadog-instrumentations/src/prisma.js +225 -30
  13. package/packages/datadog-instrumentations/src/stripe.js +92 -0
  14. package/packages/datadog-instrumentations/src/vitest.js +11 -0
  15. package/packages/datadog-instrumentations/src/ws.js +22 -0
  16. package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
  17. package/packages/datadog-plugin-cucumber/src/index.js +4 -10
  18. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +12 -1
  19. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -4
  20. package/packages/datadog-plugin-http/src/server.js +23 -8
  21. package/packages/datadog-plugin-jest/src/index.js +29 -10
  22. package/packages/datadog-plugin-jest/src/util.js +7 -1
  23. package/packages/datadog-plugin-mocha/src/index.js +5 -17
  24. package/packages/datadog-plugin-playwright/src/index.js +3 -0
  25. package/packages/datadog-plugin-prisma/src/datadog-tracing-helper.js +37 -14
  26. package/packages/datadog-plugin-prisma/src/index.js +8 -5
  27. package/packages/datadog-plugin-router/src/index.js +28 -19
  28. package/packages/datadog-plugin-vitest/src/index.js +6 -10
  29. package/packages/datadog-plugin-ws/src/server.js +8 -0
  30. package/packages/dd-trace/src/appsec/addresses.js +11 -0
  31. package/packages/dd-trace/src/appsec/channels.js +5 -1
  32. package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
  33. package/packages/dd-trace/src/appsec/iast/path-line.js +1 -0
  34. package/packages/dd-trace/src/appsec/index.js +103 -0
  35. package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
  36. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
  37. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  38. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +19 -0
  39. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +15 -0
  40. package/packages/dd-trace/src/ci-visibility/telemetry.js +36 -0
  41. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +44 -1
  42. package/packages/dd-trace/src/config/defaults.js +2 -0
  43. package/packages/dd-trace/src/config/index.js +6 -0
  44. package/packages/dd-trace/src/config/supported-configurations.json +2 -0
  45. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
  46. package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
  47. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
  48. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
  49. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
  50. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
  51. package/packages/dd-trace/src/exporters/common/agents.js +1 -1
  52. package/packages/dd-trace/src/exporters/common/request.js +35 -35
  53. package/packages/dd-trace/src/id.js +1 -1
  54. package/packages/dd-trace/src/lambda/context.js +27 -0
  55. package/packages/dd-trace/src/lambda/handler.js +5 -18
  56. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  57. package/packages/dd-trace/src/llmobs/sdk.js +34 -5
  58. package/packages/dd-trace/src/log/writer.js +1 -5
  59. package/packages/dd-trace/src/plugins/ci_plugin.js +63 -1
  60. package/packages/dd-trace/src/plugins/database.js +42 -43
  61. package/packages/dd-trace/src/plugins/outbound.js +27 -2
  62. package/packages/dd-trace/src/plugins/tracing.js +39 -4
  63. package/packages/dd-trace/src/plugins/util/git.js +27 -30
  64. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
  65. package/packages/dd-trace/src/plugins/util/test.js +3 -1
  66. package/packages/dd-trace/src/plugins/util/web.js +9 -7
  67. package/packages/dd-trace/src/profiling/config.js +6 -14
  68. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -24
  69. package/packages/dd-trace/src/profiling/profiler.js +2 -0
  70. package/packages/dd-trace/src/startup-log.js +3 -2
  71. package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
package/index.d.ts CHANGED
@@ -295,7 +295,18 @@ interface Plugins {
295
295
  }
296
296
 
297
297
  declare namespace tracer {
298
- export type SpanOptions = opentracing.SpanOptions;
298
+ export type SpanOptions = Omit<opentracing.SpanOptions, 'childOf'> & {
299
+ /**
300
+ * Set childOf to 'null' to create a root span without a parent, even when a parent span
301
+ * exists in the current async context. If 'undefined' the parent will be inferred from the
302
+ * existing async context.
303
+ */
304
+ childOf?: opentracing.Span | opentracing.SpanContext | null;
305
+ /**
306
+ * Optional name of the integration that crated this span.
307
+ */
308
+ integrationName?: string;
309
+ };
299
310
  export { Tracer };
300
311
 
301
312
  export interface TraceOptions extends Analyzable {
@@ -749,6 +760,14 @@ declare namespace tracer {
749
760
  */
750
761
  dbmPropagationMode?: 'disabled' | 'service' | 'full'
751
762
 
763
+ /**
764
+ * Whether to enable Data Streams Monitoring.
765
+ * Can also be enabled via the DD_DATA_STREAMS_ENABLED environment variable.
766
+ * When not provided, the value of DD_DATA_STREAMS_ENABLED is used.
767
+ * @default false
768
+ */
769
+ dsmEnabled?: boolean
770
+
752
771
  /**
753
772
  * Configuration of the AppSec protection. Can be a boolean as an alias to `appsec.enabled`.
754
773
  */
@@ -3224,13 +3243,13 @@ declare namespace tracer {
3224
3243
  /**
3225
3244
  * The type of evaluation metric, one of 'categorical', 'score', or 'boolean'
3226
3245
  */
3227
- metricType: 'categorical' | 'score' | 'boolean',
3246
+ metricType: 'categorical' | 'score' | 'boolean' | 'json',
3228
3247
 
3229
3248
  /**
3230
3249
  * The value of the evaluation metric.
3231
- * Must be string for 'categorical' metrics, number for 'score' metrics, and boolean for 'boolean' metrics.
3250
+ * Must be string for 'categorical' metrics, number for 'score' metrics, boolean for 'boolean' metrics and a JSON object for 'json' metrics.
3232
3251
  */
3233
- value: string | number | boolean,
3252
+ value: string | number | boolean | { [key: string]: any },
3234
3253
 
3235
3254
  /**
3236
3255
  * An object of string key-value pairs to tag the evaluation metric with.
@@ -3246,6 +3265,21 @@ declare namespace tracer {
3246
3265
  * The timestamp in milliseconds when the evaluation metric result was generated.
3247
3266
  */
3248
3267
  timestampMs?: number
3268
+
3269
+ /**
3270
+ * Reasoning for the evaluation result.
3271
+ */
3272
+ reasoning?: string,
3273
+
3274
+ /**
3275
+ * Whether the evaluation passed or failed. Valid values are pass and fail.
3276
+ */
3277
+ assessment?: 'pass' | 'fail',
3278
+
3279
+ /**
3280
+ * Arbitrary JSON data associated with the evaluation.
3281
+ */
3282
+ metadata?: { [key: string]: any }
3249
3283
  }
3250
3284
 
3251
3285
  interface Document {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.85.0",
3
+ "version": "5.87.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -59,28 +59,46 @@ class DatadogStorage extends AsyncLocalStorage {
59
59
  return stores.get(handle)
60
60
  }
61
61
  }
62
+ }
63
+
64
+ // To handle all versions always correct, feature detect AsyncContextFrame and
65
+ // fallback to manual approach if not active. With ACF `run` delegates to
66
+ // `enterWith`, without ACF `run` does not.
67
+ const isACFActive = (() => {
68
+ let active = false
69
+ const als = new AsyncLocalStorage()
70
+ als.enterWith = () => { active = true }
71
+ als.run(1, () => {})
72
+ als.disable()
73
+ return active
74
+ })()
75
+
76
+ if (!isACFActive) {
77
+ const superGetStore = AsyncLocalStorage.prototype.getStore
78
+ const superEnterWith = AsyncLocalStorage.prototype.enterWith
62
79
 
63
80
  /**
64
- * Here, we replicate the behavior of the original `run()` method. We ensure
65
- * that our `enterWith()` is called internally, so that the handle to the
66
- * store is set. As an optimization, we use super for getStore and enterWith
67
- * when dealing with the parent store, so that we don't have to access the
68
- * WeakMap.
81
+ * Override the `run` method to manually call `enterWith` and `getStore`
82
+ * when not using AsyncContextFrame.
83
+ *
84
+ * Without ACF, super.run() won't call this.enterWith(), so the WeakMap handle
85
+ * is never created and getStore() would fail.
86
+ *
69
87
  * @template R
70
- * @template TArgs = unknown[]
88
+ * @template {unknown[]} TArgs
71
89
  * @param {Store<unknown>} store
72
- * @param {() => R} fn
73
- * @param {...TArgs} args
90
+ * @param {(...args: TArgs) => R} fn
91
+ * @param {TArgs} args
74
92
  * @returns {R}
75
93
  * @override
76
94
  */
77
- run (store, fn, ...args) {
78
- const prior = super.getStore()
95
+ DatadogStorage.prototype.run = function run (store, fn, ...args) {
96
+ const prior = superGetStore.call(this)
79
97
  this.enterWith(store)
80
98
  try {
81
99
  return Reflect.apply(fn, null, args)
82
100
  } finally {
83
- super.enterWith(prior)
101
+ superEnterWith.call(this, prior)
84
102
  }
85
103
  }
86
104
  }
@@ -111,4 +129,4 @@ function storage (namespace) {
111
129
  return storages[namespace]
112
130
  }
113
131
 
114
- module.exports = { storage }
132
+ module.exports = { storage, isACFActive }
@@ -353,6 +353,20 @@ function wrapRun (pl, isLatestVersion, version) {
353
353
  isEfdRetry = numRetries > 0
354
354
  }
355
355
 
356
+ // Check if all EFD retries failed
357
+ if (isEfdRetry && (isNew || isModified)) {
358
+ const statuses = lastStatusByPickleId.get(this.pickle.id)
359
+ if (statuses.length === earlyFlakeDetectionNumRetries + 1) {
360
+ const { fail } = statuses.reduce((acc, status) => {
361
+ acc[status]++
362
+ return acc
363
+ }, { pass: 0, fail: 0 })
364
+ if (fail === earlyFlakeDetectionNumRetries + 1) {
365
+ hasFailedAllRetries = true
366
+ }
367
+ }
368
+ }
369
+
356
370
  const attemptCtx = numAttemptToCtx.get(numAttempt)
357
371
 
358
372
  const error = getErrorFromCucumberResult(result)
@@ -136,6 +136,7 @@ module.exports = {
136
136
  'selenium-webdriver': () => require('../selenium'),
137
137
  sequelize: () => require('../sequelize'),
138
138
  sharedb: () => require('../sharedb'),
139
+ stripe: () => require('../stripe'),
139
140
  tedious: () => require('../tedious'),
140
141
  tinypool: { esmFirst: true, fn: () => require('../vitest') },
141
142
  undici: () => require('../undici'),
@@ -14,6 +14,7 @@ const finishChannel = channel('apm:http:client:request:finish')
14
14
  const endChannel = channel('apm:http:client:request:end')
15
15
  const asyncStartChannel = channel('apm:http:client:request:asyncStart')
16
16
  const errorChannel = channel('apm:http:client:request:error')
17
+ const responseFinishChannel = channel('apm:http:client:response:finish')
17
18
 
18
19
  const names = ['http', 'https', 'node:http', 'node:https']
19
20
 
@@ -39,6 +40,112 @@ function normalizeCallback (inputOptions, callback, inputURL) {
39
40
  return typeof inputOptions === 'function' ? [inputOptions, inputURL || {}] : [callback, inputOptions]
40
41
  }
41
42
 
43
+ /**
44
+ * Wires the downstream response so we can observe when the customer consumes
45
+ * the body and when the stream finishes
46
+ *
47
+ * @param {object} ctx - Instrumentation context
48
+ * @param {import('http').IncomingMessage} res - The downstream response object.
49
+ * @returns {{ finalizeIfNeeded: () => void }|null} Cleanup helper used for drain.
50
+ */
51
+ function setupResponseInstrumentation (ctx, res) {
52
+ const shouldInstrumentFinish = responseFinishChannel.hasSubscribers
53
+
54
+ if (!shouldInstrumentFinish) {
55
+ return null
56
+ }
57
+
58
+ let bodyConsumed = false
59
+ let finishCalled = false
60
+ let originalRead = null
61
+ let dataListenerAdded = false
62
+ let dataReadStarted = false
63
+
64
+ const { shouldCollectBody } = ctx
65
+ const bodyChunks = shouldCollectBody ? [] : null
66
+
67
+ const collectChunk = chunk => {
68
+ if (!shouldCollectBody || !chunk) return
69
+
70
+ if (typeof chunk === 'string') {
71
+ bodyChunks.push(chunk)
72
+ } else if (Buffer.isBuffer(chunk)) {
73
+ bodyChunks.push(chunk)
74
+ } else {
75
+ // Handle Uint8Array or other array-like types
76
+ bodyChunks.push(Buffer.from(chunk))
77
+ }
78
+ }
79
+
80
+ // Listen for body consumption
81
+ const onNewListener = (eventName) => {
82
+ if (eventName === 'data' || eventName === 'readable') {
83
+ bodyConsumed = true
84
+
85
+ // For 'data' events, add our own listener to collect chunks
86
+ if (eventName === 'data' && !dataListenerAdded && !dataReadStarted) {
87
+ dataListenerAdded = true
88
+ res.on('data', collectChunk)
89
+ }
90
+
91
+ // For 'readable' events, wrap the read() method
92
+ if (eventName === 'readable' && !originalRead && !dataListenerAdded && typeof res.read === 'function') {
93
+ originalRead = res.read
94
+ res.read = function () {
95
+ const chunk = originalRead.apply(this, arguments)
96
+ if (!dataListenerAdded) {
97
+ dataReadStarted = true
98
+ collectChunk(chunk)
99
+ }
100
+ return chunk
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ res.on('newListener', onNewListener)
107
+
108
+ // Cleanup function to restore original behavior
109
+ const cleanup = () => {
110
+ res.off('newListener', onNewListener)
111
+ res.off('data', collectChunk)
112
+
113
+ if (originalRead) {
114
+ res.read = originalRead
115
+ originalRead = null
116
+ }
117
+ }
118
+
119
+ const notifyFinish = () => {
120
+ if (finishCalled) return
121
+ finishCalled = true
122
+
123
+ // Combine collected chunks into a single body
124
+ let body = null
125
+ if (bodyChunks?.length) {
126
+ const firstChunk = bodyChunks[0]
127
+ body = typeof firstChunk === 'string'
128
+ ? bodyChunks.join('')
129
+ : Buffer.concat(bodyChunks)
130
+ }
131
+
132
+ responseFinishChannel.publish({ ctx, res, body })
133
+ cleanup()
134
+ }
135
+
136
+ res.once('end', notifyFinish)
137
+ res.once('close', notifyFinish)
138
+
139
+ return {
140
+ finalizeIfNeeded () {
141
+ if (!bodyConsumed) {
142
+ // Body not consumed, resume to complete the response
143
+ notifyFinish()
144
+ }
145
+ },
146
+ }
147
+ }
148
+
42
149
  function patch (http, methodName) {
43
150
  shimmer.wrap(http, methodName, instrumentRequest)
44
151
 
@@ -103,7 +210,18 @@ function patch (http, methodName) {
103
210
  ctx.res = res
104
211
  res.once('end', finish)
105
212
  res.once(errorMonitor, finish)
106
- break
213
+
214
+ const instrumentation = setupResponseInstrumentation(ctx, res)
215
+
216
+ if (!instrumentation) {
217
+ break
218
+ }
219
+
220
+ const result = emit.apply(this, arguments)
221
+
222
+ instrumentation.finalizeIfNeeded()
223
+
224
+ return result
107
225
  }
108
226
  case 'connect':
109
227
  case 'upgrade':
@@ -7,6 +7,7 @@ const {
7
7
  getCoveredFilenamesFromCoverage,
8
8
  JEST_WORKER_TRACE_PAYLOAD_CODE,
9
9
  JEST_WORKER_COVERAGE_PAYLOAD_CODE,
10
+ JEST_WORKER_TELEMETRY_PAYLOAD_CODE,
10
11
  getTestLineStart,
11
12
  getTestSuitePath,
12
13
  getTestParametersString,
@@ -16,6 +17,7 @@ const {
16
17
  isModifiedTest,
17
18
  } = require('../../dd-trace/src/plugins/util/test')
18
19
  const {
20
+ SEED_SUFFIX_RE,
19
21
  getFormattedJestTestParameters,
20
22
  getJestTestName,
21
23
  getJestSuitesToRun,
@@ -35,6 +37,7 @@ const testSuiteErrorCh = channel('ci:jest:test-suite:error')
35
37
  const workerReportTraceCh = channel('ci:jest:worker-report:trace')
36
38
  const workerReportCoverageCh = channel('ci:jest:worker-report:coverage')
37
39
  const workerReportLogsCh = channel('ci:jest:worker-report:logs')
40
+ const workerReportTelemetryCh = channel('ci:jest:worker-report:telemetry')
38
41
 
39
42
  const testSuiteCodeCoverageCh = channel('ci:jest:test-suite:code-coverage')
40
43
 
@@ -55,6 +58,7 @@ const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
55
58
  // Message sent by jest's main process to workers to run a test suite (=test file)
56
59
  // https://github.com/jestjs/jest/blob/1d682f21c7a35da4d3ab3a1436a357b980ebd0fa/packages/jest-worker/src/types.ts#L37
57
60
  const CHILD_MESSAGE_CALL = 1
61
+
58
62
  // Maximum time we'll wait for the tracer to flush
59
63
  const FLUSH_TIMEOUT = 10_000
60
64
 
@@ -125,6 +129,8 @@ function getTestEnvironmentOptions (config) {
125
129
  return {}
126
130
  }
127
131
 
132
+ const MAX_IGNORED_TEST_NAMES = 10
133
+
128
134
  function getTestStats (testStatuses) {
129
135
  return testStatuses.reduce((acc, testStatus) => {
130
136
  acc[testStatus]++
@@ -132,6 +138,32 @@ function getTestStats (testStatuses) {
132
138
  }, { pass: 0, fail: 0 })
133
139
  }
134
140
 
141
+ /**
142
+ * @param {string[]} efdNames
143
+ * @param {string[]} quarantineNames
144
+ * @param {number} totalCount
145
+ */
146
+ function logIgnoredFailuresSummary (efdNames, quarantineNames, totalCount) {
147
+ const names = []
148
+ for (const n of efdNames) {
149
+ names.push({ name: n, reason: 'Early Flake Detection' })
150
+ }
151
+ for (const n of quarantineNames) {
152
+ names.push({ name: n, reason: 'Quarantine' })
153
+ }
154
+ const shown = names.slice(0, MAX_IGNORED_TEST_NAMES)
155
+ const more = names.length - shown.length
156
+ const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
157
+ const list = shown.map(({ name, reason }) => ` • ${name} (${reason})`).join('\n')
158
+ const line = '-'.repeat(50)
159
+ // eslint-disable-next-line no-console -- Intentional user-facing message when exit code is flipped
160
+ console.warn(
161
+ `\n${line}\nDatadog Test Optimization\n${line}\n` +
162
+ `${totalCount} test failure(s) were ignored. Exit code set to 0.\n\n` +
163
+ `${list}${moreSuffix}\n`
164
+ )
165
+ }
166
+
135
167
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
136
168
  return class DatadogEnvironment extends BaseEnvironment {
137
169
  constructor (config, context) {
@@ -515,6 +547,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
515
547
  }
516
548
  }
517
549
  if (event.name === 'test_done') {
550
+ const originalError = event.test?.errors?.[0]
518
551
  let status = 'pass'
519
552
  if (event.test.errors && event.test.errors.length) {
520
553
  status = 'fail'
@@ -571,6 +604,13 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
571
604
  } else {
572
605
  newTestsTestStatuses.set(testName, [status])
573
606
  }
607
+ const testStatuses = newTestsTestStatuses.get(testName)
608
+ // Check if this is the last EFD retry.
609
+ // If it is, we'll set the failedAllTests flag to true if all the tests failed
610
+ if (testStatuses.length === earlyFlakeDetectionNumRetries + 1 &&
611
+ testStatuses.every(status => status === 'fail')) {
612
+ failedAllTests = true
613
+ }
574
614
  }
575
615
  }
576
616
 
@@ -594,7 +634,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
594
634
  const shouldSetProbe = this.isDiEnabled && willBeRetriedByFailedTestReplay && numTestExecutions === 1
595
635
  testErrCh.publish({
596
636
  ...ctx.currentStore,
597
- error: formatJestError(event.test.errors[0]),
637
+ error: formatJestError(originalError),
598
638
  shouldSetProbe,
599
639
  promises,
600
640
  finalStatus,
@@ -954,7 +994,7 @@ function getCliWrapper (isNewJestVersion) {
954
994
  isTestManagementTestsEnabled = false
955
995
  testManagementTests = {}
956
996
  } else {
957
- testManagementTests = receivedTestManagementTests
997
+ testManagementTests = receivedTestManagementTests || {}
958
998
  }
959
999
  } catch (err) {
960
1000
  log.error('Jest test management tests error', err)
@@ -989,11 +1029,18 @@ function getCliWrapper (isNewJestVersion) {
989
1029
  coverageMap,
990
1030
  numFailedTestSuites,
991
1031
  numFailedTests,
1032
+ numRuntimeErrorTestSuites = 0,
992
1033
  numTotalTests,
993
1034
  numTotalTestSuites,
1035
+ runExecError,
1036
+ wasInterrupted,
994
1037
  },
995
1038
  } = result
996
1039
 
1040
+ const hasSuiteLevelFailures = numRuntimeErrorTestSuites > 0
1041
+ const hasRunLevelFailure = runExecError != null || wasInterrupted === true
1042
+ const mustNotFlipSuccess = hasSuiteLevelFailures || hasRunLevelFailure
1043
+
997
1044
  let testCodeCoverageLinesTotal
998
1045
 
999
1046
  if (isUserCodeCoverageEnabled) {
@@ -1012,33 +1059,67 @@ function getCliWrapper (isNewJestVersion) {
1012
1059
  * The rationale behind is the following: you may still be able to block your CI pipeline by gating
1013
1060
  * on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
1014
1061
  */
1062
+ let numEfdFailedTestsToIgnore = 0
1063
+ const efdIgnoredNames = []
1064
+ const quarantineIgnoredNames = []
1065
+
1066
+ // Build fullName -> suite map from results (for EFD display)
1067
+ const fullNameToSuite = new Map()
1068
+ for (const { testResults, testFilePath } of result.results.testResults) {
1069
+ const suite = getTestSuitePath(testFilePath, result.globalConfig.rootDir)
1070
+ for (const { fullName } of testResults) {
1071
+ const name = testSuiteAbsolutePathsWithFastCheck.has(testFilePath)
1072
+ ? fullName.replace(SEED_SUFFIX_RE, '')
1073
+ : fullName
1074
+ fullNameToSuite.set(name, suite)
1075
+ }
1076
+ }
1077
+
1078
+ /** @type {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} */
1079
+ let ignoredFailuresSummary
1015
1080
  if (isEarlyFlakeDetectionEnabled) {
1016
- let numFailedTestsToIgnore = 0
1017
- for (const testStatuses of newTestsTestStatuses.values()) {
1081
+ for (const [testName, testStatuses] of newTestsTestStatuses) {
1018
1082
  const { pass, fail } = getTestStats(testStatuses)
1019
1083
  if (pass > 0) { // as long as one passes, we'll consider the test passed
1020
- numFailedTestsToIgnore += fail
1084
+ numEfdFailedTestsToIgnore += fail
1085
+ const suite = fullNameToSuite.get(testName)
1086
+ efdIgnoredNames.push(suite ? `${suite} › ${testName}` : testName)
1021
1087
  }
1022
1088
  }
1023
1089
  // If every test that failed was an EFD retry, we'll consider the suite passed
1024
- if (numFailedTestsToIgnore !== 0 && result.results.numFailedTests === numFailedTestsToIgnore) {
1090
+ if (
1091
+ !mustNotFlipSuccess &&
1092
+ numEfdFailedTestsToIgnore !== 0 &&
1093
+ result.results.numFailedTests === numEfdFailedTestsToIgnore
1094
+ ) {
1025
1095
  result.results.success = true
1096
+ ignoredFailuresSummary = {
1097
+ efdNames: efdIgnoredNames,
1098
+ quarantineNames: [],
1099
+ totalCount: numEfdFailedTestsToIgnore,
1100
+ }
1026
1101
  }
1027
1102
  }
1028
1103
 
1104
+ let numFailedQuarantinedTests = 0
1105
+ let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
1029
1106
  if (isTestManagementTestsEnabled) {
1030
1107
  const failedTests = result
1031
1108
  .results
1032
1109
  .testResults.flatMap(({ testResults, testFilePath: testSuiteAbsolutePath }) => (
1033
1110
  testResults.map(({ fullName: testName, status }) => (
1034
- { testName, testSuiteAbsolutePath, status }
1111
+ {
1112
+ // Strip @fast-check/jest seed suffix so the name matches what was reported via TEST_NAME
1113
+ testName: testSuiteAbsolutePathsWithFastCheck.has(testSuiteAbsolutePath)
1114
+ ? testName.replace(SEED_SUFFIX_RE, '')
1115
+ : testName,
1116
+ testSuiteAbsolutePath,
1117
+ status,
1118
+ }
1035
1119
  ))
1036
1120
  ))
1037
1121
  .filter(({ status }) => status === 'failed')
1038
1122
 
1039
- let numFailedQuarantinedTests = 0
1040
- let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
1041
-
1042
1123
  for (const { testName, testSuiteAbsolutePath } of failedTests) {
1043
1124
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, result.globalConfig.rootDir)
1044
1125
  const testManagementTest = testManagementTests
@@ -1051,8 +1132,10 @@ function getCliWrapper (isNewJestVersion) {
1051
1132
  // This uses `attempt_to_fix` because this is always the main process and it's not formatted in camelCase
1052
1133
  if (testManagementTest?.attempt_to_fix && (testManagementTest?.quarantined || testManagementTest?.disabled)) {
1053
1134
  numFailedQuarantinedOrDisabledAttemptedToFixTests++
1135
+ quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
1054
1136
  } else if (testManagementTest?.quarantined) {
1055
1137
  numFailedQuarantinedTests++
1138
+ quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
1056
1139
  }
1057
1140
  }
1058
1141
 
@@ -1060,12 +1143,42 @@ function getCliWrapper (isNewJestVersion) {
1060
1143
  // Note that if a test is attempted to fix,
1061
1144
  // it's considered quarantined both if it's disabled and if it's quarantined
1062
1145
  // (it'll run but its status is ignored)
1146
+ // Skip if EFD block already flipped (to avoid logging twice)
1063
1147
  if (
1148
+ !result.results.success &&
1149
+ !mustNotFlipSuccess &&
1064
1150
  (numFailedQuarantinedOrDisabledAttemptedToFixTests !== 0 || numFailedQuarantinedTests !== 0) &&
1065
1151
  result.results.numFailedTests ===
1066
1152
  numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1067
1153
  ) {
1068
1154
  result.results.success = true
1155
+ ignoredFailuresSummary = {
1156
+ efdNames: [],
1157
+ quarantineNames: quarantineIgnoredNames,
1158
+ totalCount: numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests,
1159
+ }
1160
+ }
1161
+ }
1162
+
1163
+ // Combined check: if all failed tests are accounted for by EFD (flaky retries) and/or quarantine,
1164
+ // we should consider the suite passed even when neither check alone covers all failures.
1165
+ if (
1166
+ !result.results.success &&
1167
+ !mustNotFlipSuccess &&
1168
+ (isEarlyFlakeDetectionEnabled || isTestManagementTestsEnabled)
1169
+ ) {
1170
+ const totalIgnoredFailures =
1171
+ numEfdFailedTestsToIgnore + numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1172
+ if (
1173
+ totalIgnoredFailures !== 0 &&
1174
+ result.results.numFailedTests === totalIgnoredFailures
1175
+ ) {
1176
+ result.results.success = true
1177
+ ignoredFailuresSummary = {
1178
+ efdNames: efdIgnoredNames,
1179
+ quarantineNames: quarantineIgnoredNames,
1180
+ totalCount: totalIgnoredFailures,
1181
+ }
1069
1182
  }
1070
1183
  }
1071
1184
 
@@ -1123,6 +1236,14 @@ function getCliWrapper (isNewJestVersion) {
1123
1236
  })
1124
1237
  }
1125
1238
 
1239
+ if (ignoredFailuresSummary) {
1240
+ logIgnoredFailuresSummary(
1241
+ ignoredFailuresSummary.efdNames,
1242
+ ignoredFailuresSummary.quarantineNames,
1243
+ ignoredFailuresSummary.totalCount
1244
+ )
1245
+ }
1246
+
1126
1247
  numSkippedSuites = 0
1127
1248
 
1128
1249
  return result
@@ -1534,6 +1655,10 @@ function onMessageWrapper (onMessage) {
1534
1655
  workerReportLogsCh.publish(data)
1535
1656
  return
1536
1657
  }
1658
+ if (code === JEST_WORKER_TELEMETRY_PAYLOAD_CODE) { // datadog telemetry payload
1659
+ workerReportTelemetryCh.publish(data)
1660
+ return
1661
+ }
1537
1662
  return onMessage.apply(this, arguments)
1538
1663
  }
1539
1664
  }
@@ -165,6 +165,15 @@ function getOnEndHandler (isParallel) {
165
165
  this.failures -= numFailedQuarantinedTests + numFailedRetriedQuarantinedOrDisabledTests
166
166
  }
167
167
 
168
+ // Recompute status after EFD and quarantine adjustments have reduced failure counts
169
+ if (status === 'fail') {
170
+ if (this.stats) {
171
+ status = this.stats.failures === 0 ? 'pass' : 'fail'
172
+ } else {
173
+ status = this.failures === 0 ? 'pass' : 'fail'
174
+ }
175
+ }
176
+
168
177
  if (status === 'fail') {
169
178
  error = new Error(`Failed tests: ${this.failures}.`)
170
179
  }
@@ -271,6 +271,7 @@ function getOnTestEndHandler (config) {
271
271
  const testStatuses = testsStatuses.get(testName)
272
272
 
273
273
  const isLastAttempt = testStatuses.length === config.testManagementAttemptToFixRetries + 1
274
+ const isLastEfdRetry = testStatuses.length === config.earlyFlakeDetectionNumRetries + 1
274
275
 
275
276
  if (test._ddIsAttemptToFix && isLastAttempt) {
276
277
  if (testStatuses.includes('fail')) {
@@ -283,6 +284,11 @@ function getOnTestEndHandler (config) {
283
284
  }
284
285
  }
285
286
 
287
+ if (test._ddIsEfdRetry && isLastEfdRetry &&
288
+ testStatuses.every(status => status === 'fail')) {
289
+ hasFailedAllRetries = true
290
+ }
291
+
286
292
  const isAttemptToFixRetry = test._ddIsAttemptToFix && testStatuses.length > 1
287
293
  const isAtrRetry = config.isFlakyTestRetriesEnabled &&
288
294
  !test._ddIsAttemptToFix &&