dd-trace 6.6.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 (209) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +8 -31
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +84 -22
  20. package/ci/test-optimization-validation/generated-test-contract.js +281 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +815 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/offline-output.js +6 -6
  28. package/ci/test-optimization-validation/plan-writer.js +243 -984
  29. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  30. package/ci/test-optimization-validation/redaction.js +2 -1
  31. package/ci/test-optimization-validation/report-writer.js +391 -1357
  32. package/ci/test-optimization-validation/result-semantics.js +86 -0
  33. package/ci/test-optimization-validation/runner-command.js +249 -0
  34. package/ci/test-optimization-validation/runner-contract.js +664 -0
  35. package/ci/test-optimization-validation/safe-files.js +4 -4
  36. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  39. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  40. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  41. package/ci/test-optimization-validation/scenarios/test-management.js +6 -5
  42. package/ci/test-optimization-validation/source-text.js +87 -0
  43. package/ci/test-optimization-validation/test-output.js +12 -22
  44. package/ci/vitest-no-worker-init-setup.mjs +348 -51
  45. package/ext/tags.d.ts +1 -0
  46. package/index.d.ts +84 -13
  47. package/package.json +10 -4
  48. package/packages/datadog-core/src/utils/src/kebabcase.js +8 -2
  49. package/packages/datadog-esbuild/index.js +4 -2
  50. package/packages/datadog-esbuild/src/utils.js +1 -1
  51. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +4 -4
  52. package/packages/datadog-instrumentations/src/cucumber.js +9 -6
  53. package/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +10 -0
  54. package/packages/datadog-instrumentations/src/helpers/hook.js +19 -2
  55. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  56. package/packages/datadog-instrumentations/src/helpers/register.js +9 -3
  57. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +1 -0
  58. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  59. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  60. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +57 -0
  61. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +50 -18
  62. package/packages/datadog-instrumentations/src/helpers/router-helper.js +1 -1
  63. package/packages/datadog-instrumentations/src/helpers/shared-utils.js +39 -0
  64. package/packages/datadog-instrumentations/src/http/client.js +1 -3
  65. package/packages/datadog-instrumentations/src/jest.js +8 -14
  66. package/packages/datadog-instrumentations/src/mocha/common.js +1 -3
  67. package/packages/datadog-instrumentations/src/mocha/main.js +2 -42
  68. package/packages/datadog-instrumentations/src/mocha/utils.js +68 -2
  69. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +19 -0
  70. package/packages/datadog-instrumentations/src/mocha/worker.js +446 -42
  71. package/packages/datadog-instrumentations/src/moleculer/client.js +1 -1
  72. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  73. package/packages/datadog-instrumentations/src/playwright.js +3 -2
  74. package/packages/datadog-instrumentations/src/rhea.js +1 -1
  75. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +525 -56
  76. package/packages/datadog-instrumentations/src/vitest-main.js +90 -10
  77. package/packages/datadog-instrumentations/src/webdriverio.js +914 -0
  78. package/packages/datadog-plugin-aerospike/src/index.js +1 -3
  79. package/packages/datadog-plugin-amqplib/src/client.js +1 -1
  80. package/packages/datadog-plugin-amqplib/src/producer.js +1 -1
  81. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +2 -2
  82. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +2 -2
  83. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -3
  84. package/packages/datadog-plugin-azure-service-bus/src/producer.js +5 -3
  85. package/packages/datadog-plugin-bullmq/src/producer.js +3 -3
  86. package/packages/datadog-plugin-child_process/src/index.js +3 -3
  87. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +5 -8
  88. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -1
  89. package/packages/datadog-plugin-fastify/src/tracing.js +1 -1
  90. package/packages/datadog-plugin-fs/src/index.js +0 -4
  91. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +14 -7
  92. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +19 -10
  93. package/packages/datadog-plugin-graphql/src/execute.js +1 -0
  94. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -1
  95. package/packages/datadog-plugin-http/src/client.js +2 -1
  96. package/packages/datadog-plugin-jest/src/index.js +2 -3
  97. package/packages/datadog-plugin-jest/src/util.js +2 -1
  98. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  99. package/packages/datadog-plugin-mocha/src/index.js +25 -6
  100. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  101. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  102. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  103. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  104. package/packages/datadog-plugin-prisma/src/index.js +5 -1
  105. package/packages/datadog-plugin-rhea/src/consumer.js +16 -13
  106. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  107. package/packages/datadog-plugin-undici/src/index.js +2 -1
  108. package/packages/datadog-plugin-vitest/src/index.js +84 -11
  109. package/packages/datadog-webpack/index.js +2 -2
  110. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -1
  111. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +1 -1
  112. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +1 -1
  113. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +1 -1
  114. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +1 -3
  115. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  116. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +2 -2
  117. package/packages/dd-trace/src/appsec/sdk/track_event.js +1 -1
  118. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +7 -6
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +50 -51
  121. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -1
  122. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +4 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/webdriverio.js +26 -0
  124. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -1
  125. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  126. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -3
  127. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +7 -5
  128. package/packages/dd-trace/src/config/defaults.js +14 -6
  129. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -2
  130. package/packages/dd-trace/src/config/index.js +15 -0
  131. package/packages/dd-trace/src/config/parsers.js +19 -1
  132. package/packages/dd-trace/src/config/supported-configurations.json +12 -6
  133. package/packages/dd-trace/src/datastreams/encoding.js +3 -4
  134. package/packages/dd-trace/src/debugger/devtools_client/log.js +2 -1
  135. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +1 -4
  136. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -2
  137. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +2 -2
  138. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +16 -2
  139. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  140. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  141. package/packages/dd-trace/src/exporter.js +16 -9
  142. package/packages/dd-trace/src/exporters/common/url.js +3 -5
  143. package/packages/dd-trace/src/exporters/electron/index.js +4 -1
  144. package/packages/dd-trace/src/external-logger/src/index.js +1 -2
  145. package/packages/dd-trace/src/flare/index.js +2 -2
  146. package/packages/dd-trace/src/id.js +1 -1
  147. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -1
  148. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -2
  149. package/packages/dd-trace/src/llmobs/experiments/dataset.js +66 -14
  150. package/packages/dd-trace/src/llmobs/experiments/experiment.js +289 -95
  151. package/packages/dd-trace/src/llmobs/experiments/index.js +51 -12
  152. package/packages/dd-trace/src/llmobs/experiments/noop.js +95 -10
  153. package/packages/dd-trace/src/llmobs/experiments/result.js +13 -2
  154. package/packages/dd-trace/src/llmobs/experiments/util.js +218 -0
  155. package/packages/dd-trace/src/llmobs/index.js +9 -1
  156. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  157. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +1 -1
  158. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +1 -1
  159. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  160. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  161. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  162. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  163. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  164. package/packages/dd-trace/src/llmobs/sdk.js +6 -12
  165. package/packages/dd-trace/src/llmobs/span_processor.js +14 -7
  166. package/packages/dd-trace/src/llmobs/tagger.js +24 -5
  167. package/packages/dd-trace/src/llmobs/util.js +56 -3
  168. package/packages/dd-trace/src/llmobs/writers/spans.js +12 -6
  169. package/packages/dd-trace/src/noop/proxy.js +2 -2
  170. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  171. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  172. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  173. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  174. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +2 -1
  175. package/packages/dd-trace/src/opentracing/span.js +1 -1
  176. package/packages/dd-trace/src/opentracing/span_context.js +1 -1
  177. package/packages/dd-trace/src/plugins/ci_plugin.js +114 -23
  178. package/packages/dd-trace/src/plugins/index.js +2 -0
  179. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  180. package/packages/dd-trace/src/plugins/plugin.js +1 -1
  181. package/packages/dd-trace/src/plugins/tracing.js +1 -0
  182. package/packages/dd-trace/src/plugins/util/ci.js +7 -6
  183. package/packages/dd-trace/src/plugins/util/git.js +1 -1
  184. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +1 -1
  185. package/packages/dd-trace/src/plugins/util/llm.js +2 -2
  186. package/packages/dd-trace/src/plugins/util/tags.js +2 -0
  187. package/packages/dd-trace/src/plugins/util/test.js +38 -12
  188. package/packages/dd-trace/src/plugins/util/url.js +5 -5
  189. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  190. package/packages/dd-trace/src/plugins/util/web.js +1 -1
  191. package/packages/dd-trace/src/priority_sampler.js +6 -3
  192. package/packages/dd-trace/src/profiling/profilers/poisson.js +1 -1
  193. package/packages/dd-trace/src/profiling/profilers/shared.js +2 -2
  194. package/packages/dd-trace/src/ritm.js +5 -20
  195. package/packages/dd-trace/src/span_processor.js +1 -1
  196. package/packages/dd-trace/src/span_stats.js +22 -4
  197. package/packages/dd-trace/src/startup-log.js +3 -0
  198. package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -1
  199. package/packages/dd-trace/src/util.js +16 -0
  200. package/vendor/dist/@apm-js-collab/code-transformer/index.js +12 -12
  201. package/vendor/dist/pprof-format/index.js +1 -1
  202. package/vendor/dist/source-map/index.js +1 -1
  203. package/ci/test-optimization-validation/command-suitability.js +0 -471
  204. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  205. package/ci/test-optimization-validation/init-probe.js +0 -246
  206. package/ci/test-optimization-validation/late-initialization.js +0 -63
  207. package/ci/test-optimization-validation/local-command.js +0 -163
  208. package/ci/test-optimization-validation/setup-runner.js +0 -97
  209. package/ci/test-optimization-validation-manifest.schema.json +0 -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/ext/tags.d.ts CHANGED
