dd-trace 6.7.0 → 6.8.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 (147) hide show
  1. package/ci/test-optimization-validation/command-output-policy.js +5 -5
  2. package/ci/test-optimization-validation/framework-adapters/playwright.js +5 -5
  3. package/ci/test-optimization-validation/generated-files.js +9 -9
  4. package/ci/test-optimization-validation/generated-test-contract.js +1 -3
  5. package/ci/test-optimization-validation/manifest-scaffold.js +1 -2
  6. package/ci/test-optimization-validation/offline-output.js +6 -6
  7. package/ci/test-optimization-validation/runner-command.js +1 -1
  8. package/ci/test-optimization-validation/safe-files.js +4 -4
  9. package/ci/test-optimization-validation/scenarios/basic-reporting.js +1 -1
  10. package/ci/test-optimization-validation/scenarios/test-management.js +2 -2
  11. package/ci/vitest-no-worker-init-setup.mjs +348 -51
  12. package/index.d.ts +77 -13
  13. package/package.json +9 -3
  14. package/packages/datadog-core/src/utils/src/kebabcase.js +8 -2
  15. package/packages/datadog-esbuild/index.js +4 -2
  16. package/packages/datadog-esbuild/src/utils.js +1 -1
  17. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +4 -4
  18. package/packages/datadog-instrumentations/src/cucumber.js +9 -6
  19. package/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +10 -0
  20. package/packages/datadog-instrumentations/src/helpers/hook.js +19 -2
  21. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  22. package/packages/datadog-instrumentations/src/helpers/register.js +9 -3
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +1 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +57 -0
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +50 -18
  27. package/packages/datadog-instrumentations/src/helpers/router-helper.js +1 -1
  28. package/packages/datadog-instrumentations/src/helpers/shared-utils.js +39 -0
  29. package/packages/datadog-instrumentations/src/http/client.js +1 -3
  30. package/packages/datadog-instrumentations/src/jest.js +8 -14
  31. package/packages/datadog-instrumentations/src/mocha/common.js +1 -3
  32. package/packages/datadog-instrumentations/src/mocha/main.js +2 -42
  33. package/packages/datadog-instrumentations/src/mocha/utils.js +68 -2
  34. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +19 -0
  35. package/packages/datadog-instrumentations/src/mocha/worker.js +446 -42
  36. package/packages/datadog-instrumentations/src/moleculer/client.js +1 -1
  37. package/packages/datadog-instrumentations/src/playwright.js +3 -2
  38. package/packages/datadog-instrumentations/src/rhea.js +1 -1
  39. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +525 -56
  40. package/packages/datadog-instrumentations/src/vitest-main.js +90 -10
  41. package/packages/datadog-instrumentations/src/webdriverio.js +914 -0
  42. package/packages/datadog-plugin-aerospike/src/index.js +1 -3
  43. package/packages/datadog-plugin-amqplib/src/client.js +1 -1
  44. package/packages/datadog-plugin-amqplib/src/producer.js +1 -1
  45. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +2 -2
  46. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +2 -2
  47. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -3
  48. package/packages/datadog-plugin-azure-service-bus/src/producer.js +5 -3
  49. package/packages/datadog-plugin-bullmq/src/producer.js +3 -3
  50. package/packages/datadog-plugin-child_process/src/index.js +3 -3
  51. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +5 -8
  52. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -1
  53. package/packages/datadog-plugin-fastify/src/tracing.js +1 -1
  54. package/packages/datadog-plugin-fs/src/index.js +0 -4
  55. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +14 -7
  56. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +19 -10
  57. package/packages/datadog-plugin-graphql/src/execute.js +1 -0
  58. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -1
  59. package/packages/datadog-plugin-http/src/client.js +2 -1
  60. package/packages/datadog-plugin-jest/src/index.js +2 -3
  61. package/packages/datadog-plugin-jest/src/util.js +2 -1
  62. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  63. package/packages/datadog-plugin-mocha/src/index.js +25 -6
  64. package/packages/datadog-plugin-prisma/src/index.js +5 -1
  65. package/packages/datadog-plugin-rhea/src/consumer.js +16 -13
  66. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  67. package/packages/datadog-plugin-undici/src/index.js +2 -1
  68. package/packages/datadog-plugin-vitest/src/index.js +84 -11
  69. package/packages/datadog-webpack/index.js +2 -2
  70. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -1
  71. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +1 -1
  72. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +1 -1
  73. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +1 -1
  74. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +1 -3
  75. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  76. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +2 -2
  77. package/packages/dd-trace/src/appsec/sdk/track_event.js +1 -1
  78. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  79. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +7 -6
  80. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -2
  81. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -1
  82. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +4 -0
  83. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/webdriverio.js +26 -0
  84. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -1
  85. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -3
  86. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  87. package/packages/dd-trace/src/config/defaults.js +14 -6
  88. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -2
  89. package/packages/dd-trace/src/config/index.js +15 -0
  90. package/packages/dd-trace/src/config/parsers.js +19 -1
  91. package/packages/dd-trace/src/config/supported-configurations.json +5 -6
  92. package/packages/dd-trace/src/datastreams/encoding.js +3 -4
  93. package/packages/dd-trace/src/debugger/devtools_client/log.js +2 -1
  94. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +1 -4
  95. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -2
  96. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +2 -2
  97. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +16 -2
  98. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  99. package/packages/dd-trace/src/exporters/common/url.js +3 -5
  100. package/packages/dd-trace/src/exporters/electron/index.js +4 -1
  101. package/packages/dd-trace/src/external-logger/src/index.js +1 -2
  102. package/packages/dd-trace/src/flare/index.js +2 -2
  103. package/packages/dd-trace/src/id.js +1 -1
  104. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -1
  105. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -2
  106. package/packages/dd-trace/src/llmobs/experiments/dataset.js +66 -14
  107. package/packages/dd-trace/src/llmobs/experiments/experiment.js +289 -95
  108. package/packages/dd-trace/src/llmobs/experiments/index.js +51 -12
  109. package/packages/dd-trace/src/llmobs/experiments/noop.js +95 -10
  110. package/packages/dd-trace/src/llmobs/experiments/result.js +13 -2
  111. package/packages/dd-trace/src/llmobs/experiments/util.js +218 -0
  112. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +1 -1
  113. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +1 -1
  114. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  115. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  116. package/packages/dd-trace/src/llmobs/sdk.js +4 -11
  117. package/packages/dd-trace/src/llmobs/span_processor.js +9 -7
  118. package/packages/dd-trace/src/llmobs/tagger.js +1 -2
  119. package/packages/dd-trace/src/llmobs/writers/spans.js +12 -6
  120. package/packages/dd-trace/src/noop/proxy.js +2 -2
  121. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  122. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +2 -1
  123. package/packages/dd-trace/src/opentracing/span.js +1 -1
  124. package/packages/dd-trace/src/opentracing/span_context.js +1 -1
  125. package/packages/dd-trace/src/plugins/ci_plugin.js +105 -20
  126. package/packages/dd-trace/src/plugins/index.js +1 -0
  127. package/packages/dd-trace/src/plugins/plugin.js +1 -1
  128. package/packages/dd-trace/src/plugins/tracing.js +1 -0
  129. package/packages/dd-trace/src/plugins/util/ci.js +7 -6
  130. package/packages/dd-trace/src/plugins/util/git.js +1 -1
  131. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +1 -1
  132. package/packages/dd-trace/src/plugins/util/llm.js +2 -2
  133. package/packages/dd-trace/src/plugins/util/tags.js +2 -0
  134. package/packages/dd-trace/src/plugins/util/test.js +38 -12
  135. package/packages/dd-trace/src/plugins/util/url.js +5 -5
  136. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  137. package/packages/dd-trace/src/plugins/util/web.js +1 -1
  138. package/packages/dd-trace/src/priority_sampler.js +6 -3
  139. package/packages/dd-trace/src/profiling/profilers/poisson.js +1 -1
  140. package/packages/dd-trace/src/profiling/profilers/shared.js +2 -2
  141. package/packages/dd-trace/src/ritm.js +5 -20
  142. package/packages/dd-trace/src/startup-log.js +3 -0
  143. package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -1
  144. package/packages/dd-trace/src/util.js +16 -0
  145. package/vendor/dist/@apm-js-collab/code-transformer/index.js +12 -12
  146. package/vendor/dist/pprof-format/index.js +1 -1
  147. package/vendor/dist/source-map/index.js +1 -1
