dd-trace 5.118.0 → 5.119.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
@@ -1,14 +1,10 @@
1
- import { realpathSync } from 'node:fs'
2
- import { relative } from 'node:path'
3
- import { performance } from 'node:perf_hooks'
1
+ import { afterEach, beforeAll, beforeEach, inject } from 'vitest'
4
2
 
5
- import { afterEach, beforeAll, beforeEach } from 'vitest'
6
-
7
- // Instrumentation-less worker setup for DD_EXPERIMENTAL_TEST_OPT_VITEST_NO_WORKER_INIT.
8
- // It applies Test Optimization execution changes without initializing dd-trace in Vitest workers.
3
+ // Instrumentation-less setup for DD_EXPERIMENTAL_TEST_OPT_VITEST_NO_WORKER_INIT.
4
+ // It applies Test Optimization execution changes without initializing dd-trace and also supports Browser Mode.
9
5
  const VITEST_NO_WORKER_INIT_ACTIVE_ENV = 'DD_TEST_OPT_VITEST_NO_WORKER_INIT_ACTIVE'
10
- const isNoWorkerInitActive = getIsNoWorkerInitActive()
11
- const providedContext = isNoWorkerInitActive ? getProvidedContext() : {}
6
+ const providedContext = getProvidedContext()
7
+ const isNoWorkerInitActive = providedContext.isActive ?? getIsNoWorkerInitActive()
12
8
  const attemptToFixTests = providedContext.attemptToFixTests || {}
13
9
  const attemptToFixRetries = providedContext.attemptToFixRetries || 0
14
10
  const disabledTests = providedContext.disabledTests || {}
@@ -22,7 +18,9 @@ const isEarlyFlakeDetectionEnabled = providedContext.isEarlyFlakeDetectionEnable
22
18
  const knownTests = providedContext.knownTests || {}
23
19
  const modifiedFiles = providedContext.modifiedFiles || {}
24
20
  const quarantinedTests = providedContext.quarantinedTests || {}
25
- const repositoryRoot = realpath(providedContext.repositoryRoot || process.cwd())
21
+ const isRumCorrelationEnabled = providedContext.isRumCorrelationEnabled !== false
22
+ const rumTestExecutionIdCookieName = providedContext.rumTestExecutionIdCookieName
23
+ const testPropertiesByFilepath = providedContext.testPropertiesByFilepath || {}
26
24
  let setVitestTaskFn
