dd-trace 5.102.0 → 5.103.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 (133) hide show
  1. package/ext/exporters.js +1 -0
  2. package/package.json +12 -11
  3. package/packages/datadog-esbuild/src/utils.js +2 -2
  4. package/packages/datadog-instrumentations/src/ai.js +1 -1
  5. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +32 -15
  6. package/packages/datadog-instrumentations/src/couchbase.js +69 -220
  7. package/packages/datadog-instrumentations/src/cucumber.js +1 -1
  8. package/packages/datadog-instrumentations/src/electron/preload.js +42 -0
  9. package/packages/datadog-instrumentations/src/electron.js +240 -0
  10. package/packages/datadog-instrumentations/src/fetch.js +5 -5
  11. package/packages/datadog-instrumentations/src/graphql.js +13 -12
  12. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +1 -1
  13. package/packages/datadog-instrumentations/src/helpers/hook.js +4 -1
  14. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  15. package/packages/datadog-instrumentations/src/helpers/instrument.js +2 -2
  16. package/packages/datadog-instrumentations/src/helpers/kafka.js +41 -0
  17. package/packages/datadog-instrumentations/src/ioredis.js +16 -12
  18. package/packages/datadog-instrumentations/src/jest.js +351 -50
  19. package/packages/datadog-instrumentations/src/kafkajs.js +164 -173
  20. package/packages/datadog-instrumentations/src/mocha/main.js +73 -1
  21. package/packages/datadog-instrumentations/src/mongodb-core.js +33 -8
  22. package/packages/datadog-instrumentations/src/pg.js +24 -10
  23. package/packages/datadog-instrumentations/src/playwright.js +427 -55
  24. package/packages/datadog-instrumentations/src/redis.js +19 -10
  25. package/packages/datadog-plugin-apollo/src/gateway/request.js +1 -21
  26. package/packages/datadog-plugin-aws-sdk/src/base.js +18 -24
  27. package/packages/datadog-plugin-aws-sdk/src/services/cloudwatchlogs.js +1 -1
  28. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +1 -1
  29. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  30. package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +1 -1
  31. package/packages/datadog-plugin-aws-sdk/src/services/redshift.js +1 -1
  32. package/packages/datadog-plugin-aws-sdk/src/services/s3.js +1 -1
  33. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +2 -2
  34. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
  35. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +1 -1
  36. package/packages/datadog-plugin-couchbase/src/index.js +58 -52
  37. package/packages/datadog-plugin-cucumber/src/index.js +1 -0
  38. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +214 -22
  39. package/packages/datadog-plugin-cypress/src/support.js +13 -1
  40. package/packages/datadog-plugin-electron/src/index.js +17 -0
  41. package/packages/datadog-plugin-electron/src/ipc.js +143 -0
  42. package/packages/datadog-plugin-electron/src/net.js +82 -0
  43. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +27 -18
  44. package/packages/datadog-plugin-graphql/src/execute.js +6 -28
  45. package/packages/datadog-plugin-graphql/src/resolve.js +30 -35
  46. package/packages/datadog-plugin-graphql/src/tools/signature.js +32 -7
  47. package/packages/datadog-plugin-graphql/src/tools/transforms.js +118 -100
  48. package/packages/datadog-plugin-graphql/src/utils.js +29 -0
  49. package/packages/datadog-plugin-grpc/src/client.js +6 -7
  50. package/packages/datadog-plugin-grpc/src/util.js +57 -22
  51. package/packages/datadog-plugin-http/src/client.js +2 -2
  52. package/packages/datadog-plugin-jest/src/index.js +92 -50
  53. package/packages/datadog-plugin-mocha/src/index.js +1 -0
  54. package/packages/datadog-plugin-mongodb-core/src/index.js +36 -70
  55. package/packages/datadog-plugin-mysql/src/index.js +1 -1
  56. package/packages/datadog-plugin-openai/src/services.js +2 -1
  57. package/packages/datadog-plugin-pg/src/index.js +3 -3
  58. package/packages/datadog-plugin-playwright/src/index.js +4 -0
  59. package/packages/datadog-plugin-redis/src/index.js +18 -23
  60. package/packages/dd-trace/src/aiguard/index.js +3 -1
  61. package/packages/dd-trace/src/aiguard/sdk.js +36 -30
  62. package/packages/dd-trace/src/aiguard/tags.js +20 -11
  63. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +2 -2
  64. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +1 -1
  65. package/packages/dd-trace/src/azure_metadata.js +17 -6
  66. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +4 -4
  67. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
  68. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +6 -4
  69. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +1 -1
  70. package/packages/dd-trace/src/config/defaults.js +3 -14
  71. package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -1
  72. package/packages/dd-trace/src/config/helper.js +4 -0
  73. package/packages/dd-trace/src/config/index.js +2 -2
  74. package/packages/dd-trace/src/config/major-overrides.js +98 -0
  75. package/packages/dd-trace/src/config/parsers.js +7 -1
  76. package/packages/dd-trace/src/config/supported-configurations.json +51 -38
  77. package/packages/dd-trace/src/datastreams/checkpointer.js +2 -2
  78. package/packages/dd-trace/src/datastreams/manager.js +1 -1
  79. package/packages/dd-trace/src/datastreams/processor.js +2 -2
  80. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +2 -2
  81. package/packages/dd-trace/src/debugger/devtools_client/source-maps.js +1 -1
  82. package/packages/dd-trace/src/debugger/devtools_client/state.js +2 -1
  83. package/packages/dd-trace/src/debugger/index.js +7 -7
  84. package/packages/dd-trace/src/dogstatsd.js +2 -2
  85. package/packages/dd-trace/src/encode/0.4.js +45 -54
  86. package/packages/dd-trace/src/encode/0.5.js +34 -3
  87. package/packages/dd-trace/src/encode/agentless-json.js +1 -1
  88. package/packages/dd-trace/src/exporter.js +2 -0
  89. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  90. package/packages/dd-trace/src/exporters/agentless/index.js +3 -2
  91. package/packages/dd-trace/src/exporters/agentless/writer.js +2 -2
  92. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -1
  93. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  94. package/packages/dd-trace/src/exporters/electron/index.js +49 -0
  95. package/packages/dd-trace/src/external-logger/src/index.js +2 -1
  96. package/packages/dd-trace/src/git_metadata.js +10 -8
  97. package/packages/dd-trace/src/lambda/handler-paths.js +52 -0
  98. package/packages/dd-trace/src/lambda/index.js +62 -14
  99. package/packages/dd-trace/src/lambda/runtime/patch.js +21 -46
  100. package/packages/dd-trace/src/llmobs/index.js +13 -2
  101. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +45 -15
  102. package/packages/dd-trace/src/llmobs/writers/base.js +2 -1
  103. package/packages/dd-trace/src/openfeature/writers/base.js +2 -1
  104. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +2 -1
  105. package/packages/dd-trace/src/opentracing/propagation/text_map.js +20 -9
  106. package/packages/dd-trace/src/payload-tagging/config/index.js +2 -2
  107. package/packages/dd-trace/src/plugins/ci_plugin.js +49 -4
  108. package/packages/dd-trace/src/plugins/database.js +54 -12
  109. package/packages/dd-trace/src/plugins/index.js +1 -0
  110. package/packages/dd-trace/src/plugins/plugin.js +2 -4
  111. package/packages/dd-trace/src/plugins/util/ci.js +8 -8
  112. package/packages/dd-trace/src/plugins/util/git-cache.js +20 -18
  113. package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -2
  114. package/packages/dd-trace/src/plugins/util/test.js +37 -5
  115. package/packages/dd-trace/src/plugins/util/user-provided-git.js +17 -15
  116. package/packages/dd-trace/src/priority_sampler.js +1 -1
  117. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  118. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  119. package/packages/dd-trace/src/profiling/ssi-heuristics.js +1 -1
  120. package/packages/dd-trace/src/rate_limiter.js +1 -1
  121. package/packages/dd-trace/src/remote_config/scheduler.js +1 -1
  122. package/packages/dd-trace/src/ritm.js +2 -1
  123. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -8
  124. package/packages/dd-trace/src/serverless.js +5 -2
  125. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +20 -0
  126. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +4 -0
  127. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +20 -0
  128. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +4 -0
  129. package/packages/dd-trace/src/span_stats.js +1 -1
  130. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  131. package/packages/dd-trace/src/telemetry/endpoints.js +1 -1
  132. package/packages/dd-trace/src/telemetry/telemetry.js +2 -2
  133. package/packages/dd-trace/src/lambda/runtime/ritm.js +0 -133
