dd-trace 6.11.0 → 6.13.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 (139) hide show
  1. package/LICENSE-3rdparty.csv +5 -0
  2. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  3. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  4. package/index.d.ts +143 -25
  5. package/package.json +26 -25
  6. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  7. package/packages/datadog-instrumentations/src/cucumber.js +4 -4
  8. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  9. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  12. package/packages/datadog-instrumentations/src/jest.js +140 -18
  13. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  14. package/packages/datadog-instrumentations/src/mariadb.js +7 -0
  15. package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
  16. package/packages/datadog-instrumentations/src/pino.js +15 -4
  17. package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
  18. package/packages/datadog-instrumentations/src/playwright.js +67 -53
  19. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +13 -12
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
  21. package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
  22. package/packages/datadog-instrumentations/src/winston.js +6 -3
  23. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  24. package/packages/datadog-plugin-http/src/client.js +2 -15
  25. package/packages/datadog-plugin-http2/src/client.js +2 -15
  26. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  27. package/packages/datadog-plugin-playwright/src/index.js +13 -1
  28. package/packages/datadog-plugin-undici/src/index.js +2 -15
  29. package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
  30. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  31. package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
  32. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  33. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  34. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  35. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  36. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  37. package/packages/dd-trace/src/appsec/reporter.js +2 -0
  38. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  39. package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
  40. package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
  41. package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
  42. package/packages/dd-trace/src/carrier.js +107 -48
  43. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
  44. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
  46. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
  47. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
  48. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
  49. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
  50. package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
  51. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
  52. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
  53. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  54. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
  55. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
  56. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
  57. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
  58. package/packages/dd-trace/src/config/index.js +2 -1
  59. package/packages/dd-trace/src/config/supported-configurations.json +27 -0
  60. package/packages/dd-trace/src/debugger/constants.js +1 -0
  61. package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
  62. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
  63. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
  64. package/packages/dd-trace/src/debugger/index.js +4 -2
  65. package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
  66. package/packages/dd-trace/src/dogstatsd.js +45 -18
  67. package/packages/dd-trace/src/encode/agentless-json.js +5 -4
  68. package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
  69. package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
  70. package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
  71. package/packages/dd-trace/src/evp_proxy/path.js +22 -0
  72. package/packages/dd-trace/src/exporters/agent/index.js +22 -2
  73. package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
  74. package/packages/dd-trace/src/exporters/common/request.js +5 -2
  75. package/packages/dd-trace/src/exporters/common/writer.js +24 -2
  76. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  77. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  78. package/packages/dd-trace/src/flush.js +102 -0
  79. package/packages/dd-trace/src/llmobs/experiments/client.js +10 -2
  80. package/packages/dd-trace/src/llmobs/experiments/dataset.js +301 -14
  81. package/packages/dd-trace/src/llmobs/experiments/experiment.js +440 -112
  82. package/packages/dd-trace/src/llmobs/experiments/index.js +68 -44
  83. package/packages/dd-trace/src/llmobs/experiments/noop.js +103 -12
  84. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  85. package/packages/dd-trace/src/llmobs/experiments/util.js +81 -4
  86. package/packages/dd-trace/src/llmobs/index.js +80 -17
  87. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  88. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  89. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  90. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  91. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  92. package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
  93. package/packages/dd-trace/src/noop/span.js +5 -0
  94. package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
  95. package/packages/dd-trace/src/openfeature/index.js +7 -4
  96. package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
  97. package/packages/dd-trace/src/openfeature/writers/exposures.js +70 -15
  98. package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
  99. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  100. package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
  101. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  102. package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
  103. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  104. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
  105. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
  106. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  107. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  108. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  109. package/packages/dd-trace/src/opentracing/propagation/text_map.js +429 -352
  110. package/packages/dd-trace/src/opentracing/span.js +28 -0
  111. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  112. package/packages/dd-trace/src/plugin_manager.js +2 -0
  113. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  114. package/packages/dd-trace/src/plugins/util/test.js +74 -21
  115. package/packages/dd-trace/src/plugins/util/web.js +2 -65
  116. package/packages/dd-trace/src/profiling/oom.js +4 -3
  117. package/packages/dd-trace/src/profiling/profiler.js +7 -0
  118. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  119. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  120. package/packages/dd-trace/src/proxy.js +36 -8
  121. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  122. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
  123. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
  124. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
  125. package/packages/dd-trace/src/serverless/vercel.js +122 -0
  126. package/packages/dd-trace/src/serverless.js +37 -20
  127. package/packages/dd-trace/src/span_stats.js +40 -3
  128. package/packages/dd-trace/src/telemetry/metrics.js +62 -8
  129. package/packages/dd-trace/src/telemetry/send-data.js +1 -0
  130. package/packages/dd-trace/src/tracer.js +22 -0
  131. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  132. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  133. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  134. package/vendor/dist/https-proxy-agent/LICENSE +22 -0
  135. package/vendor/dist/https-proxy-agent/index.js +4 -0
  136. package/vendor/dist/protobufjs/index.js +1 -1
  137. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  138. package/vendor/dist/proxy-from-env/LICENSE +20 -0
  139. package/vendor/dist/proxy-from-env/index.js +1 -0