@@ -4,6 +4,7 @@ const path = require('node:path')
4
4
 
5
5
  const satisfies = require('../../../vendor/dist/semifies')
6
6
 
7
+ const { RUM_TEST_EXECUTION_ID_COOKIE_NAME } = require('../../dd-trace/src/ci-visibility/rum')
7
8
  const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
8
9
  const log = require('../../dd-trace/src/log')
9
10
  const {
@@ -29,9 +30,9 @@ const {
29
30
  testSuiteStartCh,
30
31
  } = require('./vitest-util')
31
32
 
32
- // No-worker-init instrumentation for DD_EXPERIMENTAL_TEST_OPT_VITEST_NO_WORKER_INIT.
33
- // When enabled, Vitest workers do not initialize dd-trace, so this main-process instrumentation
34
- // takes over some worker responsibilities, including test span creation and lifecycle reporting.
33
+ // Main-process Vitest reporting used by no-worker-init and Browser Mode.
34
+ // Execution changes are applied by an instrumentation-less setup file in the test environment,
35
+ // while this module creates test spans from Vitest reporter events.
35
36
  const mainProcessReporterStates = new WeakMap()
36
37
  const loggedAttemptToFixTests = new Set()
37
38
 
@@ -53,16 +54,181 @@ const VITEST_NO_WORKER_INIT_SETUP_FILE = path.join(
53
54
  'ci',
54
55
  'vitest-no-worker-init-setup.mjs'
55
56
  )
57
+ const VITEST_BROWSER_SETUP_FILE_PLUGIN = {
58
+ name: 'datadog:vitest-browser-setup-file',
59
+ config: configureVitestBrowserSetupFile,
60
+ configResolved: allowVitestBrowserSetupFile,
61
+ }
56
62
  const VITEST_NO_WORKER_INIT_ISOLATE_WARNING =
57
63
  `${VITEST_NO_WORKER_INIT_REQUEST_ENV} is ignored because Vitest isolate is disabled. ` +
58
64
  'The lighter Vitest worker path only works when each test file runs in an isolated worker.'
59
65
  const NODE_OPTIONS_QUOTE_RE = /[\s"\\]/
66
+ const VITEST_BROWSER_STACK_LOCATION_RE = /:\d+:\d+$/
67
+ const VITEST_BROWSER_URL_RE = /https?:\/\/[^\s)"'>\]]+/g
60
68
  let hasWarnedDisabledIsolate = false
61
69
  let hasWarnedUnsupportedVersion = false
62
70
  let nodeOptionsBeforeNoWorkerInit
63
71
 
64
72
  function noop () {}
65
73
 
74
+ /**
75
+ * Removes Vite and Vitest runtime query parameters from browser error URLs.
76
+ *
77
+ * @param {string} url
78
+ * @returns {string}
79
+ */
80
+ function removeVitestBrowserUrlMetadata (url) {
81
+ const locationMatch = url.match(VITEST_BROWSER_STACK_LOCATION_RE)
82
+ const location = locationMatch?.[0] || ''
83
+ const urlWithoutLocation = location ? url.slice(0, -location.length) : url
84
+ const queryStart = urlWithoutLocation.indexOf('?')
85
+ if (queryStart === -1) return url
86
+
87
+ const fragmentStart = urlWithoutLocation.indexOf('#', queryStart)
88
+ const queryEnd = fragmentStart === -1 ? urlWithoutLocation.length : fragmentStart
89
+ const parameters = urlWithoutLocation.slice(queryStart + 1, queryEnd).split('&')
90
+ const keptParameters = []
91
+ let removedParameter = false
92
+
93
+ for (const parameter of parameters) {
94
+ const valueSeparator = parameter.indexOf('=')
95
+ const name = valueSeparator === -1 ? parameter : parameter.slice(0, valueSeparator)
96
+ if (name === 'import' || name === 'browserv') {
97
+ removedParameter = true
98
+ } else {
99
+ keptParameters.push(parameter)
100
+ }
101
+ }
102
+
103
+ if (!removedParameter) return url
104
+
105
+ const query = keptParameters.length ? `?${keptParameters.join('&')}` : ''
106
+ return urlWithoutLocation.slice(0, queryStart) + query + urlWithoutLocation.slice(queryEnd) + location
107
+ }
108
+
109
+ /**
110
+ * Removes Vite and Vitest runtime URL metadata from browser error text.
111
+ *
112
+ * @param {string} value
113
+ * @returns {string}
114
+ */
115
+ function normalizeVitestBrowserErrorText (value) {
116
+ return value.replaceAll(VITEST_BROWSER_URL_RE, removeVitestBrowserUrlMetadata)
117
+ }
118
+
119
+ /**
120
+ * Removes the ephemeral Vite server origin in addition to its runtime query parameters.
121
+ *
122
+ * @param {string} url
123
+ * @returns {string}
124
+ */
125
+ function normalizeVitestBrowserStackUrl (url) {
126
+ const normalizedUrl = removeVitestBrowserUrlMetadata(url)
127
+ const protocolEnd = normalizedUrl.indexOf('://')
128
+ const pathStart = normalizedUrl.indexOf('/', protocolEnd + 3)
129
+ return pathStart === -1 ? normalizedUrl : normalizedUrl.slice(pathStart)
130
+ }
131
+
132
+ /**
133
+ * Normalizes browser URLs in a stack when Vitest does not provide parsed frames.
134
+ *
135
+ * @param {string} value
136
+ * @returns {string}
137
+ */
138
+ function normalizeVitestBrowserStackText (value) {
139
+ return value.replaceAll(VITEST_BROWSER_URL_RE, normalizeVitestBrowserStackUrl)
140
+ }
141
+
142
+ /**
143
+ * Rebuilds an error stack from the source-mapped frames already parsed by Vitest.
144
+ *
145
+ * @param {{
146
+ * stack?: string,
147
+ * stacks?: Array<{ column: number, file: string, line: number, method?: string }>
148
+ * }} error
149
+ * @returns {string|undefined}
150
+ */
151
+ function normalizeVitestBrowserErrorStack (error) {
152
+ if (typeof error.stack !== 'string') return error.stack
153
+ if (!Array.isArray(error.stacks)) {
154
+ return normalizeVitestBrowserStackText(error.stack)
155
+ }
156
+
157
+ const firstLineEnd = error.stack.indexOf('\n')
158
+ const stackLines = new Array(error.stacks.length + 1)
159
+ stackLines[0] = normalizeVitestBrowserErrorText(
160
+ firstLineEnd === -1 ? error.stack : error.stack.slice(0, firstLineEnd)
161
+ )
162
+
163
+ for (let index = 0; index < error.stacks.length; index++) {
164
+ const frame = error.stacks[index]
165
+ const location = `${frame.file}:${frame.line}:${frame.column}`
166
+ stackLines[index + 1] = frame.method
167
+ ? ` at ${frame.method} (${location})`
168
+ : ` at ${location}`
169
+ }
170
+
171
+ return stackLines.join('\n')
172
+ }
173
+
174
+ /**
175
+ * Returns an error copy with browser-only URL metadata removed.
176
+ *
177
+ * @param {{
178
+ * message?: string,
179
+ * name?: string,
180
+ * stack?: string,
181
+ * stacks?: Array<{ column: number, file: string, line: number, method?: string }>
182
+ * }|undefined} error
183
+ * @returns {{
184
+ * message?: string,
185
+ * name?: string,
186
+ * stack?: string,
187
+ * stacks?: Array<{ column: number, file: string, line: number, method?: string }>
188
+ * }|undefined}
189
+ */
190
+ function normalizeVitestBrowserError (error) {
191
+ if (!error) return error
192
+
193
+ const message = typeof error.message === 'string'
194
+ ? normalizeVitestBrowserErrorText(error.message)
195
+ : error.message
196
+ const stack = normalizeVitestBrowserErrorStack(error)
197
+ if (message === error.message && stack === error.stack) return error
198
+
199
+ const normalizedError = { ...error }
200
+ if (error.name !== undefined) normalizedError.name = error.name
201
+ if (message !== undefined) normalizedError.message = message
202
+ if (stack !== undefined) normalizedError.stack = stack
203
+ return normalizedError
204
+ }
205
+
206
+ /**
207
+ * Normalizes browser errors without mutating Vitest's task result.
208
+ *
209
+ * @param {Array<{
210
+ * message?: string,
211
+ * name?: string,
212
+ * stack?: string,
213
+ * stacks?: Array<{ column: number, file: string, line: number, method?: string }>
214
+ * }>|undefined} errors
215
+ * @returns {Array<{
216
+ * message?: string,
217
+ * name?: string,
218
+ * stack?: string,
219
+ * stacks?: Array<{ column: number, file: string, line: number, method?: string }>
220
+ * }>}
221
+ */
222
+ function normalizeVitestBrowserErrors (errors) {
223
+ if (!errors?.length) return []
224
+
225
+ const normalizedErrors = new Array(errors.length)
226
+ for (let index = 0; index < errors.length; index++) {
227
+ normalizedErrors[index] = normalizeVitestBrowserError(errors[index])
228
+ }
229
+ return normalizedErrors
230
+ }
231
+
66
232
  function isRequested () {
67
233
  return getValueFromEnvSources(VITEST_NO_WORKER_INIT_REQUEST_ENV) === true
68
234
  }
@@ -144,19 +310,22 @@ function isNoWorkerInitPool (pool, isVitestWorkerPool) {
144
310
  }
145
311
 
146
312
  function configure (ctx, frameworkVersion, testSpecifications, setupData, options) {
147
- const { getConfiguredEfdRetryCount, state } = options
313
+ const { getConfiguredEfdRetryCount, shouldReportTestModule, state } = options
148
314
  addSetupFileToVitestConfigs(ctx, VITEST_NO_WORKER_INIT_SETUP_FILE, testSpecifications)
315
+ addVitestBrowserSetupFileAccess(testSpecifications)
149
316
 
150
317
  const {
151
318
  knownTestsBySuite,
152
319
  modifiedFiles,
153
320
  repositoryRoot,
154
321
  testManagementTestsBySuite,
322
+ testPropertiesByFilepath,
155
323
  testSessionConfiguration,
156
324
  } = setupData
157
325
 
158
326
  setProvidedContext(ctx, {
159
327
  _ddVitestWorkerSetup: {
328
+ isActive: true,
160
329
  attemptToFixRetries: state.testManagementAttemptToFixRetries,
161
330
  attemptToFixTests: getSelectedTestManagementTests(testManagementTestsBySuite, 'isAttemptToFix'),
162
331
  disabledTests: getSelectedTestManagementTests(testManagementTestsBySuite, 'isDisabled'),
@@ -167,14 +336,19 @@ function configure (ctx, frameworkVersion, testSpecifications, setupData, option
167
336
  earlyFlakeDetectionRetryThresholds: EARLY_FLAKE_DETECTION_RETRY_THRESHOLDS,
168
337
  earlyFlakeDetectionSlowRetries: state.earlyFlakeDetectionSlowTestRetries,
169
338
  isEarlyFlakeDetectionEnabled: state.isEarlyFlakeDetectionEnabled && !state.isEarlyFlakeDetectionFaulty,
339
+ isRumCorrelationEnabled: !canRaceRumCorrelation(ctx, testSpecifications),
170
340
  knownTests: knownTestsBySuite || {},
171
341
  modifiedFiles: modifiedFiles || {},
172
342
  quarantinedTests: getSelectedTestManagementTests(testManagementTestsBySuite, 'isQuarantined'),
173
343
  repositoryRoot: repositoryRoot || process.cwd(),
344
+ rumTestExecutionIdCookieName: RUM_TEST_EXECUTION_ID_COOKIE_NAME,
345
+ testPropertiesByFilepath: testPropertiesByFilepath || {},
174
346
  },
175
- }, 'Could not send Vitest worker setup context, so no-worker execution changes will not work.')
347
+ }, 'Could not send Vitest setup context, so main-process execution changes will not work.')
176
348
 
177
- installMainProcessReporter(ctx, frameworkVersion, testSessionConfiguration || {}, setupData, state)
349
+ installMainProcessReporter(ctx, frameworkVersion, testSessionConfiguration || {}, setupData, state, {
350
+ shouldReportTestModule,
351
+ })
178
352
  }
179
353
 
180
354
  function deactivate (ctx) {
@@ -182,6 +356,11 @@ function deactivate (ctx) {
182
356
  if (installedReporterState) {
183
357
  installedReporterState.isActive = false
184
358
  }
359
+ setProvidedContext(ctx, {
360
+ _ddVitestWorkerSetup: {
361
+ isActive: false,
362
+ },
363
+ }, 'Could not deactivate Vitest main-process execution changes.')
185
364
  }
186
365
 
187
366
  function configureWorkerEnv (workerEnv, shouldSkipWorkerInit = false) {
@@ -224,14 +403,75 @@ function addSetupFileToVitestConfigs (ctx, setupFile, testSpecifications) {
224
403
  for (const config of configs) {
225
404
  if (!config) continue
226
405
 
406
+ config.includeTaskLocation = true
227
407
  if (config.setupFiles === undefined) {
228
408
  config.setupFiles = []
229
409
  } else if (typeof config.setupFiles === 'string') {
230
410
  config.setupFiles = [config.setupFiles]
231
411
  }
232
412
 
233
- if (!config.setupFiles.includes(setupFile)) {
234
- config.setupFiles.push(setupFile)
413
+ const setupFileIndex = config.setupFiles.indexOf(setupFile)
414
+ if (setupFileIndex !== 0) {
415
+ if (setupFileIndex > 0) {
416
+ config.setupFiles.splice(setupFileIndex, 1)
417
+ }
418
+ config.setupFiles.unshift(setupFile)
419
+ }
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Resolves the Vitest runner from the browser project's dependency tree.
425
+ *
426
+ * @param {{
427
+ * resolve?: { dedupe?: string[] }
428
+ * }} viteConfig
429
+ * @returns {void}
430
+ */
431
+ function configureVitestBrowserSetupFile (viteConfig) {
432
+ viteConfig.resolve ||= {}
433
+ viteConfig.resolve.dedupe ||= []
434
+
435
+ if (!viteConfig.resolve.dedupe.includes('@vitest/runner')) {
436
+ viteConfig.resolve.dedupe.push('@vitest/runner')
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Allows Vite to serve the Datadog setup file after its default workspace detection has run.
442
+ *
443
+ * @param {{ server?: { fs?: { allow?: string[] } } }} viteConfig
444
+ * @returns {void}
445
+ */
446
+ function allowVitestBrowserSetupFile (viteConfig) {
447
+ const allow = viteConfig.server?.fs?.allow
448
+ if (allow && !allow.includes(VITEST_NO_WORKER_INIT_SETUP_FILE)) {
449
+ allow.push(VITEST_NO_WORKER_INIT_SETUP_FILE)
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Installs the Vite access plugin on each parent project that owns a Browser Mode server.
455
+ *
456
+ * @param {object[]|undefined} testSpecifications
457
+ * @returns {void}
458
+ */
459
+ function addVitestBrowserSetupFileAccess (testSpecifications) {
460
+ if (!Array.isArray(testSpecifications)) return
461
+
462
+ const configuredProjects = new Set()
463
+ for (const testSpecification of testSpecifications) {
464
+ const project = getTestSpecificationProject(testSpecification)
465
+ if (!isBrowserTestSpecification(testSpecification)) continue
466
+
467
+ const browserServerProject = project?._parent || project
468
+ if (!browserServerProject || configuredProjects.has(browserServerProject)) continue
469
+
470
+ configuredProjects.add(browserServerProject)
471
+ browserServerProject.options ||= {}
472
+ browserServerProject.options.plugins ||= []
473
+ if (!browserServerProject.options.plugins.includes(VITEST_BROWSER_SETUP_FILE_PLUGIN)) {
474
+ browserServerProject.options.plugins.push(VITEST_BROWSER_SETUP_FILE_PLUGIN)
235
475
  }
236
476
  }
237
477
  }
@@ -279,12 +519,20 @@ function getSelectedTestManagementTests (testManagementTestsBySuite, propertyNam
279
519
  return selectedTests
280
520
  }
281
521
 
282
- function installMainProcessReporter (ctx, frameworkVersion, testSessionConfiguration, testOptimizationData, state) {
522
+ function installMainProcessReporter (
523
+ ctx,
524
+ frameworkVersion,
525
+ testSessionConfiguration,
526
+ testOptimizationData,
527
+ state,
528
+ options
529
+ ) {
283
530
  if (!ctx?.reporters) return
284
531
 
285
532
  const reporterState = {
286
533
  frameworkVersion,
287
534
  isActive: true,
535
+ shouldReportTestModule: options.shouldReportTestModule,
288
536
  state,
289
537
  testSessionConfiguration,
290
538
  testOptimizationData,
@@ -307,26 +555,26 @@ function createMainProcessReporter (reporterState) {
307
555
 
308
556
  return {
309
557
  onTestModuleStart (testModule) {
310
- if (!isActive()) return
558
+ if (!shouldReport(testModule)) return
311
559
 
312
560
  startTestSuite(testModule)
313
561
  },
314
562
 
315
563
  onTestModuleEnd (testModule) {
316
- if (!isActive()) return
564
+ if (!shouldReport(testModule)) return
317
565
 
318
566
  return reportTestModule(testModule)
319
567
  },
320
568
 
321
569
  onTestCaseResult (testCase) {
322
- if (!isActive()) return
570
+ if (!shouldReport(testCase.module || testCase.task?.file)) return
323
571
 
324
572
  const task = getTestCaseTask(testCase)
325
573
  recordFinalTaskAttemptResult(task)
326
574
  },
327
575
 
328
576
  onTaskUpdate (packs, events) {
329
- if (!isActive()) return
577
+ if (!isReporterActive()) return
330
578
  if (!events) return
331
579
 
332
580
  for (const event of events) {
@@ -336,23 +584,43 @@ function createMainProcessReporter (reporterState) {
336
584
  }
337
585
  },
338
586
 
587
+ onWatcherRerun () {
588
+ for (const testSuiteCtx of testSuiteContexts.values()) {
589
+ testSuiteFinishCh.publish({
590
+ status: 'skip',
591
+ deferFlush: true,
592
+ onDone: noop,
593
+ ...testSuiteCtx.currentStore,
594
+ })
595
+ }
596
+ testSuiteContexts.clear()
597
+ finishedTestModules.clear()
598
+ taskAttemptStatuses.clear()
599
+ tasksWithRecordedFinalAttempt.clear()
600
+ },
601
+
339
602
  onFinished (files) {
340
- if (!isActive()) return
603
+ if (!isReporterActive()) return
341
604
  if (!files) return
342
605
 
343
606
  for (const file of files) {
344
607
  const testModule = createTestModuleFromFile(file)
345
- if (!finishedTestModules.has(file.id)) {
608
+ if (shouldReport(testModule) && !finishedTestModules.has(file.id)) {
346
609
  reportTestModule(testModule)
347
610
  }
348
611
  }
349
612
  },
350
613
  }
351
614
 
352
- function isActive () {
615
+ function isReporterActive () {
353
616
  return reporterState.isActive === true
354
617
  }
355
618
 
619
+ function shouldReport (testModule) {
620
+ return isReporterActive() &&
621
+ (!reporterState.shouldReportTestModule || reporterState.shouldReportTestModule(testModule))
622
+ }
623
+
356
624
  function recordTaskAttemptStatus (taskId, status, attemptCount) {
357
625
  let statuses = taskAttemptStatuses.get(taskId)
358
626
  if (!statuses) {
@@ -389,7 +657,7 @@ function createMainProcessReporter (reporterState) {
389
657
  let testSuiteError
390
658
 
391
659
  for (const task of testTasks) {
392
- const testReport = getTestReport(task, testSuiteCtx.currentStore)
660
+ const testReport = getTestReport(task, testSuiteCtx.currentStore, testSuiteCtx.browserEnvironment)
393
661
  testReports.push(testReport)
394
662
 
395
663
  for (const attempt of testReport.nonFinalAttempts) {
@@ -412,7 +680,9 @@ function createMainProcessReporter (reporterState) {
412
680
  }
413
681
 
414
682
  if (testSuiteError) {
415
- testSuiteCtx.error = testSuiteError
683
+ testSuiteCtx.error = testSuiteCtx.browserEnvironment
684
+ ? normalizeVitestBrowserError(testSuiteError)
685
+ : testSuiteError
416
686
  testSuiteErrorCh.runStores(testSuiteCtx, () => {})
417
687
  }
418
688
 
@@ -428,16 +698,22 @@ function createMainProcessReporter (reporterState) {
428
698
  function startTestSuite (testModule) {
429
699
  const { frameworkVersion, testSessionConfiguration } = reporterState
430
700
  const testModuleId = getTestModuleId(testModule)
701
+ const browserEnvironment = getTestModuleBrowserEnvironment(testModule)
431
702
  const testSuiteCtx = {
703
+ browserEnvironment,
704
+ browserDriver: browserEnvironment?.browserDriver,
705
+ browserName: browserEnvironment?.browserName,
706
+ browserProjectName: browserEnvironment?.browserProjectName,
432
707
  testSuiteAbsolutePath: getTestModuleFilepath(testModule),
433
708
  frameworkVersion,
434
709
  testSessionId: testSessionConfiguration.testSessionId,
435
710
  testModuleId: testSessionConfiguration.testModuleId,
436
711
  testCommand: testSessionConfiguration.testCommand,
437
712
  repositoryRoot: testSessionConfiguration.repositoryRoot,
438
- codeOwnersEntries: testSessionConfiguration.codeOwnersEntries,
713
+ codeOwnersEntries: testSessionConfiguration.codeOwnersEntries ?? undefined,
439
714
  requestErrorTags: reporterState.state.requestErrorTags,
440
715
  isTestFrameworkWorker: true,
716
+ isBrowserMode: browserEnvironment !== undefined,
441
717
  isVitestNoWorkerInitActive: true,
442
718
  }
443
719
  testSuiteStartCh.runStores(testSuiteCtx, () => {})
@@ -445,7 +721,7 @@ function createMainProcessReporter (reporterState) {
445
721
  return testSuiteCtx
446
722
  }
447
723
 
448
- function getTestReport (task, testSuiteStore) {
724
+ function getTestReport (task, testSuiteStore, browserEnvironment) {
449
725
  const result = task.result
450
726
  const testSuiteAbsolutePath = task.file?.filepath
451
727
  const testName = getTestName(task)
@@ -459,6 +735,7 @@ function createMainProcessReporter (reporterState) {
459
735
 
460
736
  if (testProperties.isAttemptToFix && task.meta?.__ddTestOptAtfStatuses?.length) {
461
737
  return getRepeatedTestReport(task, testName, testSuiteAbsolutePath, testProperties, status, {
738
+ browserEnvironment,
462
739
  errorCounts: task.meta.__ddTestOptAtfErrorCounts,
463
740
  finalStatus: getAttemptToFixFinalStatus,
464
741
  state,
@@ -470,6 +747,7 @@ function createMainProcessReporter (reporterState) {
470
747
 
471
748
  if (testProperties.isEarlyFlakeDetection && task.meta?.__ddTestOptEfdStatuses?.length) {
472
749
  return getRepeatedTestReport(task, testName, testSuiteAbsolutePath, testProperties, status, {
750
+ browserEnvironment,
473
751
  errorCounts: task.meta.__ddTestOptEfdErrorCounts,
474
752
  finalStatus: getEarlyFlakeDetectionFinalStatus,
475
753
  state,
@@ -481,8 +759,9 @@ function createMainProcessReporter (reporterState) {
481
759
 
482
760
  if (!testProperties.isAttemptToFix && task.meta?.__ddTestOptRepeatStatuses?.length) {
483
761
  return getRepeatedTestReport(task, testName, testSuiteAbsolutePath, testProperties, status, {
762
+ browserEnvironment,
484
763
  errorCounts: task.meta.__ddTestOptRepeatErrorCounts,
485
- finalStatus: () => status,
764
+ finalStatus: () => getExternalFinalStatus(status, testProperties),
486
765
  state,
487
766
  statuses: task.meta.__ddTestOptRepeatStatuses,
488
767
  testSuiteStore,
@@ -491,11 +770,18 @@ function createMainProcessReporter (reporterState) {
491
770
  }
492
771
 
493
772
  const attemptStatuses = taskAttemptStatuses.get(task.id)
494
- if (!testProperties.isAttemptToFix && !testProperties.isEarlyFlakeDetection && attemptStatuses?.length > 1) {
773
+ const retryCount = task.result?.retryCount || 0
774
+ if (
775
+ !testProperties.isAttemptToFix &&
776
+ !testProperties.isEarlyFlakeDetection &&
777
+ (attemptStatuses?.length > 1 || retryCount > 0)
778
+ ) {
495
779
  return getRepeatedTestReport(task, testName, testSuiteAbsolutePath, testProperties, status, {
496
- finalStatus: () => status,
780
+ browserEnvironment,
781
+ errorCounts: task.meta?.__ddTestOptRetryErrorCounts,
782
+ finalStatus: () => getExternalFinalStatus(status, testProperties),
497
783
  state,
498
- statuses: attemptStatuses,
784
+ statuses: getRetriedTaskStatuses(attemptStatuses, retryCount, status),
499
785
  testSuiteStore,
500
786
  type: 'external',
501
787
  })
@@ -503,6 +789,7 @@ function createMainProcessReporter (reporterState) {
503
789
 
504
790
  if (!testProperties.isAttemptToFix && task.result?.repeatCount > 0) {
505
791
  return getRepeatedTestReport(task, testName, testSuiteAbsolutePath, testProperties, status, {
792
+ browserEnvironment,
506
793
  finalStatus: () => status,
507
794
  state,
508
795
  statuses: getRepeatedTaskStatuses(task, status),
@@ -522,24 +809,14 @@ function createMainProcessReporter (reporterState) {
522
809
  result.state = 'pass'
523
810
  }
524
811
 
525
- const errors = result?.errors || []
526
- const finalErrorIndex = status === 'fail' ? errors.length - 1 : -1
527
- const nonFinalAttempts = []
528
-
529
- if (status !== 'skip') {
530
- for (let index = 0; index < errors.length; index++) {
531
- if (index === finalErrorIndex) continue
532
- nonFinalAttempts.push({
533
- error: errors[index],
534
- isRetry: index > 0,
535
- status: 'fail',
536
- })
537
- }
538
- }
812
+ const errors = browserEnvironment
813
+ ? normalizeVitestBrowserErrors(result?.errors)
814
+ : result?.errors || []
539
815
 
540
816
  return {
817
+ browserEnvironment,
541
818
  errors,
542
- nonFinalAttempts,
819
+ nonFinalAttempts: [],
543
820
  state,
544
821
  status,
545
822
  task,
@@ -643,6 +920,7 @@ function createTaskFromReportedTask (reportedTask, file, suite) {
643
920
  id: reportedTask.id,
644
921
  type: reportedTask.type,
645
922
  name: reportedTask.name || reportedTask.relativeModuleId || reportedTask.moduleId,
923
+ location: reportedTask.location,
646
924
  mode: reportedTask.options?.mode,
647
925
  meta: getReportedTaskMeta(reportedTask),
648
926
  result: getReportedTaskResult(reportedTask),
@@ -731,8 +1009,10 @@ function getTestModuleProjectName (testModule) {
731
1009
 
732
1010
  function getRepeatedTestReport (task, testName, testSuiteAbsolutePath, testProperties, status, options) {
733
1011
  const result = task.result
734
- const errors = result?.errors || []
735
- const { errorCounts, finalStatus, state, statuses, testSuiteStore, type } = options
1012
+ const { browserEnvironment, errorCounts, finalStatus, state, statuses, testSuiteStore, type } = options
1013
+ const errors = browserEnvironment
1014
+ ? normalizeVitestBrowserErrors(result?.errors)
1015
+ : result?.errors || []
736
1016
  const finalAttemptStatus = finalStatus(statuses)
737
1017
  const hasFailure = finalAttemptStatus === 'fail'
738
1018
  const attempts = []
@@ -757,7 +1037,9 @@ function getRepeatedTestReport (task, testName, testSuiteAbsolutePath, testPrope
757
1037
  errorIndex++
758
1038
  }
759
1039
  const attempt = {
1040
+ index,
760
1041
  attemptToFixFailed: type === 'attempt_to_fix' && isFinalAttempt && hasFailure,
1042
+ duration: task.meta?.__ddTestOptAttemptDurations?.[index],
761
1043
  earlyFlakeAbortReason: type === 'early_flake_detection' && isFinalAttempt
762
1044
  ? task.meta?.__ddTestOptEfdAbortReason
763
1045
  : undefined,
@@ -783,6 +1065,7 @@ function getRepeatedTestReport (task, testName, testSuiteAbsolutePath, testPrope
783
1065
  }
784
1066
 
785
1067
  return {
1068
+ browserEnvironment,
786
1069
  errors,
787
1070
  finalAttempt: attempts.at(-1),
788
1071
  nonFinalAttempts: attempts.slice(0, -1),
@@ -841,6 +1124,17 @@ function getEarlyFlakeDetectionFinalStatus (statuses) {
841
1124
  return statuses.includes('pass') ? 'pass' : 'fail'
842
1125
  }
843
1126
 
1127
+ /**
1128
+ * Returns the final status for user-configured retries and repeats.
1129
+ *
1130
+ * @param {string} status
1131
+ * @param {{ isDisabled?: boolean, isQuarantined?: boolean }} testProperties
1132
+ * @returns {string}
1133
+ */
1134
+ function getExternalFinalStatus (status, testProperties) {
1135
+ return testProperties.isDisabled || testProperties.isQuarantined ? 'skip' : status
1136
+ }
1137
+
844
1138
  function getRepeatedTaskStatuses (task, status) {
845
1139
  const repeatedStatuses = []
846
1140
  const repeatCount = task.result?.repeatCount || 0
@@ -850,8 +1144,27 @@ function getRepeatedTaskStatuses (task, status) {
850
1144
  return repeatedStatuses
851
1145
  }
852
1146
 
1147
+ /**
1148
+ * Completes reporter retry events with Vitest's final retry count and status.
1149
+ *
1150
+ * @param {string[]} recordedStatuses
1151
+ * @param {number} retryCount
1152
+ * @param {string} finalStatus
1153
+ * @returns {string[]}
1154
+ */
1155
+ function getRetriedTaskStatuses (recordedStatuses = [], retryCount, finalStatus) {
1156
+ const statuses = [...recordedStatuses]
1157
+ const attemptCount = Math.max(statuses.length, retryCount + 1)
1158
+ while (statuses.length < attemptCount - 1) {
1159
+ statuses.push('fail')
1160
+ }
1161
+ statuses[attemptCount - 1] = finalStatus
1162
+ return statuses
1163
+ }
1164
+
853
1165
  function reportFinalTestAttempt (testReport) {
854
1166
  const {
1167
+ browserEnvironment,
855
1168
  errors,
856
1169
  state,
857
1170
  status,
@@ -863,13 +1176,21 @@ function reportFinalTestAttempt (testReport) {
863
1176
  } = testReport
864
1177
 
865
1178
  if (status === 'skip') {
1179
+ const {
1180
+ isRumActive,
1181
+ testExecutionId,
1182
+ } = getRumCorrelation(task)
866
1183
  testSkipCh.publish({
1184
+ ...browserEnvironment,
867
1185
  testName,
868
1186
  testSuiteAbsolutePath,
869
1187
  isNew: testProperties.isNew,
870
1188
  isDisabled: testProperties.isDisabled,
1189
+ isRumActive,
871
1190
  isTestFrameworkWorker: true,
872
1191
  requestErrorTags: state.requestErrorTags,
1192
+ testStartLine: task.location?.line,
1193
+ testExecutionId,
873
1194
  ...testSuiteStore,
874
1195
  })
875
1196
  return
@@ -880,7 +1201,7 @@ function reportFinalTestAttempt (testReport) {
880
1201
  const finalAttempt = testReport.finalAttempt
881
1202
 
882
1203
  if (status === 'fail') {
883
- const error = errors.at(-1) || errors[0]
1204
+ const error = finalAttempt?.error || errors[0]
884
1205
  reportTestAttempt(testReport, finalAttempt || {
885
1206
  error,
886
1207
  finalStatus,
@@ -892,7 +1213,7 @@ function reportFinalTestAttempt (testReport) {
892
1213
  true,
893
1214
  true
894
1215
  ),
895
- isRetry: errors.length > 1 || (result?.retryCount || 0) > 0 || (result?.repeatCount || 0) > 0,
1216
+ isRetry: (result?.retryCount || 0) > 0 || (result?.repeatCount || 0) > 0,
896
1217
  status: 'fail',
897
1218
  })
898
1219
  return finalStatus === 'skip' ? undefined : error
@@ -900,13 +1221,14 @@ function reportFinalTestAttempt (testReport) {
900
1221
 
901
1222
  reportTestAttempt(testReport, finalAttempt || {
902
1223
  finalStatus,
903
- isRetry: errors.length > 0 || (result?.retryCount || 0) > 0 || (result?.repeatCount || 0) > 0,
1224
+ isRetry: (result?.retryCount || 0) > 0 || (result?.repeatCount || 0) > 0,
904
1225
  status: 'pass',
905
1226
  })
906
1227
  }
907
1228
 
908
1229
  function reportTestAttempt (testReport, attempt) {
909
1230
  const {
1231
+ browserEnvironment,
910
1232
  state,
911
1233
  task,
912
1234
  testName,
@@ -916,7 +1238,27 @@ function reportTestAttempt (testReport, attempt) {
916
1238
  } = testReport
917
1239
  const result = task.result
918
1240
  const status = attempt.status
1241
+ const {
1242
+ isRumActive,
1243
+ testExecutionId,
1244
+ } = getRumCorrelation(task, attempt.index)
1245
+ const attemptStartTimes = task.meta?.__ddTestOptAttemptStartTimes
1246
+ const attemptStartIndex = attempt.index ?? (attemptStartTimes?.length || 1) - 1
1247
+ const attemptStartTime = attemptStartTimes?.[attemptStartIndex]
1248
+ let duration
1249
+ if (status === 'pass') {
1250
+ if (
1251
+ attempt.duration !== undefined ||
1252
+ (!attempt.isRetry && isFinalTestAttempt(testReport, attempt) && result?.duration !== undefined)
1253
+ ) {
1254
+ duration = attempt.duration ?? result.duration
1255
+ }
1256
+ } else {
1257
+ duration = attempt.duration ??
1258
+ (shouldUseTaskDurationForFailure(testReport, attempt) ? result?.duration : undefined)
1259
+ }
919
1260
  const testCtx = {
1261
+ ...browserEnvironment,
920
1262
  currentStore: testSuiteStore,
921
1263
  testName,
922
1264
  testSuiteAbsolutePath,
@@ -931,8 +1273,14 @@ function reportTestAttempt (testReport, attempt) {
931
1273
  isRetryReasonAttemptToFix: testProperties.isAttemptToFix && attempt.isRetry,
932
1274
  isRetryReasonAtr: !testProperties.isAttemptToFix && !testProperties.isEarlyFlakeDetection &&
933
1275
  testProperties.isFlakyTestRetries,
1276
+ isRumActive,
934
1277
  isTestFrameworkWorker: true,
935
1278
  requestErrorTags: state.requestErrorTags,
1279
+ startTime: Number.isFinite(attemptStartTime)
1280
+ ? attemptStartTime
1281
+ : (Number.isFinite(duration) && duration >= 0 ? Date.now() - duration : undefined),
1282
+ testStartLine: task.location?.line,
1283
+ testExecutionId,
936
1284
  }
937
1285
  if (testProperties.isAttemptToFix) {
938
1286
  recordAttemptToFixExecution(state.attemptToFixExecutions, {
@@ -954,13 +1302,8 @@ function reportTestAttempt (testReport, attempt) {
954
1302
  testStartCh.runStores(testCtx, () => {})
955
1303
  testCtx.status = status
956
1304
  testCtx.task = task
957
- if (
958
- status === 'pass' &&
959
- !attempt.isRetry &&
960
- isFinalTestAttempt(testReport, attempt) &&
961
- result?.duration !== undefined
962
- ) {
963
- testCtx.duration = result.duration
1305
+ if (status === 'pass' && duration !== undefined) {
1306
+ testCtx.duration = duration
964
1307
  }
965
1308
  testFinishTimeCh.runStores(testCtx, () => {})
966
1309
 
@@ -975,7 +1318,7 @@ function reportTestAttempt (testReport, attempt) {
975
1318
  }
976
1319
 
977
1320
  testErrorCh.publish({
978
- duration: shouldUseTaskDurationForFailure(testReport, attempt) ? result?.duration : undefined,
1321
+ duration,
979
1322
  error: attempt.error,
980
1323
  earlyFlakeAbortReason: attempt.earlyFlakeAbortReason,
981
1324
  finalStatus: attempt.finalStatus,
@@ -985,6 +1328,24 @@ function reportTestAttempt (testReport, attempt) {
985
1328
  })
986
1329
  }
987
1330
 
1331
+ /**
1332
+ * Returns the browser RUM correlation metadata for a test attempt.
1333
+ *
1334
+ * @param {object} task
1335
+ * @param {number|undefined} attemptIndex
1336
+ * @returns {{ isRumActive?: boolean, testExecutionId?: string }}
1337
+ */
1338
+ function getRumCorrelation (task, attemptIndex) {
1339
+ const testExecutionIds = task.meta?.__ddTestOptRumTestExecutionIds
1340
+ if (!Array.isArray(testExecutionIds) || testExecutionIds.length === 0) return {}
1341
+
1342
+ attemptIndex ??= testExecutionIds.length - 1
1343
+ return {
1344
+ isRumActive: task.meta?.__ddTestOptRumActive?.[attemptIndex] === true,
1345
+ testExecutionId: testExecutionIds[attemptIndex],
1346
+ }
1347
+ }
1348
+
988
1349
  function isFinalTestAttempt (testReport, attempt) {
989
1350
  return attempt.finalStatus !== undefined || testReport.finalAttempt === undefined
990
1351
  }
@@ -1207,10 +1568,11 @@ function getTestSpecificationPool (testSpecification) {
1207
1568
  const options = getTestSpecificationOptions(testSpecification)
1208
1569
  const file = getTestSpecificationFile(testSpecification)
1209
1570
  const project = getTestSpecificationProject(testSpecification)
1571
+ const projectConfig = safeConfig(project)
1210
1572
  return options?.pool ||
1211
1573
  file?.pool ||
1212
1574
  testSpecification?.pool ||
1213
- project?.config?.pool ||
1575
+ projectConfig?.pool ||
1214
1576
  project?.serializedConfig?.pool ||
1215
1577
  project?.pool
1216
1578
  }
@@ -1243,15 +1605,16 @@ function getTestSpecificationIsolate (testSpecification, pool) {
1243
1605
  const options = getTestSpecificationOptions(testSpecification)
1244
1606
  const file = getTestSpecificationFile(testSpecification)
1245
1607
  const project = getTestSpecificationProject(testSpecification)
1608
+ const projectConfig = safeConfig(project)
1246
1609
  return getPoolOptionsIsolate(options, pool) ??
1247
1610
  getPoolOptionsIsolate(file, pool) ??
1248
- getPoolOptionsIsolate(project?.config, pool) ??
1611
+ getPoolOptionsIsolate(projectConfig, pool) ??
1249
1612
  getPoolOptionsIsolate(project?.serializedConfig, pool) ??
1250
1613
  getPoolOptionsIsolate(project, pool) ??
1251
1614
  getPoolOptionsIsolate(testSpecification, pool) ??
1252
1615
  options?.isolate ??
1253
1616
  file?.isolate ??
1254
- project?.config?.isolate ??
1617
+ projectConfig?.isolate ??
1255
1618
  project?.serializedConfig?.isolate ??
1256
1619
  project?.isolate ??
1257
1620
  testSpecification?.isolate
@@ -1263,7 +1626,7 @@ function getEffectiveTestSpecificationIsolate (testSpecification, pool, defaultI
1263
1626
  }
1264
1627
 
1265
1628
  function getProjectName (project) {
1266
- return normalizeProjectName(project?.name || project?.config?.name || project?.test?.name)
1629
+ return normalizeProjectName(project?.name || safeConfig(project)?.name || project?.test?.name)
1267
1630
  }
1268
1631
 
1269
1632
  function normalizeProjectName (name) {
@@ -1273,6 +1636,111 @@ function normalizeProjectName (name) {
1273
1636
  return typeof label === 'string' ? label : undefined
1274
1637
  }
1275
1638
 
1639
+ /**
1640
+ * Returns whether a Vitest specification runs in Browser Mode.
1641
+ *
1642
+ * @param {object} testSpecification
1643
+ * @returns {boolean}
1644
+ */
1645
+ function isBrowserTestSpecification (testSpecification) {
1646
+ return getTestSpecificationPool(testSpecification) === 'browser' ||
1647
+ isBrowserProject(getTestSpecificationProject(testSpecification))
1648
+ }
1649
+
1650
+ /**
1651
+ * Returns whether a Vitest project has Browser Mode enabled.
1652
+ *
1653
+ * @param {object|undefined} project
1654
+ * @returns {boolean}
1655
+ */
1656
+ function isBrowserProject (project) {
1657
+ try {
1658
+ if (project?.isBrowserEnabled?.() === true) return true
1659
+ } catch {}
1660
+ return getProjectReportingConfig(project)?.browser?.enabled === true
1661
+ }
1662
+
1663
+ /**
1664
+ * Returns whether Vitest can overlap tests or their setup and teardown.
1665
+ *
1666
+ * @param {object|undefined} config
1667
+ * @returns {boolean}
1668
+ */
1669
+ function hasConcurrentTestExecution (config) {
1670
+ const sequence = config?.sequence
1671
+ return sequence?.concurrent === true ||
1672
+ sequence?.hooks === 'parallel' ||
1673
+ sequence?.setupFiles === 'parallel'
1674
+ }
1675
+
1676
+ /**
1677
+ * Returns whether Vitest can overlap browser execution that shares the RUM correlation cookie origin.
1678
+ *
1679
+ * @param {object} ctx
1680
+ * @param {object[]|undefined} testSpecifications
1681
+ * @returns {boolean}
1682
+ */
1683
+ function canRaceRumCorrelation (ctx, testSpecifications) {
1684
+ if (!Array.isArray(testSpecifications)) {
1685
+ return hasConcurrentTestExecution(safeConfig(ctx))
1686
+ }
1687
+
1688
+ let browserFileCount = 0
1689
+ let hasBrowserProject = false
1690
+ let project
1691
+ for (const testSpecification of testSpecifications) {
1692
+ const testProject = getTestSpecificationProject(testSpecification)
1693
+ if (!isBrowserTestSpecification(testSpecification)) continue
1694
+
1695
+ const config = getProjectReportingConfig(testProject) || safeConfig(ctx)
1696
+ if (hasConcurrentTestExecution(config)) return true
1697
+
1698
+ browserFileCount++
1699
+ if (hasBrowserProject && testProject !== project) return true
1700
+ project = testProject
1701
+ hasBrowserProject = true
1702
+ }
1703
+ if (browserFileCount < 2) return false
1704
+
1705
+ const config = getProjectReportingConfig(project) || safeConfig(ctx)
1706
+ const fileParallelism = config?.browser?.fileParallelism ?? config?.fileParallelism
1707
+ return fileParallelism !== false && config?.maxWorkers !== 1
1708
+ }
1709
+
1710
+ function getTestModuleBrowserEnvironment (testModule) {
1711
+ const project = testModule?.project
1712
+ const config = getProjectReportingConfig(project)
1713
+ const isBrowserMode = testModule?.task?.pool === 'browser' ||
1714
+ testModule?.pool === 'browser' ||
1715
+ isBrowserProject(project)
1716
+ if (!isBrowserMode) return
1717
+
1718
+ const provider = config?.browser?.provider
1719
+ const browserDriver = typeof provider === 'string' ? provider : provider?.name
1720
+
1721
+ return {
1722
+ browserDriver,
1723
+ browserName: config?.browser?.name,
1724
+ browserProjectName: getTestModuleProjectName(testModule),
1725
+ isBrowserMode: true,
1726
+ }
1727
+ }
1728
+
1729
+ /**
1730
+ * Returns the runtime or serialized project configuration used for read-only reporting metadata.
1731
+ *
1732
+ * @param {object|undefined} project
1733
+ * @returns {object|undefined}
1734
+ */
1735
+ function getProjectReportingConfig (project) {
1736
+ const config = safeConfig(project)
1737
+ if (config) return config
1738
+
1739
+ try {
1740
+ return project?.serializedConfig
1741
+ } catch {}
1742
+ }
1743
+
1276
1744
  function safeConfig (project) {
1277
1745
  let config
1278
1746
  try {
@@ -1293,5 +1761,6 @@ module.exports = {
1293
1761
  configure,
1294
1762
  deactivate,
1295
1763
  configureWorkerEnv,
1764
+ isSupportedVersion,
1296
1765
  shouldUse,
1297
1766
  }