27
25
  if (isNoWorkerInitActive) {
28
26
  try {
@@ -35,7 +33,21 @@ const earlyFlakeDetectionRetriesByTask = new WeakMap()
35
33
  const earlyFlakeDetectionSkippedResults = new WeakMap()
36
34
  const earlyFlakeDetectionStartByTask = new WeakMap()
37
35
  const nextAttemptIndexByTask = new WeakMap()
38
- const realpaths = new Map()
36
+ const retryAttemptIndexByTask = new WeakMap()
37
+ const usedRumTestExecutionIds = new Set()
38
+ let now
39
+ let timeOrigin
40
+ // Use an unfaked monotonic clock in Node and Vitest's parent frame in Browser Mode.
41
+ if (typeof globalThis.process?.uptime === 'function') {
42
+ now = () => globalThis.process.uptime() * 1000
43
+ timeOrigin = Date.now() - now()
44
+ } else {
45
+ const clock = globalThis.window?.parent && globalThis.window.parent !== globalThis.window
46
+ ? globalThis.window.parent.performance
47
+ : globalThis.performance
48
+ now = clock ? clock.now.bind(clock) : Date.now
49
+ timeOrigin = Number.isFinite(clock?.timeOrigin) ? clock.timeOrigin : Date.now() - now()
50
+ }
39
51
 
40
52
  if (isNoWorkerInitActive) {
41
53
  // eslint-disable-next-line no-empty-pattern
@@ -51,11 +63,12 @@ if (isNoWorkerInitActive) {
51
63
  const isEarlyFlakeDetectionTestAttempt = isEarlyFlakeDetectionTest(testSuite, testName)
52
64
  const isQuarantinedTest = quarantinedTests[testSuite]?.[testName] && !isAttemptToFixTest
53
65
  const attemptIndex = getNextAttemptIndex(task)
66
+ const attemptStart = now()
54
67
  if (attemptIndex > 0) {
55
68
  recordTestOptimizationStatus(task, attemptIndex - 1)
56
69
  }
57
70
 
58
- if (disabledTests[testSuite]?.[testName] && !isAttemptToFixTest) {
71
+ if (!isAttemptToFixTest && disabledTests[testSuite]?.[testName]) {
59
72
  skip('Skipped by Datadog Test Optimization')
60
73
  } else if (isAttemptToFixTest && attemptIndex > 0) {
61
74
  task.result.state = 'run'
@@ -66,27 +79,33 @@ if (isNoWorkerInitActive) {
66
79
  }
67
80
  }
68
81
 
69
- if (isAttemptToFixTest || isEarlyFlakeDetectionTestAttempt || isQuarantinedTest) {
70
- onTestFinished(() => {
71
- if (attemptIndex === getFinalAttemptIndex(task)) {
72
- if (isAttemptToFixTest || isEarlyFlakeDetectionTestAttempt) {
73
- recordTestOptimizationStatus(task, attemptIndex, true)
74
- }
75
- switchQuarantinedFinalFailure(task, attemptIndex)
82
+ prepareRumCorrelation(task, attemptIndex)
83
+
84
+ onTestFinished(() => {
85
+ recordTestAttemptTiming(task, attemptIndex, attemptStart)
86
+ recordRetryErrorCount(task)
87
+ if (
88
+ (isAttemptToFixTest || isEarlyFlakeDetectionTestAttempt || isQuarantinedTest) &&
89
+ attemptIndex === getFinalAttemptIndex(task)
90
+ ) {
91
+ if (isAttemptToFixTest || isEarlyFlakeDetectionTestAttempt) {
92
+ recordTestOptimizationStatus(task, attemptIndex, true)
76
93
  }
77
- })
78
- }
94
+ switchQuarantinedFinalFailure(task, attemptIndex)
95
+ }
96
+ finishRumCorrelation(task, attemptIndex)
97
+ })
79
98
  })
80
99
 
81
100
  afterEach(function ({ task }) {
82
101
  const attemptIndex = task.meta.__ddTestOptCurrentAttemptIndex
83
- if (restoreEarlyFlakeDetectionSkippedResult(task)) {
84
- return
85
- }
86
- if (attemptIndex === getFinalAttemptIndex(task)) {
102
+ const restoredEarlyFlakeDetectionResult = restoreEarlyFlakeDetectionSkippedResult(task)
103
+ if (!restoredEarlyFlakeDetectionResult && attemptIndex === getFinalAttemptIndex(task)) {
87
104
  recordTestOptimizationStatus(task, attemptIndex)
88
105
  }
89
- switchQuarantinedFinalFailure(task, attemptIndex)
106
+ if (!restoredEarlyFlakeDetectionResult) {
107
+ switchQuarantinedFinalFailure(task, attemptIndex)
108
+ }
90
109
  })
91
110
  }
92
111
 
@@ -110,6 +129,7 @@ function applyExecutionChanges (suite) {
110
129
  task.repeats = earlyFlakeDetectionRetries
111
130
  task.meta.__ddTestOptEfdRetries = earlyFlakeDetectionRetries
112
131
  }
132
+ wrapRetryCondition(task)
113
133
  }
114
134
  }
115
135
 
@@ -117,9 +137,194 @@ function getNextAttemptIndex (task) {
117
137
  const attemptIndex = nextAttemptIndexByTask.get(task) || 0
118
138
  nextAttemptIndexByTask.set(task, attemptIndex + 1)
119
139
  task.meta.__ddTestOptCurrentAttemptIndex = attemptIndex
140
+
141
+ const repeatCount = task.result?.repeatCount || 0
142
+ const retryAttempt = retryAttemptIndexByTask.get(task)
143
+ const retryAttemptIndex = retryAttempt?.repeatCount === repeatCount ? retryAttempt.index + 1 : 0
144
+ retryAttemptIndexByTask.set(task, {
145
+ index: retryAttemptIndex,
146
+ repeatCount,
147
+ })
148
+
120
149
  return attemptIndex
121
150
  }
122
151
 
152
+ /**
153
+ * Sets the RUM correlation cookie for a sequential browser test attempt.
154
+ *
155
+ * @param {object} task
156
+ * @param {number} attemptIndex
157
+ * @returns {void}
158
+ */
159
+ function prepareRumCorrelation (task, attemptIndex) {
160
+ // A per-origin cookie cannot identify overlapping attempts without racing.
161
+ if (!isRumCorrelationEnabled || isConcurrentTask(task)) return
162
+
163
+ if (
164
+ typeof window === 'undefined' ||
165
+ typeof document === 'undefined' ||
166
+ typeof rumTestExecutionIdCookieName !== 'string'
167
+ ) {
168
+ return
169
+ }
170
+
171
+ const testExecutionId = generateTestExecutionId()
172
+ if (
173
+ !testExecutionId ||
174
+ usedRumTestExecutionIds.has(testExecutionId) ||
175
+ !setRumCorrelationCookie(testExecutionId)
176
+ ) {
177
+ return
178
+ }
179
+
180
+ usedRumTestExecutionIds.add(testExecutionId)
181
+ task.meta.__ddTestOptRumTestExecutionIds ||= []
182
+ task.meta.__ddTestOptRumTestExecutionIds[attemptIndex] = testExecutionId
183
+ recordRumActivity(task, attemptIndex)
184
+ }
185
+
186
+ /**
187
+ * Records whether RUM was active and clears the attempt cookie.
188
+ *
189
+ * @param {object} task
190
+ * @param {number} attemptIndex
191
+ * @returns {void}
192
+ */
193
+ function finishRumCorrelation (task, attemptIndex) {
194
+ const testExecutionId = task.meta.__ddTestOptRumTestExecutionIds?.[attemptIndex]
195
+ if (!testExecutionId) return
196
+
197
+ recordRumActivity(task, attemptIndex)
198
+ clearRumCorrelationCookie(testExecutionId)
199
+ }
200
+
201
+ /**
202
+ * Retains evidence that RUM was active at any point observed during the attempt.
203
+ *
204
+ * @param {object} task
205
+ * @param {number} attemptIndex
206
+ * @returns {void}
207
+ */
208
+ function recordRumActivity (task, attemptIndex) {
209
+ if (!getIsRumActive(getRum())) return
210
+
211
+ task.meta.__ddTestOptRumActive ||= []
212
+ task.meta.__ddTestOptRumActive[attemptIndex] = true
213
+ }
214
+
215
+ /**
216
+ * Returns the RUM public API without letting application-defined accessors fail a test.
217
+ *
218
+ * @returns {object|undefined}
219
+ */
220
+ function getRum () {
221
+ try {
222
+ return window.DD_RUM
223
+ } catch {}
224
+ }
225
+
226
+ /**
227
+ * Returns whether the current RUM session is active.
228
+ *
229
+ * @param {object|undefined} rum
230
+ * @returns {boolean}
231
+ */
232
+ function getIsRumActive (rum) {
233
+ if (!rum) return false
234
+ if (typeof rum.getInternalContext !== 'function') return false
235
+
236
+ try {
237
+ return !!rum.getInternalContext()
238
+ } catch {
239
+ return false
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Returns whether a task or any containing suite is concurrent.
245
+ *
246
+ * @param {object} task
247
+ * @returns {boolean}
248
+ */
249
+ function isConcurrentTask (task) {
250
+ let currentTask = task
251
+ while (currentTask) {
252
+ if (currentTask.concurrent === true) return true
253
+ currentTask = currentTask.suite
254
+ }
255
+ return false
256
+ }
257
+
258
+ /**
259
+ * Generates a positive 63-bit decimal test execution ID.
260
+ *
261
+ * @returns {string|undefined}
262
+ */
263
+ function generateTestExecutionId () {
264
+ // This setup file executes in a browser even though lint validates it against the package's Node.js target.
265
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
266
+ if (typeof globalThis.crypto?.getRandomValues !== 'function') return
267
+
268
+ const words = new Uint32Array(2)
269
+ try {
270
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
271
+ globalThis.crypto.getRandomValues(words)
272
+ } catch {
273
+ return
274
+ }
275
+
276
+ words[0] &= 0x7F_FF_FF_FF
277
+ if (words[0] === 0 && words[1] === 0) return
278
+
279
+ return ((BigInt(words[0]) << 32n) | BigInt(words[1])).toString(10)
280
+ }
281
+
282
+ /**
283
+ * Sets and verifies the RUM correlation cookie for the current origin.
284
+ *
285
+ * @param {string} testExecutionId
286
+ * @returns {boolean}
287
+ */
288
+ function setRumCorrelationCookie (testExecutionId) {
289
+ try {
290
+ // eslint-disable-next-line unicorn/no-document-cookie
291
+ document.cookie = `${rumTestExecutionIdCookieName}=${testExecutionId}; path=/`
292
+ return getRumCorrelationCookie() === testExecutionId
293
+ } catch {
294
+ return false
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Clears the RUM correlation cookie if it still belongs to this attempt.
300
+ *
301
+ * @param {string} testExecutionId
302
+ * @returns {void}
303
+ */
304
+ function clearRumCorrelationCookie (testExecutionId) {
305
+ try {
306
+ if (getRumCorrelationCookie() === testExecutionId) {
307
+ // eslint-disable-next-line unicorn/no-document-cookie
308
+ document.cookie = `${rumTestExecutionIdCookieName}=; path=/; max-age=0`
309
+ }
310
+ } catch {}
311
+ }
312
+
313
+ /**
314
+ * Returns the current RUM correlation cookie value.
315
+ *
316
+ * @returns {string|undefined}
317
+ */
318
+ function getRumCorrelationCookie () {
319
+ const prefix = `${rumTestExecutionIdCookieName}=`
320
+ for (const cookie of document.cookie.split(';')) {
321
+ const normalizedCookie = cookie.trim()
322
+ if (normalizedCookie.startsWith(prefix)) {
323
+ return normalizedCookie.slice(prefix.length)
324
+ }
325
+ }
326
+ }
327
+
123
328
  function recordTestOptimizationStatus (task, attemptIndex = task.result?.repeatCount || 0, onlyIfNewErrors = false) {
124
329
  const testSuite = getTestSuite(task)
125
330
  const testName = getTestName(task)
@@ -206,6 +411,76 @@ function recordManualRepeatStatus (task, attemptIndex) {
206
411
  task.meta.__ddTestOptRepeatErrorCounts[attemptIndex] = task.result?.errors?.length || 0
207
412
  }
208
413
 
414
+ /**
415
+ * Records cumulative errors at the end of each configured retry attempt.
416
+ *
417
+ * @param {object} task
418
+ * @returns {void}
419
+ */
420
+ function recordRetryErrorCount (task) {
421
+ const retryLimit = getRetryLimit(task)
422
+ if (retryLimit <= 0) return
423
+
424
+ const retryCount = task.result?.retryCount || 0
425
+ task.meta.__ddTestOptRetryErrorCounts ||= []
426
+ task.meta.__ddTestOptRetryErrorCounts[retryCount] = task.result?.errors?.length || 0
427
+ }
428
+
429
+ /**
430
+ * Records the wall-clock start and elapsed time for one retry or repeat execution.
431
+ *
432
+ * @param {object} task
433
+ * @param {number} attemptIndex
434
+ * @param {number} attemptStart
435
+ * @returns {void}
436
+ */
437
+ function recordTestAttemptTiming (task, attemptIndex, attemptStart) {
438
+ task.meta.__ddTestOptAttemptStartTimes ||= []
439
+ task.meta.__ddTestOptAttemptDurations ||= []
440
+ task.meta.__ddTestOptAttemptStartTimes[attemptIndex] = timeOrigin + attemptStart
441
+ task.meta.__ddTestOptAttemptDurations[attemptIndex] = now() - attemptStart
442
+ }
443
+
444
+ /**
445
+ * Returns the configured retry count for numeric and object-form retry options.
446
+ *
447
+ * @param {object} task
448
+ * @returns {number}
449
+ */
450
+ function getRetryLimit (task) {
451
+ return typeof task.retry === 'number' ? task.retry : task.retry?.count || 0
452
+ }
453
+
454
+ /**
455
+ * Wraps Vitest's retry condition so final-attempt handling follows the condition's actual result.
456
+ *
457
+ * @param {object} task
458
+ * @returns {void}
459
+ */
460
+ function wrapRetryCondition (task) {
461
+ if (typeof task.retry !== 'object' || !task.retry?.condition || getRetryLimit(task) === 0) return
462
+
463
+ const condition = task.retry.condition
464
+ task.retry = {
465
+ ...task.retry,
466
+ condition (error) {
467
+ let shouldRetry = false
468
+ if (condition instanceof RegExp) {
469
+ shouldRetry = condition.test(error?.message || '')
470
+ } else if (typeof condition === 'function') {
471
+ shouldRetry = condition(error)
472
+ }
473
+
474
+ if (!shouldRetry && (task.result?.repeatCount || 0) >= (task.repeats || 0)) {
475
+ const attemptIndex = task.meta.__ddTestOptCurrentAttemptIndex
476
+ recordTestOptimizationStatus(task, attemptIndex)
477
+ markQuarantinedFailure(task)
478
+ }
479
+ return shouldRetry
480
+ },
481
+ }
482
+ }
483
+
209
484
  function restoreEarlyFlakeDetectionSkippedResult (task) {
210
485
  if (!task.meta.__ddTestOptEfdSkipCurrentAttempt) {
211
486
  return false
@@ -236,7 +511,18 @@ function getFinalAttemptIndex (task) {
236
511
  if (isEarlyFlakeDetectionTest(testSuite, testName)) {
237
512
  return getEarlyFlakeDetectionRetryCountForTask(task)
238
513
  }
239
- return task.repeats
514
+
515
+ const attemptIndex = task.meta.__ddTestOptCurrentAttemptIndex
516
+ const repeatCount = task.result?.repeatCount || 0
517
+ const repeatLimit = task.repeats || 0
518
+ const retryAttemptIndex = retryAttemptIndexByTask.get(task)?.index || 0
519
+ const retryLimit = getRetryLimit(task)
520
+ const retriesRemaining = task.result?.state === 'fail' || task.result?.state === 'run'
521
+ ? Math.max(retryLimit - retryAttemptIndex, 0)
522
+ : 0
523
+ const repeatsRemaining = Math.max(repeatLimit - repeatCount, 0)
524
+
525
+ return attemptIndex + retriesRemaining + (repeatsRemaining * (retryLimit + 1))
240
526
  }
241
527
 
242
528
  function switchQuarantinedFinalFailure (task, attemptIndex) {
@@ -250,9 +536,27 @@ function switchQuarantinedFinalFailure (task, attemptIndex) {
250
536
  return
251
537
  }
252
538
 
253
- const retryCount = task.result?.retryCount || 0
254
- const retryLimit = task.retry || 0
255
- if (retryCount < retryLimit || attemptIndex < getFinalAttemptIndex(task)) {
539
+ if (attemptIndex < getFinalAttemptIndex(task)) {
540
+ return
541
+ }
542
+
543
+ markQuarantinedFailure(task)
544
+ }
545
+
546
+ /**
547
+ * Converts a quarantined failure into the passing runner state expected by Vitest.
548
+ *
549
+ * @param {object} task
550
+ * @returns {void}
551
+ */
552
+ function markQuarantinedFailure (task) {
553
+ const testSuite = getTestSuite(task)
554
+ const testName = getTestName(task)
555
+ if (
556
+ !quarantinedTests[testSuite]?.[testName] ||
557
+ attemptToFixTests[testSuite]?.[testName] ||
558
+ task.result?.state !== 'fail'
559
+ ) {
256
560
  return
257
561
  }
258
562
 
@@ -292,7 +596,7 @@ function getPreviousErrorCount (errorCounts, repeatCount) {
292
596
 
293
597
  function prepareEarlyFlakeDetectionAttempt (task, attemptIndex) {
294
598
  if (attemptIndex === 0) {
295
- earlyFlakeDetectionStartByTask.set(task, performance.now())
599
+ earlyFlakeDetectionStartByTask.set(task, now())
296
600
  return false
297
601
  }
298
602
 
@@ -308,12 +612,12 @@ function prepareEarlyFlakeDetectionAttempt (task, attemptIndex) {
308
612
  }
309
613
 
310
614
  if (attemptIndex <= retryCount) {
311
- earlyFlakeDetectionStartByTask.set(task, performance.now())
615
+ earlyFlakeDetectionStartByTask.set(task, now())
312
616
  return false
313
617
  }
314
618
 
315
619
  if (!canReplaceVitestTaskFn()) {
316
- earlyFlakeDetectionStartByTask.set(task, performance.now())
620
+ earlyFlakeDetectionStartByTask.set(task, now())
317
621
  return false
318
622
  }
319
623
 
@@ -334,7 +638,7 @@ function getEarlyFlakeDetectionRetryCount (task) {
334
638
  }
335
639
 
336
640
  const executionStart = earlyFlakeDetectionStartByTask.get(task)
337
- const duration = executionStart === undefined ? task.result?.duration ?? 0 : performance.now() - executionStart
641
+ const duration = executionStart === undefined ? task.result?.duration ?? 0 : now() - executionStart
338
642
  for (const { key, limitMs } of earlyFlakeDetectionRetryThresholds) {
339
643
  if (duration < limitMs) {
340
644
  return earlyFlakeDetectionSlowRetries[key] ?? 0
@@ -376,21 +680,10 @@ function isModifiedTest (testSuite) {
376
680
  }
377
681
 
378
682
  function getTestSuite (task) {
379
- let filepath = realpaths.get(task.file.filepath)
380
- if (filepath === undefined) {
381
- filepath = realpath(task.file.filepath)
382
- realpaths.set(task.file.filepath, filepath)
383
- }
384
-
385
- return normalizePath(relative(repositoryRoot, filepath))
386
- }
387
-
388
- function realpath (filepath) {
389
- try {
390
- return realpathSync(filepath)
391
- } catch {
392
- return filepath
393
- }
683
+ const filepath = task.file.filepath
684
+ return testPropertiesByFilepath[filepath]?.testSuite ||
685
+ testPropertiesByFilepath[normalizePath(filepath)]?.testSuite ||
686
+ normalizePath(filepath)
394
687
  }
395
688
 
396
689
  function getTestName (task) {
@@ -423,8 +716,12 @@ function getIsNoWorkerInitActive () {
423
716
 
424
717
  function getProvidedContext () {
425
718
  try {
426
- return globalThis.__vitest_worker__.providedContext._ddVitestWorkerSetup || {}
719
+ return inject('_ddVitestWorkerSetup') || {}
427
720
  } catch {
428
- return {}
721
+ try {
722
+ return globalThis.__vitest_worker__.providedContext._ddVitestWorkerSetup || {}
723
+ } catch {
724
+ return {}
725
+ }
429
726
  }
430
727
  }
package/index.d.ts CHANGED
@@ -3931,29 +3931,74 @@ declare namespace tracer {
3931
3931
  flush (): void
3932
3932
  }
3933
3933
 
3934
+ /** JSON-serializable value accepted by LLMObs Experiments. */
3935
+ type JSONType = string | number | boolean | null | JSONType[] | { [key: string]: JSONType }
3936
+
3934
3937
  /**
3935
3938
  * A task run over each dataset record during an experiment.
3936
3939
  */
3937
- type ExperimentTask = (input: any, config: Record<string, any>) => any | Promise<any>
3940
+ type ExperimentTask = (
3941
+ input: JSONType,
3942
+ config: Record<string, JSONType>,
3943
+ metadata?: Record<string, JSONType>
3944
+ ) => JSONType | Promise<JSONType>
3938
3945
 
3939
3946
  /**
3940
3947
  * Scores a single task output. The return type selects the metric:
3941
- * `boolean` -> boolean, `number` -> score, anything else -> categorical.
3948
+ * `boolean` -> boolean, `number` -> score, `string` -> categorical, anything else -> json.
3949
+ */
3950
+ type ExperimentEvaluator = (
3951
+ input: JSONType,
3952
+ output: JSONType,
3953
+ expectedOutput: JSONType
3954
+ ) => JSONType | Promise<JSONType>
3955
+
3956
+ /**
3957
+ * Scores all rows in an experiment run and emits a summary metric.
3942
3958
  */
3943
- type ExperimentEvaluator = (input: any, output: any, expectedOutput: any) => any | Promise<any>
3959
+ type ExperimentSummaryEvaluator = (
3960
+ inputs: any[],
3961
+ outputs: any[],
3962
+ expectedOutputs: any[],
3963
+ evaluatorResults: Record<string, any[]>,
3964
+ metadata?: Array<Record<string, any>>
3965
+ ) => any | Promise<any>
3966
+
3967
+ interface CreateDatasetOptions {
3968
+ description?: string
3969
+ records?: Array<{
3970
+ id?: string,
3971
+ inputData: JSONType,
3972
+ expectedOutput?: JSONType,
3973
+ metadata?: Record<string, JSONType>
3974
+ }>
3975
+ }
3944
3976
 
3945
3977
  interface ExperimentOptions {
3946
3978
  name: string
3947
3979
  dataset: Dataset
3948
3980
  task: ExperimentTask
3949
- /** Evaluators keyed by metric label. */
3950
- evaluators?: Record<string, ExperimentEvaluator>
3981
+ /** Evaluators keyed by metric label, or named functions. */
3982
+ evaluators?: Record<string, ExperimentEvaluator> | ExperimentEvaluator[]
3983
+ /** Summary evaluators keyed by metric label, or named functions. */
3984
+ summaryEvaluators?: Record<string, ExperimentSummaryEvaluator> | ExperimentSummaryEvaluator[]
3951
3985
  description?: string
3952
- config?: Record<string, any>
3986
+ config?: Record<string, JSONType>
3953
3987
  tags?: Record<string, string>
3954
3988
  }
3955
3989
 
3990
+ interface ExperimentRunOptions {
3991
+ /** Maximum retries for task and evaluator failures. Default 0. */
3992
+ maxRetries?: number
3993
+ /** Delay before a retry, in milliseconds. Default 100 * (attempt + 1). */
3994
+ retryDelay?: (attempt: number) => number
3995
+ /** Reject on the first task/evaluator error instead of capturing it. Default false. */
3996
+ throwOnErrors?: boolean
3997
+ }
3998
+
3956
3999
  interface PullDatasetOptions {
4000
+ /** Dataset version to pull. Defaults to latest. */
4001
+ version?: number
3957
4002
  /** Wait until at least this many records are readable (absorbs write lag). */
3958
4003
  expectedRecordCount?: number
3959
4004
  /** Maximum total time to wait, in ms. Default 30000. */
@@ -3966,19 +4011,30 @@ declare namespace tracer {
3966
4011
  traceId: string
3967
4012
  startNs: number
3968
4013
  durationNs: number
3969
- input: any
3970
- output: any
3971
- expectedOutput: any
4014
+ input: JSONType
4015
+ output: JSONType
4016
+ expectedOutput: JSONType
3972
4017
  readonly isError: boolean
3973
4018
  errorType: string | null
3974
4019
  errorMessage: string | null
3975
- evaluations: Record<string, any>
4020
+ evaluations: Record<string, JSONType>
3976
4021
  evaluationErrors: Record<string, string>
3977
4022
  }
3978
4023
 
4024
+ interface ExperimentRun {
4025
+ runId: string
4026
+ runIteration: number
4027
+ rows: ExperimentResultRow[]
4028
+ summaryEvaluations: Record<string, { value: any, error: string | null }>
4029
+ }
4030
+
3979
4031
  interface ExperimentResult {
3980
4032
  experimentId: string
3981
4033
  rows: ExperimentResultRow[]
4034
+ /** Single-run summary evaluator results. */
4035
+ summaryEvaluations: Record<string, { value: any, error: string | null }>
4036
+ /** Experiment runs. P0 Node experiments currently return one run. */
4037
+ runs: ExperimentRun[]
3982
4038
  /** Dashboard URL for the experiment. */
3983
4039
  url: string
3984
4040
  }
@@ -3991,13 +4047,20 @@ declare namespace tracer {
3991
4047
  }
3992
4048
 
3993
4049
  interface Dataset {
3994
- addRecord (input: any, expectedOutput?: any, metadata?: Record<string, any>): Dataset
4050
+ addRecord (input: JSONType, expectedOutput?: JSONType, metadata?: Record<string, JSONType>): Dataset
3995
4051
  /** Creates the dataset remotely if needed and pushes any unpushed records. */
3996
4052
  push (): Promise<DatasetPushResult>
3997
4053
  name (): string
3998
4054
  id (): string | null
3999
4055
  projectId (): string | null
4000
- records (): Array<{ input: any, expectedOutput: any, metadata: Record<string, any> }>
4056
+ version (): number | null
4057
+ latestVersion (): number | null
4058
+ records (): Array<{
4059
+ id: string | null,
4060
+ input: JSONType,
4061
+ expectedOutput: JSONType,
4062
+ metadata: Record<string, JSONType>
4063
+ }>
4001
4064
  /** Dashboard URL for the dataset, or null until pushed. */
4002
4065
  url (): string | null
4003
4066
  }
@@ -4006,12 +4069,13 @@ declare namespace tracer {
4006
4069
  name (): string
4007
4070
  experimentId (): string | null
4008
4071
  url (): string | null
4009
- run (): Promise<ExperimentResult>
4072
+ run (options?: ExperimentRunOptions): Promise<ExperimentResult>
4010
4073
  }
4011
4074
 
4012
4075
  interface Experiments {
4013
4076
  /** Create a local dataset buffer; pushed on the first experiment run. */
4014
4077
  createDataset (name: string, description?: string): Dataset
4078
+ createDataset (name: string, options?: CreateDatasetOptions): Dataset
4015
4079
  /** Pull an existing dataset (with records) by name. */
4016
4080
  pullDataset (name: string, options?: PullDatasetOptions): Promise<Dataset>
4017
4081
  /** Build an experiment to run over a dataset. */