dd-trace 6.8.0 → 6.10.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 (209) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +9 -3
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  20. package/ci/test-optimization-validation/generated-files.js +38 -28
  21. package/ci/test-optimization-validation/generated-test-contract.js +8 -4
  22. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  23. package/ci/test-optimization-validation/literal-glob.js +52 -0
  24. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  25. package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
  26. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  27. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  28. package/ci/test-optimization-validation/package-check.js +94 -0
  29. package/ci/test-optimization-validation/plan-writer.js +183 -10
  30. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  31. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  32. package/ci/test-optimization-validation/redaction.js +18 -27
  33. package/ci/test-optimization-validation/report-writer.js +182 -48
  34. package/ci/test-optimization-validation/result-semantics.js +20 -3
  35. package/ci/test-optimization-validation/runner-command.js +40 -20
  36. package/ci/test-optimization-validation/runner-contract.js +79 -43
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  39. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  40. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  41. package/ci/test-optimization-validation/test-output.js +2 -1
  42. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  43. package/ci/vitest-no-worker-init-setup.mjs +140 -42
  44. package/index.d.ts +140 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +8 -5
  49. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  50. package/packages/datadog-esbuild/src/utils.js +16 -1
  51. package/packages/datadog-instrumentations/src/ai.js +61 -34
  52. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  53. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
  54. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  55. package/packages/datadog-instrumentations/src/cucumber.js +124 -47
  56. package/packages/datadog-instrumentations/src/dns.js +21 -2
  57. package/packages/datadog-instrumentations/src/electron.js +1 -0
  58. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  59. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  60. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  61. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  62. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +199 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +62 -0
  68. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  69. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  70. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  71. package/packages/datadog-instrumentations/src/jest.js +980 -234
  72. package/packages/datadog-instrumentations/src/mocha/main.js +108 -10
  73. package/packages/datadog-instrumentations/src/mocha/utils.js +66 -50
  74. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  75. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  76. package/packages/datadog-instrumentations/src/mysql.js +46 -16
  77. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  78. package/packages/datadog-instrumentations/src/next.js +155 -23
  79. package/packages/datadog-instrumentations/src/openai.js +14 -0
  80. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  81. package/packages/datadog-instrumentations/src/pg.js +209 -11
  82. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  83. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  84. package/packages/datadog-instrumentations/src/promise.js +3 -3
  85. package/packages/datadog-instrumentations/src/router.js +195 -19
  86. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +85 -13
  87. package/packages/datadog-instrumentations/src/vitest-main.js +528 -72
  88. package/packages/datadog-instrumentations/src/vitest-util.js +106 -12
  89. package/packages/datadog-instrumentations/src/vitest-worker.js +382 -40
  90. package/packages/datadog-instrumentations/src/webdriverio.js +212 -22
  91. package/packages/datadog-instrumentations/src/when.js +3 -3
  92. package/packages/datadog-instrumentations/src/ws.js +5 -1
  93. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  94. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  95. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
  96. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  97. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  98. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  99. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -47
  100. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  101. package/packages/datadog-plugin-electron/src/net.js +10 -4
  102. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  103. package/packages/datadog-plugin-http/src/client.js +1 -1
  104. package/packages/datadog-plugin-http2/src/client.js +1 -1
  105. package/packages/datadog-plugin-jest/src/index.js +6 -22
  106. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  107. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  108. package/packages/datadog-plugin-kafkajs/src/producer.js +5 -3
  109. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  110. package/packages/datadog-plugin-mocha/src/index.js +832 -4
  111. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  112. package/packages/datadog-plugin-next/src/index.js +51 -21
  113. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  114. package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
  115. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  116. package/packages/datadog-plugin-pg/src/index.js +66 -1
  117. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  118. package/packages/datadog-plugin-vitest/src/index.js +103 -27
  119. package/packages/datadog-plugin-ws/src/util.js +2 -1
  120. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  121. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  122. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  123. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  124. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  125. package/packages/dd-trace/src/appsec/handlers/http-request.js +228 -0
  126. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  127. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  128. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  129. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  130. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  131. package/packages/dd-trace/src/appsec/index.js +48 -476
  132. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  133. package/packages/dd-trace/src/carrier.js +356 -0
  134. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  135. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  136. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  137. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  138. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  139. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  140. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  141. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  142. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  143. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  144. package/packages/dd-trace/src/config/generated-config-types.d.ts +5 -0
  145. package/packages/dd-trace/src/config/helper.js +2 -0
  146. package/packages/dd-trace/src/config/index.js +16 -5
  147. package/packages/dd-trace/src/config/remote_config.js +1 -1
  148. package/packages/dd-trace/src/config/supported-configurations.json +18 -0
  149. package/packages/dd-trace/src/datastreams/context.js +6 -1
  150. package/packages/dd-trace/src/datastreams/manager.js +5 -1
  151. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  152. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  153. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  154. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  155. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  156. package/packages/dd-trace/src/guardrails/log.js +1 -10
  157. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  158. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  159. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  160. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  161. package/packages/dd-trace/src/llmobs/index.js +34 -2
  162. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  163. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  164. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  165. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  166. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  167. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  168. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  169. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  170. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  171. package/packages/dd-trace/src/llmobs/util.js +78 -0
  172. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  173. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  174. package/packages/dd-trace/src/log/channels.js +1 -9
  175. package/packages/dd-trace/src/log/levels.js +12 -0
  176. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  177. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  178. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  179. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  180. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  181. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  182. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  183. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  184. package/packages/dd-trace/src/plugin_manager.js +2 -0
  185. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  186. package/packages/dd-trace/src/plugins/index.js +1 -0
  187. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  188. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  189. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  190. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  191. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +3 -62
  192. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  193. package/packages/dd-trace/src/plugins/util/test.js +397 -74
  194. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  195. package/packages/dd-trace/src/plugins/util/web.js +76 -3
  196. package/packages/dd-trace/src/profiler.js +1 -1
  197. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  198. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  199. package/packages/dd-trace/src/proxy.js +1 -0
  200. package/packages/dd-trace/src/ritm.js +5 -0
  201. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  202. package/packages/dd-trace/src/sampling_rule.js +4 -2
  203. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  204. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  205. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  206. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  207. package/vendor/dist/pprof-format/index.js +1 -1
  208. package/version.js +10 -8
  209. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -1,13 +1,20 @@
