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
@@ -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
@@ -3734,29 +3734,74 @@ declare namespace tracer {
3734
3734
  flush (): void
3735
3735
  }
3736
3736
 
3737
+ /** JSON-serializable value accepted by LLMObs Experiments. */
3738
+ type JSONType = string | number | boolean | null | JSONType[] | { [key: string]: JSONType }
3739
+
3737
3740
  /**
3738
3741
  * A task run over each dataset record during an experiment.
3739
3742
  */
3740
- type ExperimentTask = (input: any, config: Record<string, any>) => any | Promise<any>
3743
+ type ExperimentTask = (
3744
+ input: JSONType,
3745
+ config: Record<string, JSONType>,
3746
+ metadata?: Record<string, JSONType>
3747
+ ) => JSONType | Promise<JSONType>
3741
3748
 
3742
3749
  /**
3743
3750
  * Scores a single task output. The return type selects the metric:
3744
- * `boolean` -> boolean, `number` -> score, anything else -> categorical.
3751
+ * `boolean` -> boolean, `number` -> score, `string` -> categorical, anything else -> json.
3752
+ */
3753
+ type ExperimentEvaluator = (
3754
+ input: JSONType,
3755
+ output: JSONType,
3756
+ expectedOutput: JSONType
3757
+ ) => JSONType | Promise<JSONType>
3758
+
3759
+ /**
3760
+ * Scores all rows in an experiment run and emits a summary metric.
3745
3761
  */
3746
- type ExperimentEvaluator = (input: any, output: any, expectedOutput: any) => any | Promise<any>
3762
+ type ExperimentSummaryEvaluator = (
3763
+ inputs: any[],
3764
+ outputs: any[],
3765
+ expectedOutputs: any[],
3766
+ evaluatorResults: Record<string, any[]>,
3767
+ metadata?: Array<Record<string, any>>
3768
+ ) => any | Promise<any>
3769
+
3770
+ interface CreateDatasetOptions {
3771
+ description?: string
3772
+ records?: Array<{
3773
+ id?: string,
3774
+ inputData: JSONType,
3775
+ expectedOutput?: JSONType,
3776
+ metadata?: Record<string, JSONType>
3777
+ }>
3778
+ }
3747
3779
 
3748
3780
  interface ExperimentOptions {
3749
3781
  name: string
3750
3782
  dataset: Dataset
3751
3783
  task: ExperimentTask
3752
- /** Evaluators keyed by metric label. */
3753
- evaluators?: Record<string, ExperimentEvaluator>
3784
+ /** Evaluators keyed by metric label, or named functions. */
3785
+ evaluators?: Record<string, ExperimentEvaluator> | ExperimentEvaluator[]
3786
+ /** Summary evaluators keyed by metric label, or named functions. */
3787
+ summaryEvaluators?: Record<string, ExperimentSummaryEvaluator> | ExperimentSummaryEvaluator[]
3754
3788
  description?: string
3755
- config?: Record<string, any>
3789
+ config?: Record<string, JSONType>
3756
3790
  tags?: Record<string, string>
3757
3791
  }
3758
3792
 
3793
+ interface ExperimentRunOptions {
3794
+ /** Maximum retries for task and evaluator failures. Default 0. */
3795
+ maxRetries?: number
3796
+ /** Delay before a retry, in milliseconds. Default 100 * (attempt + 1). */
3797
+ retryDelay?: (attempt: number) => number
3798
+ /** Reject on the first task/evaluator error instead of capturing it. Default false. */
3799
+ throwOnErrors?: boolean
3800
+ }
3801
+
3759
3802
  interface PullDatasetOptions {
3803
+ /** Dataset version to pull. Defaults to latest. */
3804
+ version?: number
3760
3805
  /** Wait until at least this many records are readable (absorbs write lag). */
3761
3806
  expectedRecordCount?: number
3762
3807
  /** Maximum total time to wait, in ms. Default 30000. */
@@ -3769,19 +3814,30 @@ declare namespace tracer {
3769
3814
  traceId: string
3770
3815
  startNs: number
3771
3816
  durationNs: number
3772
- input: any
3773
- output: any
3774
- expectedOutput: any
3817
+ input: JSONType
3818
+ output: JSONType
3819
+ expectedOutput: JSONType
3775
3820
  readonly isError: boolean
3776
3821
  errorType: string | null
3777
3822
  errorMessage: string | null
3778
- evaluations: Record<string, any>
3823
+ evaluations: Record<string, JSONType>
3779
3824
  evaluationErrors: Record<string, string>
3780
3825
  }
3781
3826
 
3827
+ interface ExperimentRun {
3828
+ runId: string
3829
+ runIteration: number
3830
+ rows: ExperimentResultRow[]
3831
+ summaryEvaluations: Record<string, { value: any, error: string | null }>
3832
+ }
3833
+
3782
3834
  interface ExperimentResult {
3783
3835
  experimentId: string
3784
3836
  rows: ExperimentResultRow[]
3837
+ /** Single-run summary evaluator results. */
3838
+ summaryEvaluations: Record<string, { value: any, error: string | null }>
3839
+ /** Experiment runs. P0 Node experiments currently return one run. */
3840
+ runs: ExperimentRun[]
3785
3841
  /** Dashboard URL for the experiment. */
3786
3842
  url: string
3787
3843
  }
@@ -3794,13 +3850,20 @@ declare namespace tracer {
3794
3850
  }
3795
3851
 
3796
3852
  interface Dataset {
3797
- addRecord (input: any, expectedOutput?: any, metadata?: Record<string, any>): Dataset
3853
+ addRecord (input: JSONType, expectedOutput?: JSONType, metadata?: Record<string, JSONType>): Dataset
3798
3854
  /** Creates the dataset remotely if needed and pushes any unpushed records. */
3799
3855
  push (): Promise<DatasetPushResult>
3800
3856
  name (): string
3801
3857
  id (): string | null
3802
3858
  projectId (): string | null
3803
- records (): Array<{ input: any, expectedOutput: any, metadata: Record<string, any> }>
3859
+ version (): number | null
3860
+ latestVersion (): number | null
3861
+ records (): Array<{
3862
+ id: string | null,
3863
+ input: JSONType,
3864
+ expectedOutput: JSONType,
3865
+ metadata: Record<string, JSONType>
3866
+ }>
3804
3867
  /** Dashboard URL for the dataset, or null until pushed. */
3805
3868
  url (): string | null
3806
3869
  }
@@ -3809,12 +3872,13 @@ declare namespace tracer {
3809
3872
  name (): string
3810
3873
  experimentId (): string | null
3811
3874
  url (): string | null
3812
- run (): Promise<ExperimentResult>
3875
+ run (options?: ExperimentRunOptions): Promise<ExperimentResult>
3813
3876
  }
3814
3877
 
3815
3878
  interface Experiments {
3816
3879
  /** Create a local dataset buffer; pushed on the first experiment run. */
3817
3880
  createDataset (name: string, description?: string): Dataset
3881
+ createDataset (name: string, options?: CreateDatasetOptions): Dataset
3818
3882
  /** Pull an existing dataset (with records) by name. */
3819
3883
  pullDataset (name: string, options?: PullDatasetOptions): Promise<Dataset>
3820
3884
  /** Build an experiment to run over a dataset. */