@@ -3,6 +3,7 @@
3
3
  // Capture real timers at module load time, before any test can install fake timers.
4
4
  const realSetTimeout = setTimeout
5
5
 
6
+ const { performance } = require('node:perf_hooks')
6
7
  const satisfies = require('../../../vendor/dist/semifies')
7
8
 
8
9
  const shimmer = require('../../datadog-shimmer')
@@ -12,6 +13,8 @@ const {
12
13
  PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE,
13
14
  getIsFaultyEarlyFlakeDetection,
14
15
  DYNAMIC_NAME_RE,
16
+ getEfdRetryCount,
17
+ getMaxEfdRetryCount,
15
18
  recordAttemptToFixExecution,
16
19
  logAttemptToFixTestExecution,
17
20
  logTestOptimizationSummary,
@@ -68,6 +71,7 @@ let remainingTestsByFile = {}
68
71
  let isKnownTestsEnabled = false
69
72
  let isEarlyFlakeDetectionEnabled = false
70
73
  let earlyFlakeDetectionNumRetries = 0
74
+ let earlyFlakeDetectionSlowTestRetries = {}
71
75
  let isEarlyFlakeDetectionFaulty = false
72
76
  let earlyFlakeDetectionFaultyThreshold = 0
73
77
  let isFlakyTestRetriesEnabled = false
@@ -83,10 +87,19 @@ let testsReportedInGenerateSummary = new Set()
83
87
  const newTestsWithDynamicNames = new Set()
84
88
  const attemptToFixExecutions = new Map()
85
89
  const loggedAttemptToFixTests = new Set()
90
+ const efdManagedTestKeys = new Set()
91
+ const efdRetryCountByTestKey = new Map()
92
+ const efdRetryCountRequestsByTestKey = new Map()
93
+ const efdRetryTestsById = new Map()
94
+ const efdScheduledOriginalTestKeys = new Set()
95
+ const efdStartedOriginalTestKeys = new Set()
96
+ const efdSlowAbortedTests = new Set()
86
97
  let rootDir = ''
87
98
  let sessionProjects = []
88
99
 
89
100
  const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0' // TODO: remove this once we drop support for v5
101
+ const EFD_RETRY_COUNT_REQUEST = 'ddEfdRetryCountRequest'
102
+ const EFD_RETRY_COUNT_RESPONSE = 'ddEfdRetryCountResponse'
90
103
 
91
104
  function isValidKnownTests (receivedKnownTests) {
92
105
  return !!receivedKnownTests.playwright
@@ -97,6 +110,222 @@ function getTestFullyQualifiedName (test) {
97
110
  return `${test._requireFile} ${fullname}`
98
111
  }
99
112
 
113
+ /**
114
+ * @param {object} test
115
+ * @returns {string|undefined}
116
+ */
117
+ function getTestProjectKey (test) {
118
+ const { _projectIndex, _projectId } = test
119
+ if (_projectIndex !== undefined) {
120
+ return `index:${_projectIndex}`
121
+ }
122
+ if (_projectId !== undefined) {
123
+ return `id:${_projectId}`
124
+ }
125
+
126
+ const projectSuite = getSuiteType(test, 'project')
127
+ const projectName = projectSuite?._fullProject?.project?.name ||
128
+ projectSuite?._fullProject?.name ||
129
+ projectSuite?.title
130
+ if (projectName) {
131
+ return `name:${projectName}`
132
+ }
133
+ }
134
+
135
+ /**
136
+ * @param {object} test
137
+ * @returns {number|undefined}
138
+ */
139
+ function getTestEfdRepeatEachIndex (test) {
140
+ if (Object.hasOwn(test, '_ddEfdOriginalRepeatEachIndex')) {
141
+ return test._ddEfdOriginalRepeatEachIndex
142
+ }
143
+ return test.repeatEachIndex
144
+ }
145
+
146
+ /**
147
+ * @param {object} test
148
+ * @returns {string|undefined}
149
+ */
150
+ function getTestRepeatEachKey (test) {
151
+ const repeatEachIndex = getTestEfdRepeatEachIndex(test)
152
+ if (repeatEachIndex !== undefined) {
153
+ return `repeat:${repeatEachIndex}`
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @param {object} test
159
+ * @returns {string}
160
+ */
161
+ function getTestEfdKey (test) {
162
+ const projectKey = getTestProjectKey(test)
163
+ const repeatEachKey = getTestRepeatEachKey(test)
164
+ const testFqn = getTestFullyQualifiedName(test)
165
+ return [projectKey, repeatEachKey, testFqn].filter(Boolean).join(' ')
166
+ }
167
+
168
+ function getConfiguredEfdRetryCount () {
169
+ if (!earlyFlakeDetectionSlowTestRetries || !Object.keys(earlyFlakeDetectionSlowTestRetries).length) {
170
+ return earlyFlakeDetectionNumRetries
171
+ }
172
+ return getMaxEfdRetryCount(earlyFlakeDetectionSlowTestRetries)
173
+ }
174
+
175
+ function markEfdManagedTest (test) {
176
+ test._ddIsEfdManagedTest = true
177
+ test._ddEfdSlowTestRetries = earlyFlakeDetectionSlowTestRetries
178
+ efdManagedTestKeys.add(getTestEfdKey(test))
179
+ }
180
+
181
+ function markEfdRetryTest (test, retryIndex, originalTest) {
182
+ test._ddIsEfdRetry = true
183
+ test._ddEfdRetryIndex = retryIndex
184
+ if (originalTest) {
185
+ test._ddEfdOriginalRepeatEachIndex = getTestEfdRepeatEachIndex(originalTest)
186
+ }
187
+ }
188
+
189
+ function registerEfdRetryTest (test) {
190
+ if (!test._ddIsEfdRetry) {
191
+ return
192
+ }
193
+
194
+ efdRetryTestsById.set(test.id, {
195
+ retryIndex: test._ddEfdRetryIndex,
196
+ testEfdKey: getTestEfdKey(test),
197
+ })
198
+ }
199
+
200
+ function getTestEfdSlowTestRetries (test) {
201
+ return test._ddEfdSlowTestRetries || earlyFlakeDetectionSlowTestRetries
202
+ }
203
+
204
+ function isTestEfdManaged (test) {
205
+ return !!test._ddIsEfdManagedTest || (
206
+ (test._ddIsNew || test._ddIsModified) &&
207
+ !test._ddIsAttemptToFix &&
208
+ isEarlyFlakeDetectionEnabled
209
+ )
210
+ }
211
+
212
+ function getFileSuiteRepeatEachIndex (fileSuite) {
213
+ const test = fileSuite.allTests()[0]
214
+ return test ? getTestEfdRepeatEachIndex(test) || 0 : 0
215
+ }
216
+
217
+ function getEfdRetryRepeatEachIndex (fileSuite, projectSuite, retryIndex, retryCount) {
218
+ const nativeRepeatEach = projectSuite._fullProject?.project?.repeatEach || 1
219
+ const originalRepeatEachIndex = getFileSuiteRepeatEachIndex(fileSuite)
220
+ return nativeRepeatEach + (originalRepeatEachIndex * retryCount) + retryIndex - 1
221
+ }
222
+
223
+ function getEfdRetryCountForTest (test) {
224
+ return efdRetryCountByTestKey.get(getTestEfdKey(test)) ?? getConfiguredEfdRetryCount()
225
+ }
226
+
227
+ function setEfdRetryCountForTest (test, retryCount) {
228
+ const testEfdKey = getTestEfdKey(test)
229
+ efdRetryCountByTestKey.set(testEfdKey, retryCount)
230
+
231
+ const requests = efdRetryCountRequestsByTestKey.get(testEfdKey)
232
+ if (requests) {
233
+ efdRetryCountRequestsByTestKey.delete(testEfdKey)
234
+ for (const resolveRequest of requests) {
235
+ resolveRequest(retryCount)
236
+ }
237
+ }
238
+ }
239
+
240
+ function sendEfdRetryCountToWorker (workerProcess, testId, retryIndex, retryCount) {
241
+ workerProcess.send({
242
+ type: EFD_RETRY_COUNT_RESPONSE,
243
+ testId,
244
+ isEfdRetry: retryIndex !== undefined,
245
+ retryIndex,
246
+ retryCount,
247
+ })
248
+ }
249
+
250
+ function sendEfdRetryCountToWorkerWhenAvailable (workerProcess, testId) {
251
+ const efdRetryTest = efdRetryTestsById.get(testId)
252
+ if (!efdRetryTest) {
253
+ sendEfdRetryCountToWorker(workerProcess, testId)
254
+ return
255
+ }
256
+
257
+ const { retryIndex, testEfdKey } = efdRetryTest
258
+
259
+ if (!testEfdKey || !efdManagedTestKeys.has(testEfdKey)) {
260
+ sendEfdRetryCountToWorker(workerProcess, testId)
261
+ return
262
+ }
263
+
264
+ const retryCount = efdRetryCountByTestKey.get(testEfdKey)
265
+ if (retryCount !== undefined) {
266
+ sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, retryCount)
267
+ return
268
+ }
269
+
270
+ if (!efdStartedOriginalTestKeys.has(testEfdKey) && !efdScheduledOriginalTestKeys.has(testEfdKey)) {
271
+ sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, 0)
272
+ return
273
+ }
274
+
275
+ if (!efdRetryCountRequestsByTestKey.has(testEfdKey)) {
276
+ efdRetryCountRequestsByTestKey.set(testEfdKey, [])
277
+ }
278
+ efdRetryCountRequestsByTestKey.get(testEfdKey).push((retryCount) => {
279
+ sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, retryCount)
280
+ })
281
+ }
282
+
283
+ /**
284
+ * @param {object} test
285
+ * @returns {boolean}
286
+ */
287
+ function shouldRequestEfdRetryCount (test) {
288
+ // The main process remains the source of truth. repeatEachIndex is only used as
289
+ // a cheap worker-side filter so first executions do not block on coordination.
290
+ return test._ddIsEfdRetry || test.repeatEachIndex > 0
291
+ }
292
+
293
+ function waitForEfdRetryCount (test) {
294
+ if (!process.send || !shouldRequestEfdRetryCount(test)) {
295
+ return Promise.resolve()
296
+ }
297
+
298
+ const testEfdKey = getTestEfdKey(test)
299
+ return new Promise(resolve => {
300
+ const messageHandler = (message) => {
301
+ if (message?.type === EFD_RETRY_COUNT_RESPONSE && message.testId === test.id) {
302
+ if (message.isEfdRetry) {
303
+ test._ddIsEfdRetry = true
304
+ test._ddEfdRetryIndex = message.retryIndex
305
+ test._ddEfdRetryCount = message.retryCount
306
+ efdRetryCountByTestKey.set(testEfdKey, message.retryCount)
307
+ }
308
+ process.removeListener('message', messageHandler)
309
+ resolve()
310
+ }
311
+ }
312
+
313
+ process.on('message', messageHandler)
314
+ process.send({
315
+ type: EFD_RETRY_COUNT_REQUEST,
316
+ testId: test.id,
317
+ })
318
+ })
319
+ }
320
+
321
+ function shouldSkipEfdRetry (test) {
322
+ if (!test._ddIsEfdRetry) {
323
+ return false
324
+ }
325
+ const retryCount = test._ddEfdRetryCount ?? efdRetryCountByTestKey.get(getTestEfdKey(test))
326
+ return retryCount !== undefined && test._ddEfdRetryIndex > retryCount
327
+ }
328
+
100
329
  function getTestProperties (test) {
101
330
  const testName = getTestFullname(test)
102
331
  const testSuite = getTestSuitePath(test._requireFile, rootDir)
@@ -125,14 +354,17 @@ function getSuiteType (test, type) {
125
354
  }
126
355
 
127
356
  // Copy of Suite#_deepClone but with a function to filter tests
128
- function deepCloneSuite (suite, filterTest, tags = []) {
357
+ function deepCloneSuite (suite, filterTest, tags = [], configureCopiedTest) {
129
358
  const copy = suite._clone()
130
359
  for (const entry of suite._entries) {
131
360
  if (entry.constructor.name === 'Suite') {
132
- copy._addSuite(deepCloneSuite(entry, filterTest, tags))
361
+ copy._addSuite(deepCloneSuite(entry, filterTest, tags, configureCopiedTest))
133
362
  } else {
134
363
  if (filterTest(entry)) {
135
364
  const copiedTest = entry._clone()
365
+ if (configureCopiedTest) {
366
+ configureCopiedTest(copiedTest, entry)
367
+ }
136
368
  for (const tag of tags) {
137
369
  const resolvedTag = typeof tag === 'function' ? tag(entry) : tag
138
370
 
@@ -303,6 +535,7 @@ function getFinalStatus ({
303
535
  isAttemptToFix,
304
536
  hasFailedAllRetries,
305
537
  hasFailedAttemptToFixRetries,
538
+ hasPassedAnyEfdAttempt,
306
539
  testStatus,
307
540
  }) {
308
541
  if (!isFinalExecution) {
@@ -311,9 +544,12 @@ function getFinalStatus ({
311
544
  if (isDisabled || isQuarantined || testStatus === 'skip') {
312
545
  return 'skip'
313
546
  }
314
- if (isAtrRetry || isEfdManagedTest) {
547
+ if (isAtrRetry) {
315
548
  return hasFailedAllRetries ? 'fail' : 'pass'
316
549
  }
550
+ if (isEfdManagedTest) {
551
+ return hasPassedAnyEfdAttempt ? 'pass' : 'fail'
552
+ }
317
553
  if (isAttemptToFix) {
318
554
  return hasFailedAttemptToFixRetries ? 'fail' : 'pass'
319
555
  }
@@ -350,6 +586,14 @@ function testBeginHandler (test, browserName, shouldCreateTestSpan) {
350
586
  if (_type === 'beforeAll' || _type === 'afterAll') {
351
587
  return
352
588
  }
589
+ if (shouldSkipEfdRetry(test)) {
590
+ test._ddShouldSkipEfdRetry = true
591
+ return
592
+ }
593
+ test._ddStartTime = performance.now()
594
+ if (isTestEfdManaged(test) && !test._ddIsEfdRetry) {
595
+ efdStartedOriginalTestKeys.add(getTestEfdKey(test))
596
+ }
353
597
  // this means that a skipped test is being handled
354
598
  if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
355
599
  return
@@ -391,6 +635,45 @@ function testBeginHandler (test, browserName, shouldCreateTestSpan) {
391
635
  }
392
636
  }
393
637
 
638
+ function finishTestSuiteIfDone (testSuiteAbsolutePath, projects) {
639
+ if (!shouldFinishTestSuite(testSuiteAbsolutePath)) {
640
+ return
641
+ }
642
+
643
+ const skippedTests = remainingTestsByFile[testSuiteAbsolutePath]
644
+ .filter(test => test.expectedStatus === 'skipped')
645
+
646
+ for (const test of skippedTests) {
647
+ const browserName = getBrowserNameFromProjects(projects, test)
648
+ testSkipCh.publish({
649
+ testName: getTestFullname(test),
650
+ testSuiteAbsolutePath,
651
+ testSourceFileAbsolutePath: test.location.file,
652
+ testSourceLine: test.location.line,
653
+ browserName,
654
+ isNew: test._ddIsNew,
655
+ isDisabled: test._ddIsDisabled,
656
+ isModified: test._ddIsModified,
657
+ isQuarantined: test._ddIsQuarantined,
658
+ })
659
+ }
660
+ remainingTestsByFile[testSuiteAbsolutePath] = []
661
+
662
+ const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath)
663
+ let testSuiteStatus = 'pass'
664
+ if (testStatuses?.includes('fail')) {
665
+ testSuiteStatus = 'fail'
666
+ } else if (testStatuses?.every(status => status === 'skip')) {
667
+ testSuiteStatus = 'skip'
668
+ }
669
+
670
+ const suiteError = getTestSuiteError(testSuiteAbsolutePath)
671
+ const testSuiteCtx = testSuiteToCtx.get(testSuiteAbsolutePath)
672
+ if (testSuiteCtx) {
673
+ testSuiteFinishCh.publish({ status: testSuiteStatus, error: suiteError, ...testSuiteCtx.currentStore })
674
+ }
675
+ }
676
+
394
677
  function testEndHandler ({
395
678
  test,
396
679
  annotations,
@@ -420,11 +703,21 @@ function testEndHandler ({
420
703
  return
421
704
  }
422
705
 
706
+ if (test._ddShouldSkipEfdRetry || shouldSkipEfdRetry(test)) {
707
+ test._ddShouldSkipEfdRetry = true
708
+ remainingTestsByFile[testSuiteAbsolutePath] = remainingTestsByFile[testSuiteAbsolutePath]
709
+ .filter(currentTest => currentTest !== test)
710
+ finishTestSuiteIfDone(testSuiteAbsolutePath, projects)
711
+ return
712
+ }
713
+
714
+ const isEfdManagedTest = isTestEfdManaged(test)
423
715
  const testFqn = getTestFullyQualifiedName(test)
424
- const testStatuses = testsToTestStatuses.get(testFqn) || []
716
+ const testStatusKey = isEfdManagedTest ? getTestEfdKey(test) : testFqn
717
+ const testStatuses = testsToTestStatuses.get(testStatusKey) || []
425
718
 
426
719
  if (testStatuses.length === 0) {
427
- testsToTestStatuses.set(testFqn, [testStatus])
720
+ testsToTestStatuses.set(testStatusKey, [testStatus])
428
721
  if (test._ddIsNew && DYNAMIC_NAME_RE.test(getTestFullname(test))) {
429
722
  newTestsWithDynamicNames.add(`${getTestSuitePath(test._requireFile, rootDir)} › ${getTestFullname(test)}`)
430
723
  }
@@ -432,6 +725,17 @@ function testEndHandler ({
432
725
  testStatuses.push(testStatus)
433
726
  }
434
727
 
728
+ const testEfdKey = getTestEfdKey(test)
729
+ if (isEfdManagedTest && !test._ddIsEfdRetry && !efdRetryCountByTestKey.has(testEfdKey)) {
730
+ const testResult = results.at(-1)
731
+ const duration = testResult?.duration > 0 ? testResult.duration : performance.now() - test._ddStartTime
732
+ const retryCount = getEfdRetryCount(duration, getTestEfdSlowTestRetries(test))
733
+ setEfdRetryCountForTest(test, retryCount)
734
+ if (retryCount === 0) {
735
+ efdSlowAbortedTests.add(testEfdKey)
736
+ }
737
+ }
738
+
435
739
  const testProperties = getTestProperties(test)
436
740
 
437
741
  if (testProperties.attemptToFix) {
@@ -460,9 +764,10 @@ function testEndHandler ({
460
764
  }
461
765
 
462
766
  // Check if all EFD retries failed
463
- if (testStatuses.length === earlyFlakeDetectionNumRetries + 1 &&
767
+ const efdRetryCount = getEfdRetryCountForTest(test)
768
+ if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
464
769
  (test._ddIsNew || test._ddIsModified) &&
465
- test._ddIsEfdRetry &&
770
+ isEarlyFlakeDetectionEnabled &&
466
771
  testStatuses.every(status => status === 'fail')) {
467
772
  test._ddHasFailedAllRetries = true
468
773
  }
@@ -480,9 +785,6 @@ function testEndHandler ({
480
785
  if (shouldCreateTestSpan) {
481
786
  const testResult = results.at(-1)
482
787
  const testCtx = testToCtx.get(test)
483
- const isEfdManagedTest = (test._ddIsNew || test._ddIsModified) &&
484
- !test._ddIsAttemptToFix &&
485
- isEarlyFlakeDetectionEnabled
486
788
  const isAtrRetry = testResult?.retry > 0 &&
487
789
  isFlakyTestRetriesEnabled &&
488
790
  !test._ddIsAttemptToFix &&
@@ -497,6 +799,7 @@ function testEndHandler ({
497
799
  isAttemptToFix: test._ddIsAttemptToFix,
498
800
  hasFailedAllRetries: test._ddHasFailedAllRetries,
499
801
  hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
802
+ hasPassedAnyEfdAttempt: testStatuses.includes('pass'),
500
803
  testStatus,
501
804
  })
502
805
 
@@ -520,6 +823,7 @@ function testEndHandler ({
520
823
  isAtrRetry,
521
824
  isModified: test._ddIsModified,
522
825
  finalStatus,
826
+ earlyFlakeAbortReason: efdSlowAbortedTests.has(testEfdKey) ? 'slow' : undefined,
523
827
  ...testCtx.currentStore,
524
828
  })
525
829
  }
@@ -540,38 +844,7 @@ function testEndHandler ({
540
844
  .filter(currentTest => currentTest !== test)
541
845
  }
542
846
 
543
- if (shouldFinishTestSuite(testSuiteAbsolutePath)) {
544
- const skippedTests = remainingTestsByFile[testSuiteAbsolutePath]
545
- .filter(test => test.expectedStatus === 'skipped')
546
-
547
- for (const test of skippedTests) {
548
- const browserName = getBrowserNameFromProjects(projects, test)
549
- testSkipCh.publish({
550
- testName: getTestFullname(test),
551
- testSuiteAbsolutePath,
552
- testSourceFileAbsolutePath: test.location.file,
553
- testSourceLine: test.location.line,
554
- browserName,
555
- isNew: test._ddIsNew,
556
- isDisabled: test._ddIsDisabled,
557
- isModified: test._ddIsModified,
558
- isQuarantined: test._ddIsQuarantined,
559
- })
560
- }
561
- remainingTestsByFile[testSuiteAbsolutePath] = []
562
-
563
- const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath)
564
- let testSuiteStatus = 'pass'
565
- if (testStatuses.includes('fail')) {
566
- testSuiteStatus = 'fail'
567
- } else if (testStatuses.every(status => status === 'skip')) {
568
- testSuiteStatus = 'skip'
569
- }
570
-
571
- const suiteError = getTestSuiteError(testSuiteAbsolutePath)
572
- const testSuiteCtx = testSuiteToCtx.get(testSuiteAbsolutePath)
573
- testSuiteFinishCh.publish({ status: testSuiteStatus, error: suiteError, ...testSuiteCtx.currentStore })
574
- }
847
+ finishTestSuiteIfDone(testSuiteAbsolutePath, projects)
575
848
  }
576
849
 
577
850
  function dispatcherRunWrapper (run) {
@@ -581,6 +854,39 @@ function dispatcherRunWrapper (run) {
581
854
  }
582
855
  }
583
856
 
857
+ function deferEfdRetryGroups (testGroups) {
858
+ const groupsWithOriginalTests = []
859
+ const efdRetryOnlyGroups = []
860
+
861
+ for (const group of testGroups) {
862
+ const originalTests = []
863
+ const efdRetryTests = []
864
+
865
+ for (const test of group.tests) {
866
+ if (test._ddIsEfdRetry) {
867
+ efdRetryTests.push(test)
868
+ } else {
869
+ originalTests.push(test)
870
+ if (isTestEfdManaged(test)) {
871
+ efdScheduledOriginalTestKeys.add(getTestEfdKey(test))
872
+ }
873
+ }
874
+ }
875
+
876
+ if (efdRetryTests.length && originalTests.length) {
877
+ group.tests = [...originalTests, ...efdRetryTests]
878
+ }
879
+
880
+ if (originalTests.length) {
881
+ groupsWithOriginalTests.push(group)
882
+ } else {
883
+ efdRetryOnlyGroups.push(group)
884
+ }
885
+ }
886
+
887
+ return [...groupsWithOriginalTests, ...efdRetryOnlyGroups]
888
+ }
889
+
584
890
  function dispatcherRunWrapperNew (run) {
585
891
  return function (testGroups) {
586
892
  // Filter out disabled tests from testGroups before they get scheduled,
@@ -593,12 +899,17 @@ function dispatcherRunWrapperNew (run) {
593
899
  testGroups = testGroups.filter(group => group.tests.length > 0)
594
900
  }
595
901
 
902
+ if (isEarlyFlakeDetectionEnabled) {
903
+ testGroups = deferEfdRetryGroups(testGroups)
904
+ }
905
+
596
906
  if (!this._allTests) {
597
907
  // Removed in https://github.com/microsoft/playwright/commit/1e52c37b254a441cccf332520f60225a5acc14c7
598
908
  // Not available from >=1.44.0
599
909
  this._ddAllTests = testGroups.flatMap(g => g.tests)
600
910
  }
601
911
  remainingTestsByFile = getTestsBySuiteFromTestGroups(testGroups)
912
+ arguments[0] = testGroups
602
913
  return run.apply(this, arguments)
603
914
  }
604
915
  }
@@ -638,7 +949,6 @@ function dispatcherHook (dispatcherExport) {
638
949
  )
639
950
  }
640
951
  })
641
-
642
952
  return worker
643
953
  })
644
954
  return dispatcherExport
@@ -690,14 +1000,11 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
690
1000
  // above) and mark the execution final once the count reaches the expected total.
691
1001
  // This mirrors how ATF finality is detected and centralizes the decision in the
692
1002
  // main process, so workers only need to act on the _ddIsFinalExecution flag.
693
- const isEfdManagedTest = (test._ddIsNew || test._ddIsModified) &&
694
- !test._ddIsAttemptToFix &&
695
- isEarlyFlakeDetectionEnabled
1003
+ const isEfdManagedTest = isTestEfdManaged(test)
696
1004
  let isFinalExecution
697
1005
  if (isEfdManagedTest) {
698
- const testFqn = getTestFullyQualifiedName(test)
699
- const efdTestStatuses = testsToTestStatuses.get(testFqn) || []
700
- isFinalExecution = efdTestStatuses.length === earlyFlakeDetectionNumRetries + 1
1006
+ const efdTestStatuses = testsToTestStatuses.get(getTestEfdKey(test)) || []
1007
+ isFinalExecution = efdTestStatuses.length === getEfdRetryCountForTest(test) + 1
701
1008
  } else if (test._ddIsAttemptToFix) {
702
1009
  isFinalExecution = !!(test._ddHasPassedAttemptToFixRetries || test._ddHasFailedAttemptToFixRetries)
703
1010
  } else {
@@ -722,10 +1029,11 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
722
1029
  _ddIsModified: test._ddIsModified,
723
1030
  _ddIsFinalExecution: isFinalExecution,
724
1031
  _ddIsEfdManagedTest: isEfdManagedTest,
1032
+ _ddEarlyFlakeAbortReason: efdSlowAbortedTests.has(getTestEfdKey(test)) ? 'slow' : undefined,
1033
+ _ddHasPassedAnyEfdAttempt: (testsToTestStatuses.get(getTestEfdKey(test)) || []).includes('pass'),
725
1034
  },
726
1035
  })
727
1036
  })
728
-
729
1037
  return worker
730
1038
  })
731
1039
  return dispatcherExport
@@ -751,6 +1059,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
751
1059
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
752
1060
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
753
1061
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
1062
+ earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
754
1063
  earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
755
1064
  isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
756
1065
  flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
@@ -900,6 +1209,13 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
900
1209
  remainingTestsByFile = {}
901
1210
  quarantinedButNotAttemptToFixFqns = new Set()
902
1211
  testsReportedInGenerateSummary = new Set()
1212
+ efdManagedTestKeys.clear()
1213
+ efdRetryCountByTestKey.clear()
1214
+ efdRetryCountRequestsByTestKey.clear()
1215
+ efdRetryTestsById.clear()
1216
+ efdScheduledOriginalTestKeys.clear()
1217
+ efdStartedOriginalTestKeys.clear()
1218
+ efdSlowAbortedTests.clear()
903
1219
 
904
1220
  // TODO: we can trick playwright into thinking the session passed by returning
905
1221
  // 'passed' here. We might be able to use this for both EFD and Test Management tests.
@@ -1001,12 +1317,29 @@ addHook({
1001
1317
  * - we execute `applyRepeatEachIndex` for each of these cloned file suites
1002
1318
  * - we add the cloned file suites to the project suite
1003
1319
  */
1004
- function applyRetriesToTests (fileSuitesWithTestsToRetry, filterTest, tagsToApply, numRetries) {
1320
+ function applyRetriesToTests (
1321
+ fileSuitesWithTestsToRetry,
1322
+ filterTest,
1323
+ tagsToApply,
1324
+ numRetries,
1325
+ configureCopiedTest,
1326
+ getRetryRepeatEachIndex
1327
+ ) {
1005
1328
  for (const [fileSuite, projectSuite] of fileSuitesWithTestsToRetry.entries()) {
1006
1329
  for (let repeatEachIndex = 1; repeatEachIndex <= numRetries; repeatEachIndex++) {
1007
- const copyFileSuite = deepCloneSuite(fileSuite, filterTest, tagsToApply)
1008
- applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
1330
+ const copyFileSuite = deepCloneSuite(fileSuite, filterTest, tagsToApply, (copiedTest, originalTest) => {
1331
+ if (configureCopiedTest) {
1332
+ configureCopiedTest(copiedTest, originalTest, repeatEachIndex)
1333
+ }
1334
+ })
1335
+ const retryRepeatEachIndex = getRetryRepeatEachIndex
1336
+ ? getRetryRepeatEachIndex(fileSuite, projectSuite, repeatEachIndex, numRetries)
1337
+ : repeatEachIndex + 1
1338
+ applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, retryRepeatEachIndex)
1009
1339
  projectSuite._addSuite(copyFileSuite)
1340
+ for (const copiedTest of copyFileSuite.allTests()) {
1341
+ registerEfdRetryTest(copiedTest)
1342
+ }
1010
1343
  }
1011
1344
  }
1012
1345
  }
@@ -1091,6 +1424,7 @@ addHook({
1091
1424
  for (const impactedTest of impactedTests) {
1092
1425
  impactedTest._ddIsModified = true
1093
1426
  if (isEarlyFlakeDetectionEnabled && impactedTest.expectedStatus !== 'skipped') {
1427
+ markEfdManagedTest(impactedTest)
1094
1428
  const fileSuite = getSuiteType(impactedTest, 'file')
1095
1429
  if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) {
1096
1430
  fileSuitesWithImpactedTestsToProjects.set(fileSuite, getSuiteType(impactedTest, 'project'))
@@ -1106,7 +1440,12 @@ addHook({
1106
1440
  '_ddIsEfdRetry',
1107
1441
  (test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null),
1108
1442
  ],
1109
- earlyFlakeDetectionNumRetries
1443
+ getConfiguredEfdRetryCount(),
1444
+ (copiedTest, originalTest, retryIndex) => {
1445
+ markEfdRetryTest(copiedTest, retryIndex, originalTest)
1446
+ markEfdManagedTest(copiedTest)
1447
+ },
1448
+ getEfdRetryRepeatEachIndex
1110
1449
  )
1111
1450
  }
1112
1451
 
@@ -1130,6 +1469,7 @@ addHook({
1130
1469
  if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
1131
1470
  // Prevent ATR or `--retries` from retrying new tests if EFD is enabled
1132
1471
  newTest.retries = 0
1472
+ markEfdManagedTest(newTest)
1133
1473
  const fileSuite = getSuiteType(newTest, 'file')
1134
1474
  if (!fileSuitesWithNewTestsToProjects.has(fileSuite)) {
1135
1475
  fileSuitesWithNewTestsToProjects.set(fileSuite, getSuiteType(newTest, 'project'))
@@ -1141,7 +1481,12 @@ addHook({
1141
1481
  fileSuitesWithNewTestsToProjects,
1142
1482
  isNewTest,
1143
1483
  ['_ddIsNew', '_ddIsEfdRetry'],
1144
- earlyFlakeDetectionNumRetries
1484
+ getConfiguredEfdRetryCount(),
1485
+ (copiedTest, originalTest, retryIndex) => {
1486
+ markEfdRetryTest(copiedTest, retryIndex, originalTest)
1487
+ markEfdManagedTest(copiedTest)
1488
+ },
1489
+ getEfdRetryRepeatEachIndex
1145
1490
  )
1146
1491
  }
1147
1492
  }
@@ -1177,6 +1522,10 @@ addHook({
1177
1522
 
1178
1523
  // We add a new listener to `this.process`, which is represents the worker
1179
1524
  this.process.on('message', (message) => {
1525
+ if (message?.type === EFD_RETRY_COUNT_REQUEST) {
1526
+ sendEfdRetryCountToWorkerWhenAvailable(this.process, message.testId)
1527
+ return
1528
+ }
1180
1529
  // These messages are [code, payload]. The payload is test data
1181
1530
  if (Array.isArray(message) && message[0] === PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE) {
1182
1531
  workerReportCh.publish(message[1])
@@ -1235,9 +1584,15 @@ addHook({
1235
1584
  const stepInfoByStepId = {}
1236
1585
 
1237
1586
  shimmer.wrap(workerPackage.WorkerMain.prototype, '_runTest', _runTest => async function (test) {
1587
+ await waitForEfdRetryCount(test)
1588
+ if (shouldSkipEfdRetry(test)) {
1589
+ test._ddShouldSkipEfdRetry = true
1590
+ test.expectedStatus = 'skipped'
1591
+ }
1238
1592
  if (test.expectedStatus === 'skipped') {
1239
1593
  return _runTest.apply(this, arguments)
1240
1594
  }
1595
+ test._ddStartTime = performance.now()
1241
1596
  steps = []
1242
1597
 
1243
1598
  const {
@@ -1320,6 +1675,21 @@ addHook({
1320
1675
  await res
1321
1676
 
1322
1677
  const { status, error, annotations, retry, testId } = testInfo
1678
+ const testEfdKey = getTestEfdKey(test)
1679
+ const isEfdManagedTest = isTestEfdManaged(test)
1680
+ if (isEfdManagedTest && !test._ddIsEfdRetry && !efdRetryCountByTestKey.has(testEfdKey)) {
1681
+ const duration = test.results?.at(-1)?.duration > 0
1682
+ ? test.results.at(-1).duration
1683
+ : performance.now() - test._ddStartTime
1684
+ const retryCount = getEfdRetryCount(
1685
+ duration,
1686
+ getTestEfdSlowTestRetries(test)
1687
+ )
1688
+ setEfdRetryCountForTest(test, retryCount)
1689
+ if (retryCount === 0) {
1690
+ efdSlowAbortedTests.add(testEfdKey)
1691
+ }
1692
+ }
1323
1693
 
1324
1694
  // testInfo.errors could be better than "error",
1325
1695
  // which will only include timeout error (even though the test failed because of a different error)
@@ -1365,6 +1735,7 @@ addHook({
1365
1735
  isAttemptToFix: test._ddIsAttemptToFix,
1366
1736
  hasFailedAllRetries: test._ddHasFailedAllRetries,
1367
1737
  hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
1738
+ hasPassedAnyEfdAttempt: test._ddHasPassedAnyEfdAttempt,
1368
1739
  testStatus: STATUS_TO_TEST_STATUS[status],
1369
1740
  })
1370
1741
 
@@ -1388,6 +1759,7 @@ addHook({
1388
1759
  isModified: test._ddIsModified,
1389
1760
  onDone,
1390
1761
  finalStatus,
1762
+ earlyFlakeAbortReason: test._ddEarlyFlakeAbortReason,
1391
1763
  ...testCtx.currentStore,
1392
1764
  })
1393
1765