1
1
  'use strict'
2
2
 
3
+ const path = require('node:path')
3
4
  const { performance } = require('node:perf_hooks')
5
+ const { fileURLToPath } = require('node:url')
6
+ const { isMainThread, parentPort } = require('node:worker_threads')
4
7
 
5
8
  const shimmer = require('../../datadog-shimmer')
9
+ const log = require('../../dd-trace/src/log')
10
+ const { getEfdRetryCountForDuration } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
6
11
  const {
7
12
  DYNAMIC_NAME_RE,
8
- getEfdRetryCount,
13
+ getTestSuitePath,
9
14
  recordAttemptToFixExecution,
10
15
  logAttemptToFixTestExecution,
16
+ VITEST_WORKER_EFD_SUITE_ADMISSION_REQUEST_CODE,
17
+ VITEST_WORKER_EFD_SUITE_ADMISSION_RESPONSE_CODE,
11
18
  } = require('../../dd-trace/src/plugins/util/test')
12
19
  const { getChannelPromise } = require('./helpers/channel')
13
20
  const { addHook } = require('./helpers/instrument')
@@ -27,16 +34,20 @@ const {
27
34
  getTypeTasks,
28
35
  getTestName,
29
36
  getProvidedContext,
37
+ realpath,
30
38
  isFlakyTestRetriesEnabledForTask,
31
39
  getVitestTestProperties,
32
40
  } = require('./vitest-util')
33
41
 
42
+ const EFD_SUITE_ADMISSION_TIMEOUT_MS = 5000
34
43
  const taskToCtx = new WeakMap()
35
44
  const taskToTestProperties = new WeakMap()
36
45
  const taskToStatuses = new WeakMap()
37
46
  const taskToReportedErrorCount = new WeakMap()
38
47
  const attemptToFixTaskToStatuses = new WeakMap()
39
48
  const fileToHasConcurrentTests = new WeakMap()
49
+ const fileToEfdSuiteAdmission = new WeakMap()
50
+ const pendingEfdSuiteAdmissionRequests = new Map()
40
51
  const originalHookFns = new WeakMap()
41
52
  const newTasks = new WeakSet()
42
53
  const dynamicNameTasks = new WeakSet()
@@ -52,10 +63,190 @@ const efdExecutionStartByTask = new WeakMap()
52
63
  const efdSkippedRetryResults = new WeakMap()
53
64
  const attemptToFixExecutions = new Map()
54
65
  const loggedAttemptToFixTests = new Set()
55
- const switchedStatuses = new WeakSet()
66
+ const switchedStatuses = new WeakMap()
56
67
  let vitestGetFn = null
57
68
  let vitestSetFn = null
58
69
  let vitestGetHooks = null