@@ -8,19 +8,24 @@ const {
8
8
  buildSpanMetadata,
9
9
  buildTags,
10
10
  durationNs,
11
+ generateRunId,
11
12
  hasEntries,
12
13
  inferMetricType,
13
- normalizeEvaluators,
14
14
  mergeTags,
15
+ normalizeEvaluators,
15
16
  normalizeJsonMetricValue,
17
+ normalizePositiveInteger,
18
+ recordTagsToObject,
16
19
  sleep,
17
20
  stringify,
18
21
  timestampMs,
19
22
  validateEvaluatorName,
20
23
  } = require('./util')
21
24
 
25
+ const TASK_ERROR_MESSAGE = 'task error; evaluation skipped'
26
+
22
27
  // One span per experiment row (LLM Obs experiment span wire format).
23
- function toSpan (row, metadata, ids, spanName, userTags) {
28
+ function toSpan (row, metadata, ids, spanName, userTags, recordTags) {
24
29
  const meta = {
25
30
  input: row.input ?? null,
26
31
  output: row.output ?? null,
@@ -33,6 +38,21 @@ function toSpan (row, metadata, ids, spanName, userTags) {
33
38
  meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: row.errorStack ?? '' }
34
39
  }
35
40
 
41
+ const tags = buildTags({
42
+ ...userTags,
43
+ ...recordTagsToObject(recordTags),
44
+ }, {
45
+ experiment_id: ids.experimentId,
46
+ run_id: ids.runId,
47
+ run_iteration: ids.runIteration,
48
+ project_id: ids.projectId,
49
+ dataset_id: ids.datasetId,
50
+ dataset_record_id: ids.datasetRecordId,
51
+ dataset_name: ids.datasetName,
52
+ experiment_name: ids.experimentName,
53
+ project_name: ids.projectName,
54
+ })
55
+
36
56
  return {
37
57
  span_id: row.spanId,
38
58
  trace_id: row.traceId,
@@ -43,19 +63,13 @@ function toSpan (row, metadata, ids, spanName, userTags) {
43
63
  duration: row.durationNs,
44
64
  status: row.isError ? 'error' : 'ok',
45
65
  meta,
46
- tags: buildTags(userTags, {
47
- experiment_id: ids.experimentId,
48
- run_id: ids.runId,
49
- run_iteration: ids.runIteration,
50
- dataset_id: ids.datasetId,
51
- dataset_record_id: ids.datasetRecordId,
52
- }),
66
+ tags,
53
67
  }
54
68
  }
55
69
 
56
70
  // One metric per evaluator per row or summary evaluator.
57
71
  function toMetric (
58
- label, value, errorMessage, spanId, traceId, timestampMs, experimentId, userTags, source = 'custom'
72
+ label, value, errorMessage, spanId, traceId, timestampMs, experimentId, userTags, source = 'custom', ids = {}
59
73
  ) {
60
74
  const metric = {
61
75
  metric_source: source,
@@ -63,7 +77,11 @@ function toMetric (
63
77
  span_id: spanId,
64
78
  trace_id: traceId,
65
79
  timestamp_ms: timestampMs,
66
- tags: buildTags(userTags, { experiment_id: experimentId }),
80
+ tags: buildTags(userTags, {
81
+ experiment_id: experimentId,
82
+ run_id: ids.runId,
83
+ run_iteration: ids.runIteration,
84
+ }),
67
85
  experiment_id: experimentId,
68
86
  }
69
87
 
@@ -115,8 +133,43 @@ function errorMessage (error) {
115
133
  return error.message ?? String(error)
116
134
  }
117
135
 
118
- // Builder + run() orchestration: runs rows sequentially, emits one root span
119
- // per dataset row, and posts spans + metrics to the experiments events API.
136
+ function createLimiter (concurrency) {
137
+ const waiting = []
138
+ let active = 0
139
+ let cancellation
140
+
141
+ const limit = async function limit (fn, cancelOnError = false) {
142
+ if (cancellation !== undefined) throw cancellation.error
143
+ if (active >= concurrency) {
144
+ await new Promise((resolve, reject) => waiting.push({ resolve, reject }))
145
+ }
146
+ if (cancellation !== undefined) throw cancellation.error
147
+ active++
148
+ try {
149
+ return await fn()
150
+ } catch (error) {
151
+ if (cancelOnError) limit.cancel(error)
152
+ throw error
153
+ } finally {
154
+ active--
155
+ const next = waiting.shift()
156
+ if (next !== undefined) next.resolve()
157
+ }
158
+ }
159
+
160
+ limit.cancel = (error) => {
161
+ if (cancellation !== undefined) return
162
+ cancellation = { error }
163
+ const queued = [...waiting]
164
+ waiting.length = 0
165
+ for (const waiter of queued) waiter.reject(error)
166
+ }
167
+
168
+ return limit
169
+ }
170
+
171
+ // Builder + run() orchestration: emits one root span per dataset row and
172
+ // posts spans + metrics to the experiments events API.
120
173
  class Experiment {
121
174
  #client
122
175
  #llmobs
@@ -130,6 +183,8 @@ class Experiment {
130
183
  #config
131
184
  #tags
132
185
  #metadata
186
+ #runs
187
+ #projectName
133
188
  #projectId
134
189
  #experimentId
135
190
  #runId
@@ -152,8 +207,13 @@ class Experiment {
152
207
  this.#evaluators = normalizeEvaluators(options.evaluators, 'row')
153
208
  this.#summaryEvaluators = normalizeEvaluators(options.summaryEvaluators, 'summary')
154
209
  this.#config = { ...options.config }
210
+ const filterTags = this.#dataset.filterTags?.() ?? []
211
+ if (filterTags.length > 0) this.#config.filtered_record_tags = filterTags
212
+ this.#projectName = options.projectName
155
213
  this.#tags = { ...options.tags }
214
+ if (this.#projectName !== undefined) this.#tags.project_name = this.#projectName
156
215
  this.#metadata = { ...options.metadata }
216
+ this.#runs = this.#external ? 1 : normalizePositiveInteger(options.runs ?? 1, 'runs')
157
217
  this.#projectId = null
158
218
  this.#experimentId = null
159
219
  this.#runId = null
@@ -270,6 +330,7 @@ class Experiment {
270
330
  projectId: this.#projectId,
271
331
  datasetId: this.#dataset.id,
272
332
  datasetRecordId: input.datasetRecordId,
333
+ projectName: this.#projectName,
273
334
  runId: input.runId ?? this.#runId,
274
335
  runIteration: input.runIteration ?? this.#runIteration,
275
336
  }, input.name ?? this.#name, mergeTags(this.#tags, input.tags))
@@ -313,6 +374,8 @@ class Experiment {
313
374
  log.warn('LLMObs experiments: skipping external metric %s because it has neither value nor error', metric.label)
314
375
  continue
315
376
  }
377
+ const metricTags = mergeTags(this.#tags, metric.tags)
378
+ if (this.#projectName !== undefined) metricTags.project_name = this.#projectName
316
379
  payload.push(toMetric(
317
380
  metric.label,
318
381
  metric.value,
@@ -321,7 +384,7 @@ class Experiment {
321
384
  span.traceId,
322
385
  timestampMs(metric.timestamp),
323
386
  experimentId,
324
- mergeTags(this.#tags, metric.tags),
387
+ metricTags,
325
388
  metric.source ?? 'custom'
326
389
  ))
327
390
  }
@@ -348,10 +411,12 @@ class Experiment {
348
411
  maxRetries = 0,
349
412
  retryDelay = (attempt) => 100 * (attempt + 1),
350
413
  throwOnErrors = false,
414
+ concurrency = 10,
351
415
  } = options
352
416
 
353
417
  if (maxRetries < 0) throw new Error('maxRetries must be >= 0')
354
418
  if (typeof retryDelay !== 'function') throw new TypeError('retryDelay must be a function')
419
+ const concurrencyLimit = normalizePositiveInteger(concurrency, 'concurrency')
355
420
 
356
421
  const projectId = await this.#client.ensureProjectId()
357
422
 
@@ -369,13 +434,12 @@ class Experiment {
369
434
  dataset_id: datasetId,
370
435
  description: this.#description,
371
436
  ensure_unique: true,
372
- run_count: 1,
437
+ run_count: this.#runs,
373
438
  metadata: { tags: buildTags(this.#tags, {}) },
374
439
  }
375
440
  const datasetVersion = this.#dataset.version()
376
441
  if (datasetVersion !== null) attributes.dataset_version = datasetVersion
377
- // eslint-disable-next-line no-restricted-syntax -- faster than tracking entries while copying arbitrary config
378
- if (Object.keys(this.#config).length > 0) attributes.config = this.#config
442
+ if (hasEntries(this.#config)) attributes.config = this.#config
379
443
 
380
444
  let created
381
445
  try {
@@ -385,28 +449,21 @@ class Experiment {
385
449
  }
386
450
  this.#experimentId = created.experimentId
387
451
  const experimentId = this.#experimentId
388
- const runId = id().toString(16).padStart(16, '0')
389
- const runIteration = 0
390
452
 
391
453
  try {
392
454
  const records = this.#dataset.records()
393
455
  const recordIds = this.#dataset.recordIds()
394
- const rows = []
395
- const spans = []
396
- const metrics = []
397
- const evaluatorResults = {}
398
456
  const usesLLMObsTrace = Boolean(this.#llmobs?.enabled)
399
- let hasRowError = false
457
+ const runs = []
458
+ let hasRunError = false
400
459
 
401
- for (let i = 0; i < records.length; i++) {
402
- const record = records[i]
403
- const datasetRecordId = i < recordIds.length ? recordIds[i] : ''
404
- // Rows currently run sequentially by design; jobs/concurrency is a P1 follow-up.
460
+ for (let runIndex = 0; runIndex < this.#runs; runIndex++) {
461
+ const runId = generateRunId()
462
+ const runIteration = runIndex + 1
405
463
  // eslint-disable-next-line no-await-in-loop
406
- const row = await this.#processRecord({
407
- index: i,
408
- record,
409
- datasetRecordId,
464
+ const result = await this.#runSingle({
465
+ records,
466
+ recordIds,
410
467
  projectId,
411
468
  datasetId,
412
469
  experimentId,
@@ -415,78 +472,296 @@ class Experiment {
415
472
  maxRetries,
416
473
  retryDelay,
417
474
  throwOnErrors,
475
+ concurrency: concurrencyLimit,
476
+ usesLLMObsTrace,
418
477
  })
478
+ runs.push(result.run)
479
+ // Submit each run before starting the next iteration so results are available incrementally.
480
+ // eslint-disable-next-line no-await-in-loop
481
+ await this.#postEvents(experimentId, result.spans, result.metrics)
482
+ this.#llmobs?.flush?.()
483
+ if (result.hasRowError) hasRunError = true
484
+ }
419
485
 
420
- const timestampMs = Date.now()
421
- for (const [label, evaluator] of this.#evaluators) {
422
- if (!evaluatorResults[label]) evaluatorResults[label] = []
423
- if (row.isError) {
424
- const msg = 'task error; evaluation skipped'
425
- row.evaluationErrors[label] = msg
426
- evaluatorResults[label].push(null)
427
- metrics.push(toMetric(label, null, msg, row.spanId, row.traceId, timestampMs, experimentId, this.#tags))
428
- continue
429
- }
430
- try {
431
- // eslint-disable-next-line no-await-in-loop
432
- const value = await this.#runWithRetries(
433
- () => evaluator(record.input, row.output, record.expectedOutput),
434
- maxRetries,
435
- retryDelay
436
- )
437
- row.evaluations[label] = value
438
- evaluatorResults[label].push(value)
439
- metrics.push(toMetric(label, value, null, row.spanId, row.traceId, timestampMs, experimentId, this.#tags))
440
- } catch (err) {
441
- if (throwOnErrors) throw err
442
- const msg = err.message ?? String(err)
443
- row.evaluationErrors[label] = msg
444
- evaluatorResults[label].push(null)
445
- metrics.push(toMetric(label, null, msg, row.spanId, row.traceId, timestampMs, experimentId, this.#tags))
446
- }
447
- }
486
+ // A row error doesn't abort the run, but the experiment didn't succeed cleanly.
487
+ await this.#updateStatus(
488
+ experimentId,
489
+ hasRunError ? 'failed' : 'completed',
490
+ hasRunError ? 'one or more rows failed' : null
491
+ )
448
492
 
449
- rows.push(row)
450
- if (row.isError || hasEntries(row.evaluationErrors)) hasRowError = true
451
- if (!usesLLMObsTrace) {
452
- spans.push(toSpan(row, record.metadata, {
453
- experimentId,
454
- projectId,
455
- datasetId,
456
- datasetRecordId,
457
- runId,
458
- runIteration,
459
- }, this.#task.name || this.#name, this.#tags))
460
- }
461
- }
493
+ const firstRun = runs[0]
494
+ return new ExperimentResult(
495
+ experimentId,
496
+ firstRun?.rows ?? [],
497
+ this.url(),
498
+ runs,
499
+ firstRun?.summaryEvaluations ?? {}
500
+ )
501
+ } catch (err) {
502
+ await this.#updateStatus(experimentId, 'failed', err.message ?? String(err))
503
+ throw err
504
+ }
505
+ }
462
506
 
463
- const summaryEvaluations = await this.#runSummaryEvaluators(rows, records, evaluatorResults, {
507
+ async #runSingle ({
508
+ records,
509
+ recordIds,
510
+ projectId,
511
+ datasetId,
512
+ experimentId,
513
+ runId,
514
+ runIteration,
515
+ maxRetries,
516
+ retryDelay,
517
+ throwOnErrors,
518
+ concurrency,
519
+ usesLLMObsTrace,
520
+ }) {
521
+ const limit = createLimiter(concurrency)
522
+ const results = await this.#mapRecords(records, (index) => {
523
+ const record = records[index]
524
+ const datasetRecordId = index < recordIds.length ? recordIds[index] : ''
525
+ return this.#processRecordWithEvaluators({
526
+ index,
527
+ record,
528
+ datasetRecordId,
529
+ projectId,
530
+ datasetId,
531
+ experimentId,
532
+ runId,
533
+ runIteration,
464
534
  maxRetries,
465
535
  retryDelay,
466
536
  throwOnErrors,
467
- experimentId,
468
- metrics,
537
+ limit,
469
538
  })
470
- if (hasEntries(summaryEvaluations)) {
471
- for (const value of Object.values(summaryEvaluations)) {
472
- if (value?.error) hasRowError = true
473
- }
539
+ }, limit, concurrency, throwOnErrors)
540
+
541
+ const rows = new Array(results.length)
542
+ const spans = []
543
+ const metrics = []
544
+ const evaluatorResults = {}
545
+ let hasRowError = false
546
+ for (const [label] of this.#evaluators) evaluatorResults[label] = []
547
+
548
+ for (let i = 0; i < results.length; i++) {
549
+ const result = results[i]
550
+ const row = result.row
551
+ rows[i] = row
552
+ for (const [label] of this.#evaluators) {
553
+ const value = Object.hasOwn(result.evaluatorValues, label) ? result.evaluatorValues[label] : null
554
+ evaluatorResults[label].push(value)
555
+ }
556
+ for (const metric of result.metrics) metrics.push(metric)
557
+ if (result.hasRowError) hasRowError = true
558
+ if (!usesLLMObsTrace) {
559
+ spans.push(toSpan(row, records[i].metadata, {
560
+ experimentId,
561
+ projectId,
562
+ datasetId,
563
+ datasetRecordId: i < recordIds.length ? recordIds[i] : '',
564
+ datasetName: this.#dataset.name(),
565
+ experimentName: this.#name,
566
+ projectName: this.#projectName,
567
+ runId,
568
+ runIteration,
569
+ }, this.#task.name || this.#name, this.#tags, records[i].tags))
474
570
  }
571
+ }
475
572
 
476
- await this.#postEvents(experimentId, spans, metrics)
477
- this.#llmobs?.flush?.()
478
- // A row error doesn't abort the run, but the experiment didn't succeed cleanly.
479
- await this.#updateStatus(
573
+ const summaryEvaluations = await this.#runSummaryEvaluators(rows, records, evaluatorResults, {
574
+ maxRetries,
575
+ retryDelay,
576
+ throwOnErrors,
577
+ experimentId,
578
+ runId,
579
+ runIteration,
580
+ metrics,
581
+ limit,
582
+ })
583
+ if (hasEntries(summaryEvaluations)) {
584
+ for (const value of Object.values(summaryEvaluations)) {
585
+ if (value?.error !== null && value?.error !== undefined) hasRowError = true
586
+ }
587
+ }
588
+
589
+ return {
590
+ run: new ExperimentRun({ runId, runIteration, hasError: hasRowError, rows, summaryEvaluations }),
591
+ spans,
592
+ metrics,
593
+ hasRowError,
594
+ }
595
+ }
596
+
597
+ async #mapRecords (records, processRecord, limit, concurrency, throwOnErrors) {
598
+ const results = new Array(records.length)
599
+ let nextIndex = 0
600
+
601
+ const worker = async () => {
602
+ while (nextIndex < records.length) {
603
+ const index = nextIndex++
604
+ // eslint-disable-next-line no-await-in-loop -- each worker processes one record at a time
605
+ results[index] = await processRecord(index)
606
+ }
607
+ }
608
+
609
+ const workers = new Array(Math.min(concurrency, records.length))
610
+ for (let i = 0; i < workers.length; i++) workers[i] = worker()
611
+
612
+ try {
613
+ await Promise.all(workers)
614
+ } catch (error) {
615
+ if (throwOnErrors) limit.cancel(error)
616
+ throw error
617
+ }
618
+ return results
619
+ }
620
+
621
+ async #processRecordWithEvaluators ({
622
+ index,
623
+ record,
624
+ datasetRecordId,
625
+ projectId,
626
+ datasetId,
627
+ experimentId,
628
+ runId,
629
+ runIteration,
630
+ maxRetries,
631
+ retryDelay,
632
+ throwOnErrors,
633
+ limit,
634
+ }) {
635
+ const row = await limit(() => this.#processRecord({
636
+ index,
637
+ record,
638
+ datasetRecordId,
639
+ projectId,
640
+ datasetId,
641
+ experimentId,
642
+ runId,
643
+ runIteration,
644
+ maxRetries,
645
+ retryDelay,
646
+ throwOnErrors,
647
+ }), throwOnErrors)
648
+ const timestampMs = Date.now()
649
+ const metrics = []
650
+ const evaluatorValues = {}
651
+ let firstError
652
+
653
+ const pending = new Array(this.#evaluators.length)
654
+ for (let i = 0; i < this.#evaluators.length; i++) {
655
+ const [label, evaluator] = this.#evaluators[i]
656
+ pending[i] = this.#runRowEvaluator({
657
+ label,
658
+ evaluator,
659
+ row,
660
+ record,
661
+ timestampMs,
480
662
  experimentId,
481
- hasRowError ? 'failed' : 'completed',
482
- hasRowError ? 'one or more rows failed' : null
483
- )
663
+ runId,
664
+ runIteration,
665
+ maxRetries,
666
+ retryDelay,
667
+ throwOnErrors,
668
+ limit,
669
+ })
670
+ }
484
671
 
485
- const run = new ExperimentRun({ runId, runIteration, rows, summaryEvaluations })
486
- return new ExperimentResult(experimentId, rows, this.url(), [run], summaryEvaluations)
672
+ const evaluatorResults = await Promise.all(pending)
673
+ for (const result of evaluatorResults) {
674
+ if (result.metric !== null) metrics.push(result.metric)
675
+ evaluatorValues[result.label] = result.value
676
+ if (result.error !== undefined && firstError === undefined) firstError = result.error
677
+ }
678
+ if (firstError !== undefined) throw firstError
679
+
680
+ return {
681
+ row,
682
+ metrics,
683
+ evaluatorValues,
684
+ hasRowError: row.isError || hasEntries(row.evaluationErrors),
685
+ }
686
+ }
687
+
688
+ async #runRowEvaluator ({
689
+ label,
690
+ evaluator,
691
+ row,
692
+ record,
693
+ timestampMs,
694
+ experimentId,
695
+ runId,
696
+ runIteration,
697
+ maxRetries,
698
+ retryDelay,
699
+ throwOnErrors,
700
+ limit,
701
+ }) {
702
+ if (row.isError) {
703
+ row.evaluationErrors[label] = TASK_ERROR_MESSAGE
704
+ return {
705
+ label,
706
+ value: null,
707
+ metric: toMetric(
708
+ label,
709
+ null,
710
+ TASK_ERROR_MESSAGE,
711
+ row.spanId,
712
+ row.traceId,
713
+ timestampMs,
714
+ experimentId,
715
+ this.#tags,
716
+ 'custom',
717
+ { runId, runIteration }
718
+ ),
719
+ }
720
+ }
721
+
722
+ try {
723
+ const value = await limit(() => this.#runWithRetries(
724
+ () => evaluator(record.input, row.output, record.expectedOutput),
725
+ maxRetries,
726
+ retryDelay
727
+ ), throwOnErrors)
728
+ row.evaluations[label] = value
729
+ return {
730
+ label,
731
+ value,
732
+ metric: toMetric(
733
+ label,
734
+ value,
735
+ null,
736
+ row.spanId,
737
+ row.traceId,
738
+ timestampMs,
739
+ experimentId,
740
+ this.#tags,
741
+ 'custom',
742
+ { runId, runIteration }
743
+ ),
744
+ }
487
745
  } catch (err) {
488
- await this.#updateStatus(experimentId, 'failed', err.message ?? String(err))
489
- throw err
746
+ if (throwOnErrors) throw err
747
+ const msg = err.message ?? String(err)
748
+ row.evaluationErrors[label] = msg
749
+ return {
750
+ label,
751
+ value: null,
752
+ metric: toMetric(
753
+ label,
754
+ null,
755
+ msg,
756
+ row.spanId,
757
+ row.traceId,
758
+ timestampMs,
759
+ experimentId,
760
+ this.#tags,
761
+ 'custom',
762
+ { runId, runIteration }
763
+ ),
764
+ }
490
765
  }
491
766
  }
492
767
 
@@ -521,7 +796,8 @@ class Experiment {
521
796
  dataset_name: this.#dataset.name(),
522
797
  experiment_name: this.#name,
523
798
  }
524
- const tags = mergeTags(this.#tags, autoTags)
799
+ if (this.#projectName !== undefined) autoTags.project_name = this.#projectName
800
+ const tags = mergeTags(this.#tags, { ...recordTagsToObject(record.tags), ...autoTags })
525
801
 
526
802
  const execute = () => this.#runWithRetries(
527
803
  () => this.#task(record.input, this.#config, record.metadata),
@@ -615,17 +891,65 @@ class Experiment {
615
891
  const metadata = records.map(record => buildSpanMetadata(record.metadata, this.#config))
616
892
  const summaryEvaluations = {}
617
893
  const timestampMs = Date.now()
894
+ const pending = new Array(this.#summaryEvaluators.length)
895
+ let firstError
896
+
897
+ for (let i = 0; i < this.#summaryEvaluators.length; i++) {
898
+ const [label, evaluator] = this.#summaryEvaluators[i]
899
+ pending[i] = this.#runSummaryEvaluator({
900
+ label,
901
+ evaluator,
902
+ inputs,
903
+ outputs,
904
+ expectedOutputs,
905
+ evaluatorResults,
906
+ metadata,
907
+ timestampMs,
908
+ options,
909
+ })
910
+ }
618
911
 
619
- for (const [label, evaluator] of this.#summaryEvaluators) {
620
- try {
621
- // eslint-disable-next-line no-await-in-loop
622
- const value = await this.#runWithRetries(
623
- () => evaluator(inputs, outputs, expectedOutputs, evaluatorResults, metadata),
624
- options.maxRetries,
625
- options.retryDelay
626
- )
627
- summaryEvaluations[label] = { value, error: null }
628
- options.metrics.push(toMetric(
912
+ let results
913
+ try {
914
+ results = await Promise.all(pending)
915
+ } catch (err) {
916
+ if (options.throwOnErrors) options.limit.cancel(err)
917
+ throw err
918
+ }
919
+ for (const result of results) {
920
+ if (result.error !== undefined) {
921
+ if (firstError === undefined) firstError = result.error
922
+ continue
923
+ }
924
+ summaryEvaluations[result.label] = result.evaluation
925
+ options.metrics.push(result.metric)
926
+ }
927
+ if (firstError !== undefined) throw firstError
928
+
929
+ return summaryEvaluations
930
+ }
931
+
932
+ async #runSummaryEvaluator ({
933
+ label,
934
+ evaluator,
935
+ inputs,
936
+ outputs,
937
+ expectedOutputs,
938
+ evaluatorResults,
939
+ metadata,
940
+ timestampMs,
941
+ options,
942
+ }) {
943
+ try {
944
+ const value = await options.limit(() => this.#runWithRetries(
945
+ () => evaluator(inputs, outputs, expectedOutputs, evaluatorResults, metadata),
946
+ options.maxRetries,
947
+ options.retryDelay
948
+ ), options.throwOnErrors)
949
+ return {
950
+ label,
951
+ evaluation: { value, error: null },
952
+ metric: toMetric(
629
953
  label,
630
954
  value,
631
955
  null,
@@ -634,13 +958,17 @@ class Experiment {
634
958
  timestampMs,
635
959
  options.experimentId,
636
960
  this.#tags,
637
- 'summary'
638
- ))
639
- } catch (err) {
640
- if (options.throwOnErrors) throw err
641
- const msg = err.message ?? String(err)
642
- summaryEvaluations[label] = { value: null, error: msg }
643
- options.metrics.push(toMetric(
961
+ 'summary',
962
+ { runId: options.runId, runIteration: options.runIteration }
963
+ ),
964
+ }
965
+ } catch (err) {
966
+ if (options.throwOnErrors) throw err
967
+ const msg = err.message ?? String(err)
968
+ return {
969
+ label,
970
+ evaluation: { value: null, error: msg },
971
+ metric: toMetric(
644
972
  label,
645
973
  null,
646
974
  msg,
@@ -649,11 +977,11 @@ class Experiment {
649
977
  timestampMs,
650
978
  options.experimentId,
651
979
  this.#tags,
652
- 'summary'
653
- ))
980
+ 'summary',
981
+ { runId: options.runId, runIteration: options.runIteration }
982
+ ),
654
983
  }
655
984
  }
656
- return summaryEvaluations
657
985
  }
658
986
 
659
987
  async #postEvents (experimentId, spans, metrics) {