dd-trace 6.9.0 → 6.11.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 (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -1,19 +1,21 @@
1
1
  'use strict'
2
2
 
3
3
  const id = require('../../id')
4
+ const log = require('../../log')
4
5
 
5
- const { API_BASE_PATH } = require('./client')
6
6
  const { Row, ExperimentResult, ExperimentRun } = require('./result')
7
7
  const {
8
- buildExperimentTagObject,
9
8
  buildSpanMetadata,
10
9
  buildTags,
10
+ durationNs,
11
11
  hasEntries,
12
12
  inferMetricType,
13
13
  normalizeEvaluators,
14
+ mergeTags,
14
15
  normalizeJsonMetricValue,
15
16
  sleep,
16
17
  stringify,
18
+ timestampMs,
17
19
  validateEvaluatorName,
18
20
  } = require('./util')
19
21
 
@@ -28,7 +30,7 @@ function toSpan (row, metadata, ids, spanName, userTags) {
28
30
  meta.metadata = metadata
29
31
  }
30
32
  if (row.isError) {
31
- meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: '' }
33
+ meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: row.errorStack ?? '' }
32
34
  }
33
35
 
34
36
  return {
@@ -91,11 +93,34 @@ function createFallbackSpanContext (startNs) {
91
93
  return { spanId, traceId }
92
94
  }
93
95
 
96
+ function normalizeError (error) {
97
+ if (error == null) {
98
+ return { errorType: null, errorMessage: null, errorStack: '' }
99
+ }
100
+
101
+ if (typeof error === 'string') {
102
+ return { errorType: 'Error', errorMessage: error, errorStack: '' }
103
+ }
104
+
105
+ return {
106
+ errorType: error.type ?? error.name ?? 'Error',
107
+ errorMessage: error.message ?? String(error),
108
+ errorStack: error.stack ?? '',
109
+ }
110
+ }
111
+
112
+ function errorMessage (error) {
113
+ if (error == null) return null
114
+ if (typeof error === 'string') return error
115
+ return error.message ?? String(error)
116
+ }
117
+
94
118
  // Builder + run() orchestration: runs rows sequentially, emits one root span
95
119
  // per dataset row, and posts spans + metrics to the experiments events API.
96
120
  class Experiment {
97
121
  #client
98
122
  #llmobs
123
+ #external
99
124
  #name
100
125
  #description
101
126
  #dataset
@@ -104,24 +129,35 @@ class Experiment {
104
129
  #summaryEvaluators
105
130
  #config
106
131
  #tags
132
+ #metadata
133
+ #projectId
107
134
  #experimentId
135
+ #runId
136
+ #runIteration
108
137
 
109
138
  constructor (client, options = {}, llmobs) {
110
139
  if (!options.name) throw new Error('Experiment name is required')
111
- if (!options.dataset) throw new Error('Experiment dataset is required')
112
- if (typeof options.task !== 'function') throw new Error('Experiment task is required')
140
+ this.#external = options.external === true
141
+ if (!this.#external) {
142
+ if (!options.dataset) throw new Error('Experiment dataset is required')
143
+ if (typeof options.task !== 'function') throw new Error('Experiment task is required')
144
+ }
113
145
 
114
146
  this.#client = client
115
147
  this.#llmobs = llmobs
116
148
  this.#name = options.name
117
149
  this.#description = options.description ?? ''
118
- this.#dataset = options.dataset
150
+ this.#dataset = options.dataset ?? {}
119
151
  this.#task = options.task
120
152
  this.#evaluators = normalizeEvaluators(options.evaluators, 'row')
121
153
  this.#summaryEvaluators = normalizeEvaluators(options.summaryEvaluators, 'summary')
122
154
  this.#config = { ...options.config }
123
155
  this.#tags = { ...options.tags }
156
+ this.#metadata = { ...options.metadata }
157
+ this.#projectId = null
124
158
  this.#experimentId = null
159
+ this.#runId = null
160
+ this.#runIteration = null
125
161
  }
126
162
 
127
163
  name () {
@@ -137,7 +173,177 @@ class Experiment {
137
173
  return `${this.#client.appBase}/llm/experiments/${this.#experimentId}`
138
174
  }
139
175
 
176
+ /**
177
+ * @returns {Promise<Experiment>}
178
+ */
179
+ async start () {
180
+ if (!this.#external) throw new Error('Experiment is not externally driven')
181
+
182
+ this.#projectId = await this.#client.ensureProjectId()
183
+
184
+ const dataset = await this.#ensureExternalDataset()
185
+ const attributes = {
186
+ name: this.#name,
187
+ project_id: this.#projectId,
188
+ dataset_id: dataset.id,
189
+ description: this.#description,
190
+ ensure_unique: true,
191
+ run_count: 1,
192
+ }
193
+ if (dataset.version != null) attributes.dataset_version = dataset.version
194
+ if (hasEntries(this.#config)) attributes.config = this.#config
195
+
196
+ const metadata = { ...this.#metadata }
197
+ if (hasEntries(this.#tags)) metadata.tags = buildTags(this.#tags, {})
198
+ if (hasEntries(metadata)) attributes.metadata = metadata
199
+
200
+ let created
201
+ try {
202
+ created = await this.#client.createExperiment(attributes)
203
+ } catch (err) {
204
+ throw new Error(`Failed to create experiment '${this.#name}': ${err.message}`)
205
+ }
206
+ this.#experimentId = created.experimentId
207
+ this.#runId = id().toString(16).padStart(16, '0')
208
+ this.#runIteration = 0
209
+ return this
210
+ }
211
+
212
+ async #ensureExternalDataset () {
213
+ if (this.#dataset.id) {
214
+ return { id: this.#dataset.id, version: this.#dataset.version }
215
+ }
216
+
217
+ const name = this.#dataset.name ?? `${this.#name} dataset`
218
+ const description = this.#dataset.description ??
219
+ `Placeholder dataset for externally-driven experiment '${this.#name}'`
220
+ let response
221
+ try {
222
+ response = await this.#client.createDataset(this.#projectId, { name, description })
223
+ } catch (err) {
224
+ throw new Error(`Failed to create placeholder dataset '${name}': ${err.message}`)
225
+ }
226
+
227
+ const id = response.id()
228
+ if (id === null) {
229
+ throw new Error(`Failed to create placeholder dataset '${name}': backend response is missing dataset id`)
230
+ }
231
+
232
+ this.#dataset = {
233
+ ...this.#dataset,
234
+ id,
235
+ version: this.#dataset.version ?? response.version(),
236
+ }
237
+ return { id: this.#dataset.id, version: this.#dataset.version }
238
+ }
239
+
240
+ /**
241
+ * @param {object} input
242
+ * @returns {Promise<{experimentId: string, spanId: string, traceId: string, url: string | null}>}
243
+ */
244
+ async submitSpan (input = {}) {
245
+ if (this.#experimentId === null || this.#projectId === null) {
246
+ throw new Error('Experiment has not been started')
247
+ }
248
+
249
+ const startMs = timestampMs(input.startedAt)
250
+ const startNs = Math.round(startMs * 1e6)
251
+ const { spanId, traceId } = createFallbackSpanContext(startNs)
252
+ const error = normalizeError(input.error)
253
+ const row = new Row({
254
+ spanId,
255
+ traceId,
256
+ startNs,
257
+ durationNs: durationNs(input, startMs),
258
+ input: input.input,
259
+ output: input.output,
260
+ expectedOutput: input.expectedOutput,
261
+ errorType: error.errorType,
262
+ errorMessage: error.errorMessage,
263
+ errorStack: error.errorStack,
264
+ evaluations: {},
265
+ evaluationErrors: {},
266
+ })
267
+
268
+ const span = toSpan(row, input.metadata, {
269
+ experimentId: this.#experimentId,
270
+ projectId: this.#projectId,
271
+ datasetId: this.#dataset.id,
272
+ datasetRecordId: input.datasetRecordId,
273
+ runId: input.runId ?? this.#runId,
274
+ runIteration: input.runIteration ?? this.#runIteration,
275
+ }, input.name ?? this.#name, mergeTags(this.#tags, input.tags))
276
+
277
+ await this.#postEvents(this.#experimentId, [span], [])
278
+
279
+ return {
280
+ experimentId: this.#experimentId,
281
+ spanId,
282
+ traceId,
283
+ url: this.url(),
284
+ }
285
+ }
286
+
287
+ /**
288
+ * @param {{experimentId?: string, spanId: string, traceId?: string}} span
289
+ * @param {object[]} metrics
290
+ * @returns {Promise<void>}
291
+ */
292
+ async submitEvaluationMetrics (span, metrics) {
293
+ if (this.#experimentId === null) {
294
+ throw new Error('Experiment has not been started')
295
+ }
296
+ if (!span?.spanId) {
297
+ throw new Error('Experiment span id is required')
298
+ }
299
+
300
+ const experimentId = span.experimentId ?? this.#experimentId
301
+ if (experimentId !== this.#experimentId) {
302
+ throw new Error(`Experiment span belongs to '${experimentId}', not '${this.#experimentId}'`)
303
+ }
304
+ if (!span.traceId) {
305
+ throw new Error('Experiment trace id is required')
306
+ }
307
+
308
+ const payload = []
309
+ for (const metric of metrics) {
310
+ validateEvaluatorName(metric.label)
311
+ const metricError = errorMessage(metric.error)
312
+ if (metric.value === undefined && metricError == null) {
313
+ log.warn('LLMObs experiments: skipping external metric %s because it has neither value nor error', metric.label)
314
+ continue
315
+ }
316
+ payload.push(toMetric(
317
+ metric.label,
318
+ metric.value,
319
+ metricError,
320
+ span.spanId,
321
+ span.traceId,
322
+ timestampMs(metric.timestamp),
323
+ experimentId,
324
+ mergeTags(this.#tags, metric.tags),
325
+ metric.source ?? 'custom'
326
+ ))
327
+ }
328
+
329
+ if (payload.length === 0) return
330
+ await this.#postEvents(experimentId, [], payload)
331
+ }
332
+
333
+ /**
334
+ * @param {object} options
335
+ * @returns {Promise<void>}
336
+ */
337
+ async close (options = {}) {
338
+ if (this.#experimentId === null) return
339
+ const error = errorMessage(options.error)
340
+ const status = options.status ?? (error === null ? 'completed' : 'failed')
341
+ await this.#updateStatus(this.#experimentId, status, error, false)
342
+ }
343
+
140
344
  async run (options = {}) {
345
+ if (this.#external) throw new Error('Externally-driven experiments cannot run local tasks')
346
+
141
347
  const {
142
348
  maxRetries = 0,
143
349
  retryDelay = (attempt) => 100 * (attempt + 1),
@@ -168,17 +374,16 @@ class Experiment {
168
374
  }
169
375
  const datasetVersion = this.#dataset.version()
170
376
  if (datasetVersion !== null) attributes.dataset_version = datasetVersion
377
+ // eslint-disable-next-line no-restricted-syntax -- faster than tracking entries while copying arbitrary config
171
378
  if (Object.keys(this.#config).length > 0) attributes.config = this.#config
172
379
 
173
380
  let created
174
381
  try {
175
- created = await this.#client.request('POST', `${API_BASE_PATH}/experiments`, {
176
- data: { type: 'experiments', attributes },
177
- })
382
+ created = await this.#client.createExperiment(attributes)
178
383
  } catch (err) {
179
384
  throw new Error(`Failed to create experiment '${this.#name}': ${err.message}`)
180
385
  }
181
- this.#experimentId = created?.data?.id ?? null
386
+ this.#experimentId = created.experimentId
182
387
  const experimentId = this.#experimentId
183
388
  const runId = id().toString(16).padStart(16, '0')
184
389
  const runIteration = 0
@@ -316,7 +521,7 @@ class Experiment {
316
521
  dataset_name: this.#dataset.name(),
317
522
  experiment_name: this.#name,
318
523
  }
319
- const tags = buildExperimentTagObject(this.#tags, autoTags)
524
+ const tags = mergeTags(this.#tags, autoTags)
320
525
 
321
526
  const execute = () => this.#runWithRetries(
322
527
  () => this.#task(record.input, this.#config, record.metadata),
@@ -454,24 +659,79 @@ class Experiment {
454
659
  async #postEvents (experimentId, spans, metrics) {
455
660
  const attributes = { metrics }
456
661
  if (spans.length > 0) attributes.spans = spans
457
- await this.#client.request('POST', `${API_BASE_PATH}/experiments/${experimentId}/events`, {
458
- data: { type: 'experiments', attributes },
459
- })
662
+ await this.#client.postExperimentEvents(experimentId, attributes)
460
663
  }
461
664
 
462
- async #updateStatus (experimentId, status, error) {
665
+ async #updateStatus (experimentId, status, error, suppressErrors = true) {
463
666
  if (!experimentId) return
464
667
  // The experiment-update model has no status field, so this is a direct PATCH.
465
668
  const attributes = { status }
466
669
  if (error !== null) attributes.error = error
467
670
  try {
468
- await this.#client.request('PATCH', `${API_BASE_PATH}/experiments/${experimentId}`, {
469
- data: { type: 'experiments', attributes },
470
- })
471
- } catch {
671
+ await this.#client.updateExperiment(experimentId, attributes)
672
+ } catch (err) {
673
+ if (!suppressErrors) throw err
472
674
  // Status update is best-effort; never let it mask the real result/error.
473
675
  }
474
676
  }
475
677
  }
476
678
 
477
- module.exports = { Experiment, normalizeEvaluators, validateEvaluatorName }
679
+ class ExternalExperiment {
680
+ #experiment
681
+
682
+ /**
683
+ * @param {Pick<Experiment, 'name' | 'experimentId' | 'url' | 'submitSpan' | 'submitEvaluationMetrics' |
684
+ * 'close'>} experiment
685
+ */
686
+ constructor (experiment) {
687
+ this.#experiment = experiment
688
+ }
689
+
690
+ /**
691
+ * @returns {string}
692
+ */
693
+ name () {
694
+ return this.#experiment.name()
695
+ }
696
+
697
+ /**
698
+ * @returns {string | null}
699
+ */
700
+ experimentId () {
701
+ return this.#experiment.experimentId()
702
+ }
703
+
704
+ /**
705
+ * @returns {string | null}
706
+ */
707
+ url () {
708
+ return this.#experiment.url()
709
+ }
710
+
711
+ /**
712
+ * @param {object} [input]
713
+ * @returns {Promise<object>}
714
+ */
715
+ submitSpan (input) {
716
+ return this.#experiment.submitSpan(input)
717
+ }
718
+
719
+ /**
720
+ * @param {object} span
721
+ * @param {object[]} metrics
722
+ * @returns {Promise<void>}
723
+ */
724
+ submitEvaluationMetrics (span, metrics) {
725
+ return this.#experiment.submitEvaluationMetrics(span, metrics)
726
+ }
727
+
728
+ /**
729
+ * @param {object} [options]
730
+ * @returns {Promise<void>}
731
+ */
732
+ close (options) {
733
+ return this.#experiment.close(options)
734
+ }
735
+ }
736
+
737
+ module.exports = { Experiment, ExternalExperiment, normalizeEvaluators, validateEvaluatorName }
@@ -1,9 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const log = require('../../log')
4
- const { ExperimentsClient, API_BASE_PATH } = require('./client')
4
+ const { ExperimentsClient } = require('./client')
5
5
  const { Dataset, DatasetRecord } = require('./dataset')
6
- const { Experiment } = require('./experiment')
6
+ const { Experiment, ExternalExperiment } = require('./experiment')
7
7
  const NoopExperiments = require('./noop')
8
8
 
9
9
  // Poll `attempt` with exponential backoff until it returns true or the time
@@ -26,17 +26,27 @@ async function retryWithBackoff (attempt, { maxTotalMs = 30_000, baseDelayMs = 2
26
26
  // experiments against the LLM Obs backend using the tracer's own config.
27
27
  class Experiments {
28
28
  #client
29
+ #config
29
30
  #llmobs
30
31
  #projectName
31
32
 
32
33
  constructor (config, llmobs) {
34
+ this.#config = config
33
35
  this.#llmobs = llmobs
34
36
  this.#projectName = config.llmobs?.mlApp || config.service
35
- this.#client = new ExperimentsClient({
36
- apiKey: config.DD_API_KEY,
37
- appKey: config.DD_APP_KEY,
38
- site: config.site,
39
- projectName: this.#projectName,
37
+ this.#client = this.#clientForProject(this.#projectName)
38
+ }
39
+
40
+ /**
41
+ * @param {string} projectName
42
+ * @returns {ExperimentsClient}
43
+ */
44
+ #clientForProject (projectName) {
45
+ return new ExperimentsClient({
46
+ apiKey: this.#config.DD_API_KEY,
47
+ appKey: this.#config.DD_APP_KEY,
48
+ site: this.#config.site,
49
+ projectName,
40
50
  })
41
51
  }
42
52
 
@@ -69,68 +79,41 @@ class Experiments {
69
79
  const { expectedRecordCount, maxWaitMs = 30_000, version } = options
70
80
  const projectId = await this.#client.ensureProjectId()
71
81
 
72
- let datasetId = null
73
- let description = ''
82
+ let pulledDataset = null
74
83
  let records = []
75
- let recordIds = []
76
84
  let datasetVersion = version ?? null
77
85
  let latestVersion = null
78
86
  let lastError = ''
79
87
 
80
88
  const succeeded = await retryWithBackoff(async () => {
81
89
  try {
82
- if (datasetId === null) {
83
- const listed = await this.#client.request(
84
- 'GET',
85
- `${API_BASE_PATH}/${projectId}/datasets?filter[name]=${encodeURIComponent(name)}`
86
- )
87
- const datasets = listed?.data
88
- if (datasets) {
89
- for (const item of datasets) {
90
- if (item?.attributes?.name === name) {
91
- datasetId = String(item?.id ?? '')
92
- description = String(item?.attributes?.description ?? '')
93
- latestVersion = item?.attributes?.current_version ?? null
94
- datasetVersion = version ?? latestVersion
95
- break
96
- }
90
+ if (pulledDataset === null) {
91
+ const datasets = await this.#client.listDatasets(projectId, { name })
92
+ for (const dataset of datasets) {
93
+ if (dataset.name() === name) {
94
+ pulledDataset = dataset
95
+ latestVersion = dataset.latestVersion()
96
+ datasetVersion = version ?? latestVersion
97
+ break
97
98
  }
98
99
  }
99
- if (datasetId === null) return false
100
+ if (pulledDataset === null) return false
100
101
  }
101
102
 
102
103
  const recs = []
103
- const ids = []
104
104
  let cursor = ''
105
105
  // Follow the meta.after / page[cursor] pagination until the last page.
106
106
  for (;;) {
107
- const query = new URLSearchParams()
108
- if (cursor) query.set('page[cursor]', cursor)
109
- if (datasetVersion !== null) query.set('filter[version]', String(datasetVersion))
110
107
  // eslint-disable-next-line no-await-in-loop
111
- const resp = await this.#client.request(
112
- 'GET',
113
- `${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records?${query.toString()}`
114
- )
115
- const recordData = resp?.data
116
- if (recordData) {
117
- for (const item of recordData) {
118
- const attrs = item?.attributes ?? item
119
- const recordId = String(item?.id ?? attrs?.id ?? '')
120
- recs.push(new DatasetRecord(
121
- attrs?.input ?? null,
122
- attrs?.expected_output ?? null,
123
- attrs?.metadata ?? {},
124
- recordId === '' ? null : recordId
125
- ))
126
- ids.push(recordId)
127
- }
128
- }
129
- cursor = resp?.meta?.after ?? ''
108
+ const page = await this.#client.listDatasetRecords(projectId, pulledDataset.id(), {
109
+ cursor,
110
+ version: datasetVersion,
111
+ })
112
+ for (const record of page.records) recs.push(record)
113
+ cursor = page.after
130
114
  if (!cursor) break
131
115
  }
132
116
  records = recs
133
- recordIds = ids
134
117
  lastError = ''
135
118
 
136
119
  return expectedRecordCount == null || recs.length >= expectedRecordCount
@@ -140,10 +123,10 @@ class Experiments {
140
123
  }
141
124
  }, { maxTotalMs: maxWaitMs })
142
125
 
143
- if (datasetId === null && lastError) {
126
+ if (pulledDataset === null && lastError) {
144
127
  throw new Error(`Failed to list datasets in project '${this.#projectName}': ${lastError}`)
145
128
  }
146
- if (datasetId === null) {
129
+ if (pulledDataset === null) {
147
130
  throw new Error(`Dataset '${name}' not found in project '${this.#projectName}' (after ${maxWaitMs}ms)`)
148
131
  }
149
132
  if (!succeeded && lastError) {
@@ -159,11 +142,10 @@ class Experiments {
159
142
  return Dataset.fromExisting(
160
143
  this.#client,
161
144
  name,
162
- description,
163
- datasetId,
145
+ pulledDataset.description(),
146
+ pulledDataset.id(),
164
147
  projectId,
165
148
  records,
166
- recordIds,
167
149
  datasetVersion,
168
150
  latestVersion
169
151
  )
@@ -173,6 +155,22 @@ class Experiments {
173
155
  experiment (options) {
174
156
  return new Experiment(this.#client, options, this.#llmobs)
175
157
  }
158
+
159
+ /**
160
+ * Start an externally-driven experiment for eval frameworks that already own
161
+ * task execution. Call submitSpan() once per completed row, then
162
+ * submitEvaluationMetrics() with the generated span id.
163
+ *
164
+ * @param {object} options
165
+ * @returns {Promise<ExternalExperiment>}
166
+ */
167
+ startExperiment (options) {
168
+ const client = options?.projectName === undefined || options.projectName === this.#projectName
169
+ ? this.#client
170
+ : this.#clientForProject(options.projectName)
171
+ return new Experiment(client, { ...options, external: true }).start()
172
+ .then(experiment => new ExternalExperiment(experiment))
173
+ }
176
174
  }
177
175
 
178
176
  // Factory used by the LLMObs SDK: returns a real Experiments instance when
@@ -186,12 +184,13 @@ function createExperiments (config, llmobs) {
186
184
  return new NoopExperiments('DD_API_KEY and DD_APP_KEY are required for experiments')
187
185
  }
188
186
  if (!config.llmobs?.mlApp && !config.service) {
189
- log.warn('LLMObs experiments: no project name configured, set DD_LLMOBS_ML_APP or DD_SERVICE')
190
- return new NoopExperiments(
191
- 'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
187
+ const reason = 'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
192
188
  'tracer.init()) to name the LLM Obs project, or DD_SERVICE (or service in tracer.init()) as a fallback, ' +
193
189
  'then retry'
194
- )
190
+ const experiments = new Experiments(config, llmobs)
191
+ return new NoopExperiments(reason, {
192
+ startExperiment: (options) => experiments.startExperiment(options),
193
+ })
195
194
  }
196
195
  return new Experiments(config, llmobs)
197
196
  }