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,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const METHODS = [...require('http').METHODS.map(v => v.toLowerCase()), 'all']
4
+ const ROUTE_RESOLUTION_FAILED = Symbol('routeResolutionFailed')
4
5
  const shimmer = require('../../datadog-shimmer')
5
6
  const { addHook, channel, createErrorPublisher } = require('./helpers/instrument')
6
7
  const { getCompileToRegexp } = require('./path-to-regexp')
@@ -35,6 +36,7 @@ function isFastSlash (layer, matchers) {
35
36
  * @param {{ handle: Function, name?: string, path?: string,
36
37
  * regexp?: { fast_star?: boolean, fast_slash?: boolean } }} layer
37
38
  * @param {Array<{ path?: string, regex?: RegExp }> & { hasStarPath?: boolean, hasSlashPath?: boolean }} matchers
39
+ * @returns {void}
38
40
  */
39
41
  function annotateLayer (layer, matchers) {
40
42
  const handle = layer.handle
@@ -71,6 +73,25 @@ function resolveLayerRoute (meta, layer) {
71
73
  }
72
74
  }
73
75
 
76
+ /**
77
+ * Preserve the host Layer's error boundary while resolving a route outside its
78
+ * dispatch method. This path runs only for multi-pattern layers.
79
+ *
80
+ * @param {{ captureRoute?: string, needMultiMatch: boolean,
81
+ * matchers: Array<{ path?: string, regex?: RegExp }> }} meta
82
+ * @param {{ path?: string }} layer
83
+ * @param {Function} next
84
+ * @returns {string | undefined | typeof ROUTE_RESOLUTION_FAILED}
85
+ */
86
+ function resolveLayerRouteOrForwardError (meta, layer, next) {
87
+ try {
88
+ return resolveLayerRoute(meta, layer)
89
+ } catch (error) {
90
+ next(error)
91
+ return ROUTE_RESOLUTION_FAILED
92
+ }
93
+ }
94
+
74
95
  /**
75
96
  * Build the request/error dispatch wrappers for one host (`express` / `router`).
76
97
  * They wrap the layer's prototype dispatch and read the side-table metadata, so
@@ -79,6 +100,11 @@ function resolveLayerRoute (meta, layer) {
79
100
  * span is published only for the layer the host actually runs.
80
101
  *
81
102
  * @param {string} name Channel namespace (`apm:<name>:middleware:*`).
103
+ * @returns {{
104
+ * wrapLayerRequest: (originalRequest: Function) => Function,
105
+ * wrapLayerError: (originalError: Function) => Function,
106
+ * wrapLegacyHandle: (layer: object, original: Function, guardRepeatedNext?: boolean) => Function
107
+ * }}
82
108
  */
83
109
  function createLayerDispatchWrappers (name) {
84
110
  const enterChannel = channel(`apm:${name}:middleware:enter`)
@@ -128,6 +154,10 @@ function createLayerDispatchWrappers (name) {
128
154
  // `wrappedNext` through captures both without a tracer-side try/catch; only
129
155
  // `exit` needs the `finally`. express 4's native dispatch converts only the
130
156
  // synchronous throw — exactly what the pre-refactor handle wrap caught.
157
+ /**
158
+ * @param {Function} originalRequest
159
+ * @returns {Function}
160
+ */
131
161
  function wrapLayerRequest (originalRequest) {
132
162
  return function (req, res, next) {
133
163
  if (!enterChannel.hasSubscribers) return originalRequest.call(this, req, res, next)
@@ -135,8 +165,14 @@ function createLayerDispatchWrappers (name) {
135
165
  const meta = getLayerMeta(this)
136
166
  if (meta === undefined || this.handle.length > 3) return originalRequest.call(this, req, res, next)
137
167
 
168
+ let route = meta.captureRoute
169
+ if (meta.needMultiMatch) {
170
+ route = resolveLayerRouteOrForwardError(meta, this, next)
171
+ if (route === ROUTE_RESOLUTION_FAILED) return
172
+ }
173
+
138
174
  const wrappedNext = typeof next === 'function' ? wrapNext(req, meta.name, next) : next
139
- enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, this), layer: this })
175
+ enterChannel.publish({ name: meta.name, req, route, layer: this })
140
176
 
141
177
  try {
142
178
  return originalRequest.call(this, req, res, wrappedNext)
@@ -146,6 +182,10 @@ function createLayerDispatchWrappers (name) {
146
182
  }
147
183
  }
148
184
 
185
+ /**
186
+ * @param {Function} originalError
187
+ * @returns {Function}
188
+ */
149
189
  function wrapLayerError (originalError) {
150
190
  return function (error, req, res, next) {
151
191
  if (!enterChannel.hasSubscribers) return originalError.call(this, error, req, res, next)
@@ -153,8 +193,14 @@ function createLayerDispatchWrappers (name) {
153
193
  const meta = getLayerMeta(this)
154
194
  if (meta === undefined || this.handle.length !== 4) return originalError.call(this, error, req, res, next)
155
195
 
196
+ let route = meta.captureRoute
197
+ if (meta.needMultiMatch) {
198
+ route = resolveLayerRouteOrForwardError(meta, this, next)
199
+ if (route === ROUTE_RESOLUTION_FAILED) return
200
+ }
201
+
156
202
  const wrappedNext = typeof next === 'function' ? wrapNext(req, meta.name, next) : next
157
- enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, this), layer: this })
203
+ enterChannel.publish({ name: meta.name, req, route, layer: this })
158
204
 
159
205
  try {
160
206
  return originalError.call(this, error, req, res, wrappedNext)
@@ -164,36 +210,163 @@ function createLayerDispatchWrappers (name) {
164
210
  }
165
211
  }
166
212
 
167
- // express <4.6.0 has no `Layer` prototype dispatch: the router invokes
168
- // `layer.handle` directly and routes errors by its arity. There the handle is
169
- // replaced in place, with the arity preserved so the host still routes
170
- // correctly. Newer express, express 5, and the router package keep `handle`
171
- // pristine and are traced through the prototype wraps above.
172
- function wrapLegacyHandle (layer, original) {
213
+ // express <4.6.0 dispatches `layer.handle` directly, so its replacement must preserve arity.
214
+ /**
215
+ * @param {object} layer
216
+ * @param {Function} original
217
+ * @returns {Function}
218
+ */
219
+ function wrapNativeLegacyRequestHandle (layer, original) {
220
+ const meta = getLayerMeta(layer)
221
+ const { name, captureRoute, needMultiMatch } = meta
222
+ return shimmer.wrapFunction(original, inner => function (req, res, next) {
223
+ if (!enterChannel.hasSubscribers) return inner.call(this, req, res, next)
224
+
225
+ let calls = 0
226
+ if (typeof next === 'function') {
227
+ next = shimmer.wrapCallback(next, originalNext => function next (error) {
228
+ calls++
229
+ if (calls === 1) {
230
+ if (error && error !== 'route' && error !== 'router') {
231
+ publishError({ req, error })
232
+ }
233
+
234
+ nextChannel.publish({ req })
235
+ finishChannel.publish({ req })
236
+ } else if (calls === 2) {
237
+ repeatChannel.publish({ req, name, error })
238
+ }
239
+
240
+ originalNext.apply(this, arguments)
241
+ })
242
+ }
243
+
244
+ const route = needMultiMatch ? resolveLayerRoute(meta, layer) : captureRoute
245
+ enterChannel.publish({ name, req, route, layer })
246
+
247
+ try {
248
+ return inner.call(this, req, res, next)
249
+ } catch (error) {
250
+ if (calls === 0) {
251
+ calls = 1
252
+ publishError({ req, error })
253
+ nextChannel.publish({ req })
254
+ finishChannel.publish({ req })
255
+ }
256
+
257
+ throw error
258
+ } finally {
259
+ exitChannel.publish({ req })
260
+ }
261
+ })
262
+ }
263
+
264
+ /**
265
+ * @param {object} layer
266
+ * @param {Function} original
267
+ * @returns {Function}
268
+ */
269
+ function wrapNativeLegacyErrorHandle (layer, original) {
270
+ const meta = getLayerMeta(layer)
271
+ const { name, captureRoute, needMultiMatch } = meta
272
+ return shimmer.wrapFunction(original, inner => function (error, req, res, next) {
273
+ if (!enterChannel.hasSubscribers) return inner.call(this, error, req, res, next)
274
+
275
+ let calls = 0
276
+ if (typeof next === 'function') {
277
+ next = shimmer.wrapCallback(next, originalNext => function next (nextError) {
278
+ calls++
279
+ if (calls === 1) {
280
+ if (nextError && nextError !== 'route' && nextError !== 'router') {
281
+ publishError({ req, error: nextError })
282
+ }
283
+
284
+ nextChannel.publish({ req })
285
+ finishChannel.publish({ req })
286
+ } else if (calls === 2) {
287
+ repeatChannel.publish({ req, name, error: nextError })
288
+ }
289
+
290
+ originalNext.apply(this, arguments)
291
+ })
292
+ }
293
+
294
+ const route = needMultiMatch ? resolveLayerRoute(meta, layer) : captureRoute
295
+ enterChannel.publish({ name, req, route, layer })
296
+
297
+ try {
298
+ return inner.call(this, error, req, res, next)
299
+ } catch (caught) {
300
+ if (calls === 0) {
301
+ calls = 1
302
+ publishError({ req, error: caught })
303
+ nextChannel.publish({ req })
304
+ finishChannel.publish({ req })
305
+ }
306
+
307
+ throw caught
308
+ } finally {
309
+ exitChannel.publish({ req })
310
+ }
311
+ })
312
+ }
313
+
314
+ /**
315
+ * @param {object} layer
316
+ * @param {Function} original
317
+ * @param {boolean} [guardRepeatedNext]
318
+ * @returns {Function}
319
+ */
320
+ function wrapLegacyHandle (layer, original, guardRepeatedNext = false) {
321
+ if (!guardRepeatedNext) {
322
+ return original.length === 4
323
+ ? wrapNativeLegacyErrorHandle(layer, original)
324
+ : wrapNativeLegacyRequestHandle(layer, original)
325
+ }
326
+
173
327
  // `annotateLayer` always runs first in `wrapStack`, so the captured meta is
174
328
  // never undefined here (unlike the prototype wraps, where `this` can be any
175
329
  // layer the host dispatches).
176
330
  const meta = getLayerMeta(layer)
331
+ const { name, captureRoute, needMultiMatch } = meta
177
332
  const wrapped = shimmer.wrapFunction(original, inner => function (...args) {
178
333
  if (!enterChannel.hasSubscribers) return inner.apply(this, args)
179
334
 
180
335
  const isErrorHandler = original.length === 4
181
336
  const req = args[isErrorHandler ? 1 : 0]
182
337
  const nextIndex = isErrorHandler ? 3 : 2
183
- if (typeof args[nextIndex] === 'function') args[nextIndex] = wrapNext(req, meta.name, args[nextIndex])
338
+ let calls = 0
339
+ if (typeof args[nextIndex] === 'function') {
340
+ args[nextIndex] = shimmer.wrapCallback(args[nextIndex], originalNext => function next (error) {
341
+ calls++
342
+ if (calls === 1) {
343
+ if (error && error !== 'route' && error !== 'router') {
344
+ publishError({ req, error })
345
+ }
184
346
 
185
- enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, layer), layer })
347
+ nextChannel.publish({ req })
348
+ finishChannel.publish({ req })
349
+ } else if (calls === 2) {
350
+ repeatChannel.publish({ req, name, error })
351
+ }
352
+
353
+ originalNext.apply(this, arguments)
354
+ })
355
+ }
356
+
357
+ const route = needMultiMatch ? resolveLayerRoute(meta, layer) : captureRoute
358
+ enterChannel.publish({ name, req, route, layer })
186
359
 
187
360
  try {
188
361
  return inner.apply(this, args)
189
362
  } catch (error) {
190
- // Unlike the prototype hosts, this router catches a synchronous throw
191
- // outside the layer and calls its own `next(error)`, never `wrappedNext`.
192
- // Mirror `wrapNext` here so the throwing layer still tags its error and
193
- // finishes, rather than lingering on the stack until request finish.
194
- publishError({ req, error })
195
- nextChannel.publish({ req })
196
- finishChannel.publish({ req })
363
+ // Legacy hosts catch outside the layer and never call its wrapped `next`, so finish before rethrowing.
364
+ if (calls === 0) {
365
+ calls = 1
366
+ publishError({ req, error })
367
+ nextChannel.publish({ req })
368
+ finishChannel.publish({ req })
369
+ }
197
370
 
198
371
  throw error
199
372
  } finally {
@@ -209,6 +382,7 @@ function createLayerDispatchWrappers (name) {
209
382
 
210
383
  /**
211
384
  * @param {{ handle_request?: unknown, handleRequest?: unknown }} layer
385
+ * @returns {boolean}
212
386
  */
213
387
  function hasLayerDispatch (layer) {
214
388
  return typeof layer.handle_request === 'function' || typeof layer.handleRequest === 'function'
@@ -221,9 +395,11 @@ function hasLayerDispatch (layer) {
221
395
  * Host-resolved path-to-regexp compile adapter, or undefined when the host
222
396
  * instance ships no path-to-regexp. Captured here so each express/router
223
397
  * instance keeps the dialect it actually loaded.
224
- * @param {((layer: object, original: Function) => Function) | undefined} [wrapLegacyHandle]
398
+ * @param {((layer: object, original: Function, guardRepeatedNext?: boolean) => Function) | undefined}
399
+ * [wrapLegacyHandle]
225
400
  * Fallback that replaces `layer.handle` for hosts without a `Layer` prototype
226
401
  * dispatch (express <4.6.0). Omitted for hosts that always ship one.
402
+ * @returns {(original: Function) => Function}
227
403
  */
228
404
  function createWrapRouterMethod (name, compile, wrapLegacyHandle) {
229
405
  const routeAddedChannel = channel(`apm:${name}:route:added`)
@@ -234,7 +410,7 @@ function createWrapRouterMethod (name, compile, wrapLegacyHandle) {
234
410
 
235
411
  if (wrapLegacyHandle !== undefined && !hasLayerDispatch(layer)) {
236
412
  if (layer.__handle) { // express-async-errors
237
- layer.__handle = wrapLegacyHandle(layer, layer.__handle)
413
+ layer.__handle = wrapLegacyHandle(layer, layer.__handle, true)
238
414
  } else {
239
415
  layer.handle = wrapLegacyHandle(layer, layer.handle)
240
416
  }
@@ -4,14 +4,15 @@ const path = require('node:path')
4
4
 
5
5
  const satisfies = require('../../../vendor/dist/semifies')
6
6
 
7
+ const { hasEfdRetries } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
7
8
  const { RUM_TEST_EXECUTION_ID_COOKIE_NAME } = require('../../dd-trace/src/ci-visibility/rum')
8
9
  const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
9
10
  const log = require('../../dd-trace/src/log')
10
11
  const {
11
12
  DYNAMIC_NAME_RE,
12
- EARLY_FLAKE_DETECTION_RETRY_THRESHOLDS,
13
13
  getTestSuitePath,
14
14
  logAttemptToFixTestExecution,
15
+ recordTestManagementExecution,
15
16
  recordAttemptToFixExecution,
16
17
  } = require('../../dd-trace/src/plugins/util/test')
17
18
  const {
@@ -46,6 +47,7 @@ const VITEST_NO_WORKER_INIT_ACTIVE_ENV = 'DD_TEST_OPT_VITEST_NO_WORKER_INIT_ACTI
46
47
  const VITEST_NO_WORKER_INIT_REQUEST_ENV = 'DD_EXPERIMENTAL_TEST_OPT_VITEST_NO_WORKER_INIT'
47
48
  const VITEST_NO_WORKER_INIT_MINIMUM_VERSION = '3.2.6'
48
49
  const VITEST_DEFAULT_POOL = 'forks'
50
+ const VITEST_BROWSER_EFD_SUITE_ADMISSION_COMMAND = '__dd_vitest_efd_suite_admission'
49
51
  const VITEST_NO_WORKER_INIT_SETUP_FILE = path.join(
50
52
  __dirname,
51
53
  '..',
@@ -68,6 +70,7 @@ const VITEST_BROWSER_URL_RE = /https?:\/\/[^\s)"'>\]]+/g
68
70
  let hasWarnedDisabledIsolate = false
69
71
  let hasWarnedUnsupportedVersion = false
70
72
  let nodeOptionsBeforeNoWorkerInit
73
+ let reserveEarlyFlakeDetectionSuite
71
74
 
72
75
  function noop () {}
73
76
 
@@ -309,8 +312,19 @@ function isNoWorkerInitPool (pool, isVitestWorkerPool) {
309
312
  return pool === undefined || isVitestWorkerPool(pool)
310
313
  }
311
314
 
315
+ /**
316
+ * @param {object} state
317
+ * @returns {boolean}
318
+ */
319
+ function isEarlyFlakeDetectionActive (state) {
320
+ return state.isEarlyFlakeDetectionEnabled &&
321
+ !state.isEarlyFlakeDetectionFaulty &&
322
+ hasEfdRetries(state.earlyFlakeDetectionRetryPolicy)
323
+ }
324
+
312
325
  function configure (ctx, frameworkVersion, testSpecifications, setupData, options) {
313
- const { getConfiguredEfdRetryCount, shouldReportTestModule, state } = options
326
+ const { shouldReportTestModule, state } = options
327
+ reserveEarlyFlakeDetectionSuite = options.reserveEarlyFlakeDetectionSuite
314
328
  addSetupFileToVitestConfigs(ctx, VITEST_NO_WORKER_INIT_SETUP_FILE, testSpecifications)
315
329
  addVitestBrowserSetupFileAccess(testSpecifications)
316
330
 
@@ -329,13 +343,10 @@ function configure (ctx, frameworkVersion, testSpecifications, setupData, option
329
343
  attemptToFixRetries: state.testManagementAttemptToFixRetries,
330
344
  attemptToFixTests: getSelectedTestManagementTests(testManagementTestsBySuite, 'isAttemptToFix'),
331
345
  disabledTests: getSelectedTestManagementTests(testManagementTestsBySuite, 'isDisabled'),
332
- earlyFlakeDetectionRetries: getConfiguredEfdRetryCount(
333
- state.earlyFlakeDetectionSlowTestRetries,
334
- state.earlyFlakeDetectionNumRetries
335
- ),
336
- earlyFlakeDetectionRetryThresholds: EARLY_FLAKE_DETECTION_RETRY_THRESHOLDS,
337
- earlyFlakeDetectionSlowRetries: state.earlyFlakeDetectionSlowTestRetries,
338
- isEarlyFlakeDetectionEnabled: state.isEarlyFlakeDetectionEnabled && !state.isEarlyFlakeDetectionFaulty,
346
+ earlyFlakeDetectionRetryPolicy: state.earlyFlakeDetectionRetryPolicy,
347
+ efdSuiteAdmissionBrowserCommand: VITEST_BROWSER_EFD_SUITE_ADMISSION_COMMAND,
348
+ isEfdSuiteAdmissionEnabled: state.isEfdSuiteAdmissionEnabled,
349
+ isEarlyFlakeDetectionEnabled: isEarlyFlakeDetectionActive(state),
339
350
  isRumCorrelationEnabled: !canRaceRumCorrelation(ctx, testSpecifications),
340
351
  knownTests: knownTestsBySuite || {},
341
352
  modifiedFiles: modifiedFiles || {},
@@ -437,6 +448,18 @@ function configureVitestBrowserSetupFile (viteConfig) {
437
448
  }
438
449
  }
439
450
 
451
+ /**
452
+ * Reserves EFD retries for a suite executing in Vitest Browser Mode.
453
+ *
454
+ * @param {unknown} _context
455
+ * @param {string} testSuite
456
+ * @param {unknown} hasNewTest
457
+ * @returns {boolean}
458
+ */
459
+ function handleBrowserEfdSuiteAdmission (_context, testSuite, hasNewTest) {
460
+ return reserveEarlyFlakeDetectionSuite?.(testSuite, hasNewTest === true) === true
461
+ }
462
+
440
463
  /**
441
464
  * Allows Vite to serve the Datadog setup file after its default workspace detection has run.
442
465
  *
@@ -445,7 +468,9 @@ function configureVitestBrowserSetupFile (viteConfig) {
445
468
  */
446
469
  function allowVitestBrowserSetupFile (viteConfig) {
447
470
  const allow = viteConfig.server?.fs?.allow
448
- if (allow && !allow.includes(VITEST_NO_WORKER_INIT_SETUP_FILE)) {
471
+ if (!allow) return
472
+
473
+ if (!allow.includes(VITEST_NO_WORKER_INIT_SETUP_FILE)) {
449
474
  allow.push(VITEST_NO_WORKER_INIT_SETUP_FILE)
450
475
  }
451
476
  }
@@ -465,10 +490,16 @@ function addVitestBrowserSetupFileAccess (testSpecifications) {
465
490
  if (!isBrowserTestSpecification(testSpecification)) continue
466
491
 
467
492
  const browserServerProject = project?._parent || project
468
- if (!browserServerProject || configuredProjects.has(browserServerProject)) continue
493
+ if (!browserServerProject) continue
494
+
495
+ addVitestBrowserCommand(safeConfig(project))
496
+ if (configuredProjects.has(browserServerProject)) continue
469
497
 
470
498
  configuredProjects.add(browserServerProject)
499
+ addVitestBrowserCommand(safeConfig(browserServerProject))
471
500
  browserServerProject.options ||= {}
501
+ browserServerProject.options.browser ||= {}
502
+ addVitestBrowserCommand(browserServerProject.options)
472
503
  browserServerProject.options.plugins ||= []
473
504
  if (!browserServerProject.options.plugins.includes(VITEST_BROWSER_SETUP_FILE_PLUGIN)) {
474
505
  browserServerProject.options.plugins.push(VITEST_BROWSER_SETUP_FILE_PLUGIN)
@@ -476,6 +507,20 @@ function addVitestBrowserSetupFileAccess (testSpecifications) {
476
507
  }
477
508
  }
478
509
 
510
+ /**
511
+ * Adds the private EFD admission command to a Vitest Browser Mode configuration.
512
+ *
513
+ * @param {object|undefined} config
514
+ * @returns {void}
515
+ */
516
+ function addVitestBrowserCommand (config) {
517
+ const browserConfig = config?.browser
518
+ if (!browserConfig) return
519
+
520
+ browserConfig.commands ||= {}
521
+ browserConfig.commands[VITEST_BROWSER_EFD_SUITE_ADMISSION_COMMAND] = handleBrowserEfdSuiteAdmission
522
+ }
523
+
479
524
  function getNoWorkerInitVitestConfigs (ctx, testSpecifications) {
480
525
  const configs = new Set()
481
526
  if (Array.isArray(testSpecifications)) {
@@ -841,7 +886,7 @@ function createMainProcessReporter (reporterState) {
841
886
  !isAttemptToFix &&
842
887
  state.isKnownTestsEnabled &&
843
888
  knownTests &&
844
- !state.isEarlyFlakeDetectionFaulty &&
889
+ (state.isEfdSuiteAdmissionEnabled || !state.isEarlyFlakeDetectionFaulty) &&
845
890
  !knownTests.includes(testName)
846
891
  )
847
892
  const isModified = testProperties?.isModified === true
@@ -855,7 +900,7 @@ function createMainProcessReporter (reporterState) {
855
900
  return {
856
901
  isAttemptToFix,
857
902
  isDisabled: testManagementProperties.isDisabled,
858
- isEarlyFlakeDetection: (isNew || isModified) && state.isEarlyFlakeDetectionEnabled,
903
+ isEarlyFlakeDetection: task.meta?.__ddTestOptEfdRetries !== undefined,
859
904
  isFlakyTestRetries,
860
905
  isQuarantined: testManagementProperties.isQuarantined,
861
906
  isModified,
@@ -1176,6 +1221,23 @@ function reportFinalTestAttempt (testReport) {
1176
1221
  } = testReport
1177
1222
 
1178
1223
  if (status === 'skip') {
1224
+ recordTestManagementExecution({
1225
+ testSuite: testProperties.testSuite,
1226
+ testName,
1227
+ status,
1228
+ isAttemptToFix: testProperties.isAttemptToFix,
1229
+ isDisabled: testProperties.isDisabled,
1230
+ isQuarantined: testProperties.isQuarantined,
1231
+ })
1232
+ if (testProperties.isAttemptToFix) {
1233
+ recordAttemptToFixExecution(state.attemptToFixExecutions, {
1234
+ testSuite: testProperties.testSuite,
1235
+ testName,
1236
+ status,
1237
+ isDisabled: testProperties.isDisabled,
1238
+ isQuarantined: testProperties.isQuarantined,
1239
+ })
1240
+ }
1179
1241
  const {
1180
1242
  isRumActive,
1181
1243
  testExecutionId,
@@ -1185,7 +1247,9 @@ function reportFinalTestAttempt (testReport) {
1185
1247
  testName,
1186
1248
  testSuiteAbsolutePath,
1187
1249
  isNew: testProperties.isNew,
1250
+ isAttemptToFix: testProperties.isAttemptToFix,
1188
1251
  isDisabled: testProperties.isDisabled,
1252
+ isQuarantined: testProperties.isQuarantined,
1189
1253
  isRumActive,
1190
1254
  isTestFrameworkWorker: true,
1191
1255
  requestErrorTags: state.requestErrorTags,
@@ -1282,6 +1346,14 @@ function reportTestAttempt (testReport, attempt) {
1282
1346
  testStartLine: task.location?.line,
1283
1347
  testExecutionId,
1284
1348
  }
1349
+ recordTestManagementExecution({
1350
+ testSuite: testProperties.testSuite,
1351
+ testName,
1352
+ status,
1353
+ isAttemptToFix: testProperties.isAttemptToFix,
1354
+ isDisabled: testProperties.isDisabled,
1355
+ isQuarantined: testProperties.isQuarantined,
1356
+ })
1285
1357
  if (testProperties.isAttemptToFix) {
1286
1358
  recordAttemptToFixExecution(state.attemptToFixExecutions, {
1287
1359
  testSuite: testProperties.testSuite,