70
+ let preciseCoverageSession
71
+ let didInitializeEfdSuiteAdmissionTransport = false
72
+ let nextEfdSuiteAdmissionRequestId = 0
73
+ let sendEfdSuiteAdmissionMessage
74
+ let isPreciseCoverageUnavailable = false
75
+ let vitestCoverageSnapshot
76
+ const wrappedCoverageWorkerStates = new WeakSet()
77
+ const nonIsolatedCoverageFiles = new Set()
78
+
79
+ /**
80
+ * Sends a command to a Node.js inspector session.
81
+ *
82
+ * @param {import('node:inspector').Session} session
83
+ * @param {string} method
84
+ * @param {object} [params]
85
+ * @returns {Promise<object>}
86
+ */
87
+ function postInspectorCommand (session, method, params) {
88
+ return new Promise((resolve, reject) => {
89
+ session.post(method, params, (error, result) => {
90
+ if (error) {
91
+ reject(error)
92
+ } else {
93
+ resolve(result || {})
94
+ }
95
+ })
96
+ })
97
+ }
98
+
99
+ async function startPreciseCoverage () {
100
+ if (preciseCoverageSession) return true
101
+ if (isPreciseCoverageUnavailable) return false
102
+
103
+ let session
104
+ try {
105
+ const inspector = require('node:inspector')
106
+ session = new inspector.Session()
107
+ session.connect()
108
+ await postInspectorCommand(session, 'Profiler.enable')
109
+ await postInspectorCommand(session, 'Profiler.startPreciseCoverage', {
110
+ callCount: false,
111
+ detailed: false,
112
+ })
113
+ preciseCoverageSession = session
114
+ return true
115
+ } catch (error) {
116
+ isPreciseCoverageUnavailable = true
117
+ try {
118
+ session?.disconnect()
119
+ } catch {}
120
+ log.warn('Could not start Vitest TIA code coverage: %s', error?.message)
121
+ return false
122
+ }
123
+ }
124
+
125
+ function getCoverageFilename (url) {
126
+ if (!url) return
127
+
128
+ if (url.startsWith('file://')) {
129
+ try {
130
+ return fileURLToPath(url)
131
+ } catch {
132
+ return
133
+ }
134
+ }
135
+
136
+ if (path.isAbsolute(url)) return url
137
+ }
138
+
139
+ function isFileInRepository (filename, repositoryRoot) {
140
+ const relativeFilename = path.relative(repositoryRoot, filename)
141
+ return relativeFilename !== '..' &&
142
+ !relativeFilename.startsWith(`..${path.sep}`) &&
143
+ !path.isAbsolute(relativeFilename) &&
144
+ !relativeFilename.startsWith(`node_modules${path.sep}`) &&
145
+ !relativeFilename.includes(`${path.sep}node_modules${path.sep}`)
146
+ }
147
+
148
+ function isV8ScriptCovered (scriptCoverage) {
149
+ for (const functionCoverage of scriptCoverage.functions) {
150
+ for (const range of functionCoverage.ranges) {
151
+ if (range.count > 0) return true
152
+ }
153
+ }
154
+ return false
155
+ }
156
+
157
+ function getCoveredFilesFromV8Result (coverage, repositoryRoot) {
158
+ const coveredFiles = []
159
+ const scriptCoverageResults = coverage?.result
160
+ if (scriptCoverageResults) {
161
+ for (const scriptCoverage of scriptCoverageResults) {
162
+ if (!isV8ScriptCovered(scriptCoverage)) continue
163
+
164
+ const coverageFilename = getCoverageFilename(scriptCoverage.url)
165
+ if (!coverageFilename) continue
166
+
167
+ const filename = realpath(coverageFilename)
168
+ if (isFileInRepository(filename, repositoryRoot)) {
169
+ coveredFiles.push(filename)
170
+ }
171
+ }
172
+ }
173
+ return coveredFiles
174
+ }
175
+
176
+ function getVitestCoverageOptions () {
177
+ return globalThis.__vitest_worker__?.config?.coverage
178
+ }
179
+
180
+ /**
181
+ * Check whether the current Vitest worker reuses its module cache across test suites.
182
+ *
183
+ * @returns {boolean}
184
+ */
185
+ function isNonIsolatedRun () {
186
+ const config = globalThis.__vitest_worker__?.config
187
+ if (config?.isolate === false) return true
188
+
189
+ return config?.poolOptions?.[config.pool]?.isolate === false
190
+ }
191
+
192
+ /**
193
+ * Conservatively include files covered by earlier suites when Vitest reuses its module cache.
194
+ *
195
+ * @param {string[] | undefined} coverageFiles
196
+ * @returns {string[] | undefined}
197
+ */
198
+ function includePreviouslyCoveredFiles (coverageFiles) {
199
+ if (!coverageFiles || !isNonIsolatedRun()) return coverageFiles
200
+
201
+ // TODO: Track module cache hits per suite instead; cumulative coverage can under-skip with isolate:false.
202
+ for (const filename of coverageFiles) {
203
+ nonIsolatedCoverageFiles.add(filename)
204
+ }
205
+ return [...nonIsolatedCoverageFiles]
206
+ }
207
+
208
+ function usesVitestV8CoverageSnapshot (coverageOptions) {
209
+ return coverageOptions?.enabled === true &&
210
+ coverageOptions.provider === 'v8'
211
+ }
212
+
213
+ async function getPreciseCoverageFiles (repositoryRoot) {
214
+ if (!await startPreciseCoverage()) return
215
+
216
+ try {
217
+ const coverage = await postInspectorCommand(preciseCoverageSession, 'Profiler.takePreciseCoverage')
218
+ return getCoveredFilesFromV8Result(coverage, repositoryRoot)
219
+ } catch (error) {
220
+ isPreciseCoverageUnavailable = true
221
+ try {
222
+ preciseCoverageSession.disconnect()
223
+ } catch {}
224
+ preciseCoverageSession = undefined
225
+ log.warn('Could not collect Vitest TIA code coverage: %s', error?.message)
226
+ }
227
+ }
228
+
229
+ function getVitestCoverageFiles (repositoryRoot) {
230
+ return getCoveredFilesFromV8Result(vitestCoverageSnapshot, repositoryRoot)
231
+ }
232
+
233
+ function wrapVitestCoverageRpc () {
234
+ const workerState = globalThis.__vitest_worker__
235
+ if (!workerState?.rpc || wrappedCoverageWorkerStates.has(workerState)) return
236
+
237
+ wrappedCoverageWorkerStates.add(workerState)
238
+ workerState.rpc = new Proxy(workerState.rpc, {
239
+ get (target, property, receiver) {
240
+ const value = Reflect.get(target, property, receiver)
241
+ if (property !== 'onAfterSuiteRun' || typeof value !== 'function') return value
242
+
243
+ return function (metadata) {
244
+ vitestCoverageSnapshot = metadata?.coverage
245
+ return value.apply(this, arguments)
246
+ }
247
+ },
248
+ })
249
+ }
59
250
 