@@ -20,6 +20,7 @@ declare const tags: {
20
20
  HTTP_RESPONSE_HEADERS: 'http.response.headers'
21
21
  HTTP_USERAGENT: 'http.useragent',
22
22
  HTTP_CLIENT_IP: 'http.client_ip',
23
+ GRPC_STATUS_CODE: 'grpc.status.code'
23
24
  PATHWAY_HASH: 'pathway.hash'
24
25
  }
25
26
 
package/index.d.ts CHANGED
@@ -291,6 +291,7 @@ interface Plugins {
291
291
  "next": tracer.plugins.next;
292
292
  "nyc": tracer.plugins.nyc;
293
293
  "openai": tracer.plugins.openai;
294
+ "openai-agents": tracer.plugins.openai_agents;
294
295
  "opensearch": tracer.plugins.opensearch;
295
296
  "oracledb": tracer.plugins.oracledb;
296
297
  "playwright": tracer.plugins.playwright;
@@ -2968,6 +2969,12 @@ declare namespace tracer {
2968
2969
  */
2969
2970
  interface openai extends Instrumentation {}
2970
2971
 
2972
+ /**
2973
+ * This plugin automatically instruments the
2974
+ * [@openai/agents](https://www.npmjs.com/package/@openai/agents) library.
2975
+ */
2976
+ interface openai_agents extends Instrumentation {}
2977
+
2971
2978
  /**
2972
2979
  * This plugin automatically instruments the
2973
2980
  * [opensearch](https://github.com/opensearch-project/opensearch-js) module.
@@ -3727,29 +3734,74 @@ declare namespace tracer {
3727
3734
  flush (): void
3728
3735
  }
3729
3736
 
3737
+ /** JSON-serializable value accepted by LLMObs Experiments. */
3738
+ type JSONType = string | number | boolean | null | JSONType[] | { [key: string]: JSONType }
3739
+
3730
3740
  /**
3731
3741
  * A task run over each dataset record during an experiment.
3732
3742
  */
3733
- 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>
3734
3748
 
3735
3749
  /**
3736
3750
  * Scores a single task output. The return type selects the metric:
3737
- * `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.
3738
3761
  */
3739
- 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
+ }
3740
3779
 
3741
3780
  interface ExperimentOptions {
3742
3781
  name: string
3743
3782
  dataset: Dataset
3744
3783
  task: ExperimentTask
3745
- /** Evaluators keyed by metric label. */
3746
- 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[]
3747
3788
  description?: string
3748
- config?: Record<string, any>
3789
+ config?: Record<string, JSONType>
3749
3790
  tags?: Record<string, string>
3750
3791
  }
3751
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
+
3752
3802
  interface PullDatasetOptions {
3803
+ /** Dataset version to pull. Defaults to latest. */
3804
+ version?: number
3753
3805
  /** Wait until at least this many records are readable (absorbs write lag). */
3754
3806
  expectedRecordCount?: number
3755
3807
  /** Maximum total time to wait, in ms. Default 30000. */
@@ -3762,19 +3814,30 @@ declare namespace tracer {
3762
3814
  traceId: string
3763
3815
  startNs: number
3764
3816
  durationNs: number
3765
- input: any
3766
- output: any
3767
- expectedOutput: any
3817
+ input: JSONType
3818
+ output: JSONType
3819
+ expectedOutput: JSONType
3768
3820
  readonly isError: boolean
3769
3821
  errorType: string | null
3770
3822
  errorMessage: string | null
3771
- evaluations: Record<string, any>
3823
+ evaluations: Record<string, JSONType>
3772
3824
  evaluationErrors: Record<string, string>
3773
3825
  }
3774
3826
 
3827
+ interface ExperimentRun {
3828
+ runId: string
3829
+ runIteration: number
3830
+ rows: ExperimentResultRow[]
3831
+ summaryEvaluations: Record<string, { value: any, error: string | null }>
3832
+ }
3833
+
3775
3834
  interface ExperimentResult {
3776
3835
  experimentId: string
3777
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[]
3778
3841
  /** Dashboard URL for the experiment. */
3779
3842
  url: string
3780
3843
  }
@@ -3787,13 +3850,20 @@ declare namespace tracer {
3787
3850
  }
3788
3851
 
3789
3852
  interface Dataset {
3790
- addRecord (input: any, expectedOutput?: any, metadata?: Record<string, any>): Dataset
3853
+ addRecord (input: JSONType, expectedOutput?: JSONType, metadata?: Record<string, JSONType>): Dataset
3791
3854
  /** Creates the dataset remotely if needed and pushes any unpushed records. */
3792
3855
  push (): Promise<DatasetPushResult>
3793
3856
  name (): string
3794
3857
  id (): string | null
3795
3858
  projectId (): string | null
3796
- 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
+ }>
3797
3867
  /** Dashboard URL for the dataset, or null until pushed. */
3798
3868
  url (): string | null
3799
3869
  }
@@ -3802,12 +3872,13 @@ declare namespace tracer {
3802
3872
  name (): string
3803
3873
  experimentId (): string | null
3804
3874
  url (): string | null
3805
- run (): Promise<ExperimentResult>
3875
+ run (options?: ExperimentRunOptions): Promise<ExperimentResult>
3806
3876
  }
3807
3877
 
3808
3878
  interface Experiments {
3809
3879
  /** Create a local dataset buffer; pushed on the first experiment run. */
3810
3880
  createDataset (name: string, description?: string): Dataset
3881
+ createDataset (name: string, options?: CreateDatasetOptions): Dataset
3811
3882
  /** Pull an existing dataset (with records) by name. */
3812
3883
  pullDataset (name: string, options?: PullDatasetOptions): Promise<Dataset>
3813
3884
  /** Build an experiment to run over a dataset. */