60
251
  function waitForHitProbe () {
61
252
  const promises = {}
@@ -166,6 +357,123 @@ function hasConcurrentTests (file) {
166
357
  return hasConcurrent
167
358
  }
168
359
 
360
+ /**
361
+ * Returns whether a collected Vitest file contains a runnable new test eligible for EFD.
362
+ *
363
+ * @param {{ filepath: string, tasks?: object[] }} file
364
+ * @param {object} providedContext
365
+ * @returns {boolean}
366
+ */
367
+ function hasRunnableNewTest (file, providedContext) {
368
+ for (const task of getTypeTasks(file.tasks)) {
369
+ if (task.mode === 'skip' || task.mode === 'todo') continue
370
+
371
+ const testProperties = getVitestTestProperties(providedContext, file.filepath, getTestName(task))
372
+ if (testProperties.isNew && !testProperties.isAttemptToFix && !testProperties.isDisabled) {
373
+ return true
374
+ }
375
+ }
376
+ return false
377
+ }
378
+
379
+ /**
380
+ * Resolves and removes one pending EFD suite admission request.
381
+ *
382
+ * @param {number} requestId
383
+ * @param {boolean} allowed
384
+ * @returns {void}
385
+ */
386
+ function finishEfdSuiteAdmissionRequest (requestId, allowed) {
387
+ const request = pendingEfdSuiteAdmissionRequests.get(requestId)
388
+ if (!request) return
389
+
390
+ clearTimeout(request.timeout)
391
+ pendingEfdSuiteAdmissionRequests.delete(requestId)
392
+ request.resolve(allowed)
393
+ }
394
+
395
+ /**
396
+ * Handles an EFD suite admission response from the Vitest main process.
397
+ *
398
+ * @param {unknown} message
399
+ * @returns {void}
400
+ */
401
+ function handleEfdSuiteAdmissionResponse (message) {
402
+ if (!Array.isArray(message) || message[0] !== VITEST_WORKER_EFD_SUITE_ADMISSION_RESPONSE_CODE) return
403
+
404
+ const { allowed, requestId } = message[1] || {}
405
+ if (Number.isSafeInteger(requestId)) {
406
+ finishEfdSuiteAdmissionRequest(requestId, allowed === true)
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Returns the child-process or worker-thread sender used by the current Vitest worker.
412
+ *
413
+ * @returns {((message: unknown) => void)|undefined}
414
+ */
415
+ function getEfdSuiteAdmissionSender () {
416
+ if (didInitializeEfdSuiteAdmissionTransport) return sendEfdSuiteAdmissionMessage
417
+ didInitializeEfdSuiteAdmissionTransport = true
418
+
419
+ if (typeof process.send === 'function') {
420
+ process.on('message', handleEfdSuiteAdmissionResponse)
421
+ sendEfdSuiteAdmissionMessage = process.send.bind(process)
422
+ } else if (!isMainThread && parentPort) {
423
+ parentPort.on('message', handleEfdSuiteAdmissionResponse)
424
+ sendEfdSuiteAdmissionMessage = parentPort.postMessage.bind(parentPort)
425
+ }
426
+ return sendEfdSuiteAdmissionMessage
427
+ }
428
+
429
+ /**
430
+ * Requests permission from the Vitest main process to schedule EFD retries for one suite.
431
+ *
432
+ * @param {string} testSuite
433
+ * @param {boolean} hasNewTest
434
+ * @returns {Promise<boolean>}
435
+ */
436
+ function requestEfdSuiteAdmission (testSuite, hasNewTest) {
437
+ const sendMessage = getEfdSuiteAdmissionSender()
438
+ if (!sendMessage) return Promise.resolve(false)
439
+
440
+ const requestId = ++nextEfdSuiteAdmissionRequestId
441
+ return new Promise(resolve => {
442
+ const timeout = setTimeout(
443
+ () => finishEfdSuiteAdmissionRequest(requestId, false),
444
+ EFD_SUITE_ADMISSION_TIMEOUT_MS
445
+ )
446
+ timeout.unref?.()
447
+ pendingEfdSuiteAdmissionRequests.set(requestId, { resolve, timeout })
448
+
449
+ try {
450
+ sendMessage([VITEST_WORKER_EFD_SUITE_ADMISSION_REQUEST_CODE, { hasNewTest, requestId, testSuite }])
451
+ } catch {
452
+ finishEfdSuiteAdmissionRequest(requestId, false)
453
+ }
454
+ })
455
+ }
456
+
457
+ /**
458
+ * Requests a single EFD admission decision for all tests collected in a Vitest file.
459
+ *
460
+ * @param {{ file: { filepath: string, tasks?: object[] } }} task
461
+ * @param {object} providedContext
462
+ * @param {string|undefined} testSuite
463
+ * @returns {Promise<boolean>}
464
+ */
465
+ function isEfdSuiteAdmissionAllowed (task, providedContext, testSuite) {
466
+ let admission = fileToEfdSuiteAdmission.get(task.file)
467
+ if (admission) return admission
468
+
469
+ admission = requestEfdSuiteAdmission(
470
+ testSuite || task.file.filepath,
471
+ hasRunnableNewTest(task.file, providedContext)
472
+ )
473
+ fileToEfdSuiteAdmission.set(task.file, admission)
474
+ return admission
475
+ }
476
+
169
477
  /**
170
478
  * Gets the task associated with a Vitest hook invocation.
171
479
  *
@@ -201,14 +509,14 @@ function wrapSuiteHookFn (hookType, fn, fallbackTask) {
201
509
  function wrapVitestTestRunner (VitestTestRunner) {
202
510
  // `onBeforeRunTask` is run before any repetition or attempt is run
203
511
  // `onBeforeRunTask` is an async function
204
- shimmer.wrap(VitestTestRunner.prototype, 'onBeforeRunTask', onBeforeRunTask => function (task) {
512
+ shimmer.wrap(VitestTestRunner.prototype, 'onBeforeRunTask', onBeforeRunTask => async function (task) {
205
513
  const testName = getTestName(task)
206
514
 
207
515
  const providedContext = getProvidedContext()
208
516
  const {
209
517
  isEarlyFlakeDetectionEnabled,
210
518
  isKnownTestsEnabled,
211
- numRepeats,
519
+ earlyFlakeDetectionRetryPolicy,
212
520
  isTestManagementTestsEnabled,
213
521
  testManagementAttemptToFixRetries,
214
522
  isImpactedTestsEnabled,
@@ -244,35 +552,38 @@ function wrapVitestTestRunner (VitestTestRunner) {
244
552
  }
245
553
 
246
554
  if (isImpactedTestsEnabled && testProperties.isModified) {
247
- if (isEarlyFlakeDetectionEnabled) {
248
- efdRetryTasks.add(task)
249
- disableFrameworkRetries(task)
250
- task.repeats = numRepeats
251
- }
252
555
  modifiedTasks.add(task)
253
- taskToStatuses.set(task, [])
254
556
  }
255
557
 
256
558
  if (isKnownTestsEnabled && testProperties.isNew && !attemptToFixTasks.has(task)) {
257
- if (isEarlyFlakeDetectionEnabled && !modifiedTasks.has(task)) {
258
- efdRetryTasks.add(task)
259
- disableFrameworkRetries(task)
260
- task.repeats = numRepeats
261
- }
262
559
  newTasks.add(task)
263
- taskToStatuses.set(task, [])
264
560
  if (DYNAMIC_NAME_RE.test(testName)) {
265
561
  dynamicNameTasks.add(task)
266
562
  }
267
563
  }
268
564
 
565
+ const isEfdCandidate = isEarlyFlakeDetectionEnabled &&
566
+ !attemptToFixTasks.has(task) &&
567
+ !disabledTasks.has(task) &&
568
+ (modifiedTasks.has(task) || newTasks.has(task))
569
+ if (isEfdCandidate) {
570
+ const isAdmissionAllowed = !providedContext.isEfdSuiteAdmissionEnabled ||
571
+ await isEfdSuiteAdmissionAllowed(task, providedContext, testProperties.testSuite)
572
+ if (isAdmissionAllowed) {
573
+ efdRetryTasks.add(task)
574
+ disableFrameworkRetries(task)
575
+ task.repeats = earlyFlakeDetectionRetryPolicy.schedulingRetryCount
576
+ taskToStatuses.set(task, [])
577
+ }
578
+ }
579
+
269
580
  return onBeforeRunTask.apply(this, arguments)
270
581
  })
271
582
 
272
583
  // `onAfterRunTask` is run after all repetitions or attempts are run
273
584
  // `onAfterRunTask` is an async function
274
585
  shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => function (task) {
275
- const { isEarlyFlakeDetectionEnabled, isTestManagementTestsEnabled } = getProvidedContext()
586
+ const { isTestManagementTestsEnabled } = getProvidedContext()
276
587
 
277
588
  if (isTestManagementTestsEnabled) {
278
589
  const isAttemptingToFix = attemptToFixTasks.has(task)
@@ -281,25 +592,25 @@ function wrapVitestTestRunner (VitestTestRunner) {
281
592
  if (isAttemptingToFix) {
282
593
  const statuses = attemptToFixTaskToStatuses.get(task)
283
594
  if (task.result.state === 'pass' && statuses?.includes('fail')) {
284
- switchedStatuses.add(task)
595
+ switchedStatuses.set(task, task.result.state)
285
596
  task.result.state = 'fail'
286
597
  }
287
598
  }
288
599
 
289
600
  if (!isAttemptingToFix && isQuarantined) {
290
601
  if (task.result.state === 'fail') {
291
- switchedStatuses.add(task)
602
+ switchedStatuses.set(task, task.result.state)
292
603
  }
293
604
  task.result.state = 'pass'
294
605
  }
295
606
  }
296
607
 
297
- if (isEarlyFlakeDetectionEnabled && taskToStatuses.has(task) && !attemptToFixTasks.has(task)) {
608
+ if (efdRetryTasks.has(task)) {
298
609
  const statuses = taskToStatuses.get(task)
299
610
  // If the test has passed at least once, we consider it passed
300
611
  if (statuses.includes('pass')) {
301
612
  if (task.result.state === 'fail') {
302
- switchedStatuses.add(task)
613
+ switchedStatuses.set(task, task.result.state)
303
614
  }
304
615
  task.result.state = 'pass'
305
616
  }
@@ -319,9 +630,8 @@ function wrapVitestTestRunner (VitestTestRunner) {
319
630
  const providedContext = getProvidedContext()
320
631
  const {
321
632
  isKnownTestsEnabled,
322
- isEarlyFlakeDetectionEnabled,
323
633
  isDiEnabled,
324
- slowTestRetries,
634
+ earlyFlakeDetectionRetryPolicy,
325
635
  } = providedContext
326
636
 
327
637
  if (isKnownTestsEnabled) {
@@ -330,14 +640,14 @@ function wrapVitestTestRunner (VitestTestRunner) {
330
640
 
331
641
  const { retry: numAttempt, repeats: numRepetition } = retryInfo
332
642
  const isFailedTestReplayAllowed = !hasConcurrentTests(task.file)
333
- const isEfdManagedTask = isEarlyFlakeDetectionEnabled && taskToStatuses.has(task) && !attemptToFixTasks.has(task)
643
+ const isEfdManagedTask = efdRetryTasks.has(task)
334
644
 
335
645
  if (isEfdManagedTask && numRepetition > 0 && !efdDeterminedRetries.has(task)) {
336
646
  const previousExecutionStart = efdExecutionStartByTask.get(task)
337
647
  const duration = previousExecutionStart === undefined
338
648
  ? task.result?.duration ?? 0
339
649
  : performance.now() - previousExecutionStart
340
- const retryCount = getEfdRetryCount(duration, slowTestRetries)
650
+ const retryCount = getEfdRetryCountForDuration(duration, earlyFlakeDetectionRetryPolicy)
341
651
  efdDeterminedRetries.set(task, retryCount)
342
652
  task.repeats = retryCount
343
653
  if (retryCount === 0) {
@@ -392,8 +702,9 @@ function wrapVitestTestRunner (VitestTestRunner) {
392
702
 
393
703
  const lastExecutionStatus = task.result.state
394
704
  const isAtf = attemptToFixTasks.has(task)
395
- const shouldTrackStatuses = isEarlyFlakeDetectionEnabled || isAtf
396
- const shouldFlipStatus = isEarlyFlakeDetectionEnabled || isAtf
705
+ const isEfd = efdRetryTasks.has(task)
706
+ const shouldTrackStatuses = isEfd || isAtf
707
+ const shouldFlipStatus = isEfd || isAtf
397
708
  const statuses = isAtf ? attemptToFixTaskToStatuses.get(task) : taskToStatuses.get(task)
398
709
 
399
710
  // These clauses handle task.repeats, whether EFD is enabled or not
@@ -515,9 +826,8 @@ function wrapVitestTestRunner (VitestTestRunner) {
515
826
  const result = await onAfterTryTask.apply(this, arguments)
516
827
 
517
828
  const {
518
- isEarlyFlakeDetectionEnabled,
519
829
  testManagementAttemptToFixRetries,
520
- slowTestRetries,
830
+ earlyFlakeDetectionRetryPolicy,
521
831
  } = getProvidedContext()
522
832
 
523
833
  const status = getVitestTestStatus(task, retryInfo.retry)
@@ -537,15 +847,13 @@ function wrapVitestTestRunner (VitestTestRunner) {
537
847
  }
538
848
 
539
849
  if (
540
- isEarlyFlakeDetectionEnabled &&
850
+ efdRetryTasks.has(task) &&
541
851
  (retryInfo.repeats ?? 0) === 0 &&
542
- taskToStatuses.has(task) &&
543
- !attemptToFixTasks.has(task) &&
544
852
  !efdDeterminedRetries.has(task)
545
853
  ) {
546
854
  const executionStart = efdExecutionStartByTask.get(task)
547
855
  const duration = executionStart === undefined ? task.result?.duration ?? 0 : performance.now() - executionStart
548
- const retryCount = getEfdRetryCount(duration, slowTestRetries)
856
+ const retryCount = getEfdRetryCountForDuration(duration, earlyFlakeDetectionRetryPolicy)
549
857
  efdDeterminedRetries.set(task, retryCount)
550
858
  task.repeats = retryCount
551
859
  if (retryCount === 0) {
@@ -649,6 +957,19 @@ addHook({
649
957
  // From >=3.0.1, the first arguments changes from a string to an object containing the filepath
650
958
  const testSuiteAbsolutePath = testPaths[0]?.filepath || testPaths[0]
651
959
  const providedContext = getProvidedContext()
960
+ const repositoryRoot = providedContext.repositoryRoot || process.cwd()
961
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, repositoryRoot)
962
+ const coverageOptions = getVitestCoverageOptions()
963
+ const shouldUseVitestCoverage = usesVitestV8CoverageSnapshot(coverageOptions)
964
+ const coverageLibrary = 'v8'
965
+ vitestCoverageSnapshot = undefined
966
+ if (providedContext.isCodeCoverageEnabled) {
967
+ if (shouldUseVitestCoverage) {
968
+ wrapVitestCoverageRpc()
969
+ } else {
970
+ await startPreciseCoverage()
971
+ }
972
+ }
652
973
 
653
974
  const testSuiteCtx = {
654
975
  testSuiteAbsolutePath,
@@ -656,8 +977,13 @@ addHook({
656
977
  testSessionId: providedContext.testSessionId,
657
978
  testModuleId: providedContext.testModuleId,
658
979
  testCommand: providedContext.testCommand,
659
- repositoryRoot: providedContext.repositoryRoot,
980
+ repositoryRoot,
660
981
  codeOwnersEntries: providedContext.codeOwnersEntries,
982
+ isCodeCoverageEnabled: providedContext.isCodeCoverageEnabled,
983
+ coverageLibrary,
984
+ itrCorrelationId: providedContext.itrCorrelationId,
985
+ isUnskippable: providedContext.unskippableSuites?.[testSuite] === true,
986
+ isForcedToRun: providedContext.forcedToRunSuites?.[testSuite] === true,
661
987
  }
662
988
  testSuiteStartCh.runStores(testSuiteCtx, () => {})
663
989
  const startTestsResponse = await startTests.apply(this, arguments)
@@ -671,14 +997,15 @@ addHook({
671
997
  const { result } = task
672
998
  // We have to trick vitest into thinking that the test has passed
673
999
  // but we want to report it as failed if it did fail
674
- const isSwitchedStatus = switchedStatuses.has(task)
1000
+ const switchedStatus = switchedStatuses.get(task)
1001
+ const isSwitchedStatus = switchedStatus !== undefined
675
1002
 
676
1003
  if (result) {
677
1004
  const { state, duration, errors } = result
678
1005
  const testError = getCurrentAttemptTestError(task, errors)
679
1006
  if (attemptToFixTasks.has(task)) {
680
- const status = getFinalAttemptToFixStatus(task, state, isSwitchedStatus, testCtx)
681
- recordFinalAttemptToFixExecution(task, status, providedContext)
1007
+ const attemptToFixStatus = getFinalAttemptToFixStatus(task, state, isSwitchedStatus, testCtx)
1008
+ recordFinalAttemptToFixExecution(task, attemptToFixStatus, providedContext)
682
1009
  }
683
1010
 
684
1011
  if (state === 'skip') { // programmatic skip
@@ -686,7 +1013,9 @@ addHook({
686
1013
  testName: getTestName(task),
687
1014
  testSuiteAbsolutePath: task.file.filepath,
688
1015
  isNew: newTasks.has(task),
1016
+ isAttemptToFix: attemptToFixTasks.has(task),
689
1017
  isDisabled: disabledTasks.has(task),
1018
+ isQuarantined: quarantinedTasks.has(task),
690
1019
  })
691
1020
  } else if (state === 'pass' && !isSwitchedStatus) {
692
1021
  if (testCtx) {
@@ -718,11 +1047,11 @@ addHook({
718
1047
  }
719
1048
 
720
1049
  // Check if all EFD retries failed
721
- const isEfdRetry =
722
- providedContext.isEarlyFlakeDetectionEnabled && (newTasks.has(task) || modifiedTasks.has(task))
1050
+ const isEfdRetry = efdRetryTasks.has(task)
723
1051
  if (isEfdRetry) {
724
1052
  const statuses = taskToStatuses.get(task)
725
- const efdRetryCount = efdDeterminedRetries.get(task) ?? providedContext.numRepeats
1053
+ const efdRetryCount = efdDeterminedRetries.get(task) ??
1054
+ providedContext.earlyFlakeDetectionRetryPolicy.schedulingRetryCount
726
1055
  // statuses only includes repetitions (not the initial run), so we check against retry count (not +1)
727
1056
  if (efdRetryCount > 0 && statuses && statuses.length === efdRetryCount &&
728
1057
  statuses.every(status => status === 'fail')) {
@@ -783,7 +1112,9 @@ addHook({
783
1112
  testName: getTestName(task),
784
1113
  testSuiteAbsolutePath: task.file.filepath,
785
1114
  isNew: newTasks.has(task),
1115
+ isAttemptToFix: attemptToFixTasks.has(task),
786
1116
  isDisabled: disabledTasks.has(task),
1117
+ isQuarantined: quarantinedTasks.has(task),
787
1118
  })
788
1119
  }
789
1120
  }
@@ -807,8 +1138,19 @@ addHook({
807
1138
  testSuiteErrorCh.runStores(testSuiteCtx, () => {})
808
1139
  }
809
1140
 
1141
+ let coverageFiles
1142
+ if (providedContext.isCodeCoverageEnabled) {
1143
+ const currentCoverageFiles = shouldUseVitestCoverage
1144
+ ? getVitestCoverageFiles(repositoryRoot)
1145
+ : await getPreciseCoverageFiles(repositoryRoot)
1146
+ coverageFiles = includePreviouslyCoveredFiles(currentCoverageFiles)
1147
+ }
1148
+
810
1149
  await getChannelPromise(testSuiteFinishCh, {
811
1150
  status: testSuiteResult.state,
1151
+ coverageFiles,
1152
+ coverageLibrary,
1153
+ testSuiteAbsolutePath,
812
1154
  ...testSuiteCtx.currentStore,
813
1155
  })
814
1156