dd-trace 6.12.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 (97) hide show
  1. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  2. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  3. package/index.d.ts +127 -26
  4. package/package.json +10 -10
  5. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  6. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  7. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  10. package/packages/datadog-instrumentations/src/jest.js +140 -18
  11. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  12. package/packages/datadog-instrumentations/src/mariadb.js +7 -0
  13. package/packages/datadog-instrumentations/src/pino.js +15 -4
  14. package/packages/datadog-instrumentations/src/playwright.js +34 -20
  15. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -4
  16. package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
  17. package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
  18. package/packages/datadog-instrumentations/src/winston.js +6 -3
  19. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  20. package/packages/datadog-plugin-http/src/client.js +2 -15
  21. package/packages/datadog-plugin-http2/src/client.js +2 -15
  22. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  23. package/packages/datadog-plugin-playwright/src/index.js +9 -1
  24. package/packages/datadog-plugin-undici/src/index.js +2 -15
  25. package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
  26. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  27. package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
  28. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  29. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  30. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  31. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  32. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  33. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  34. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
  35. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
  36. package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
  37. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
  38. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
  39. package/packages/dd-trace/src/config/index.js +2 -1
  40. package/packages/dd-trace/src/config/supported-configurations.json +27 -0
  41. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
  42. package/packages/dd-trace/src/dogstatsd.js +45 -18
  43. package/packages/dd-trace/src/encode/agentless-json.js +5 -4
  44. package/packages/dd-trace/src/exporters/agent/index.js +22 -2
  45. package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
  46. package/packages/dd-trace/src/exporters/common/writer.js +24 -2
  47. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  48. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  49. package/packages/dd-trace/src/flush.js +102 -0
  50. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  51. package/packages/dd-trace/src/llmobs/experiments/dataset.js +51 -2
  52. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  53. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  54. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  55. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  56. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  57. package/packages/dd-trace/src/llmobs/index.js +80 -17
  58. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  59. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  60. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  61. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  62. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  63. package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
  64. package/packages/dd-trace/src/noop/span.js +5 -0
  65. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  66. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  67. package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
  68. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  69. package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
  70. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
  72. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
  73. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  74. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  75. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  76. package/packages/dd-trace/src/opentracing/propagation/text_map.js +410 -342
  77. package/packages/dd-trace/src/opentracing/span.js +28 -0
  78. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  79. package/packages/dd-trace/src/plugin_manager.js +2 -0
  80. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  81. package/packages/dd-trace/src/plugins/util/test.js +74 -21
  82. package/packages/dd-trace/src/plugins/util/web.js +2 -65
  83. package/packages/dd-trace/src/profiling/oom.js +4 -3
  84. package/packages/dd-trace/src/profiling/profiler.js +7 -0
  85. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  86. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  87. package/packages/dd-trace/src/proxy.js +36 -8
  88. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  89. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
  90. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
  91. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
  92. package/packages/dd-trace/src/serverless/vercel.js +122 -0
  93. package/packages/dd-trace/src/serverless.js +37 -20
  94. package/packages/dd-trace/src/span_stats.js +40 -3
  95. package/packages/dd-trace/src/tracer.js +22 -0
  96. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  97. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
@@ -8,11 +8,13 @@ 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,
16
18
  recordTagsToObject,
17
19
  sleep,
18
20
  stringify,
@@ -20,6 +22,8 @@ const {
20
22
  validateEvaluatorName,
21
23
  } = require('./util')
22
24
 
25
+ const TASK_ERROR_MESSAGE = 'task error; evaluation skipped'
26
+
23
27
  // One span per experiment row (LLM Obs experiment span wire format).
24
28
  function toSpan (row, metadata, ids, spanName, userTags, recordTags) {
25
29
  const meta = {
@@ -46,6 +50,7 @@ function toSpan (row, metadata, ids, spanName, userTags, recordTags) {
46
50
  dataset_record_id: ids.datasetRecordId,
47
51
  dataset_name: ids.datasetName,
48
52
  experiment_name: ids.experimentName,
53
+ project_name: ids.projectName,
49
54
  })
50
55
 
51
56
  return {
@@ -64,7 +69,7 @@ function toSpan (row, metadata, ids, spanName, userTags, recordTags) {
64
69
 
65
70
  // One metric per evaluator per row or summary evaluator.
66
71
  function toMetric (
67
- label, value, errorMessage, spanId, traceId, timestampMs, experimentId, userTags, source = 'custom'
72
+ label, value, errorMessage, spanId, traceId, timestampMs, experimentId, userTags, source = 'custom', ids = {}
68
73
  ) {
69
74
  const metric = {
70
75
  metric_source: source,
@@ -72,7 +77,11 @@ function toMetric (
72
77
  span_id: spanId,
73
78
  trace_id: traceId,
74
79
  timestamp_ms: timestampMs,
75
- 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
+ }),
76
85
  experiment_id: experimentId,
77
86
  }
78
87
 
@@ -124,8 +133,43 @@ function errorMessage (error) {
124
133
  return error.message ?? String(error)
125
134
  }
126
135
 
127
- // Builder + run() orchestration: runs rows sequentially, emits one root span
128
- // 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.
129
173
  class Experiment {
130
174
  #client
131
175
  #llmobs
@@ -139,6 +183,8 @@ class Experiment {
139
183
  #config
140
184
  #tags
141
185
  #metadata
186
+ #runs
187
+ #projectName
142
188
  #projectId
143
189
  #experimentId
144
190
  #runId
@@ -163,8 +209,11 @@ class Experiment {
163
209
  this.#config = { ...options.config }
164
210
  const filterTags = this.#dataset.filterTags?.() ?? []
165
211
  if (filterTags.length > 0) this.#config.filtered_record_tags = filterTags
212
+ this.#projectName = options.projectName
166
213
  this.#tags = { ...options.tags }
214
+ if (this.#projectName !== undefined) this.#tags.project_name = this.#projectName
167
215
  this.#metadata = { ...options.metadata }
216
+ this.#runs = this.#external ? 1 : normalizePositiveInteger(options.runs ?? 1, 'runs')
168
217
  this.#projectId = null
169
218
  this.#experimentId = null
170
219
  this.#runId = null
@@ -281,6 +330,7 @@ class Experiment {
281
330
  projectId: this.#projectId,
282
331
  datasetId: this.#dataset.id,
283
332
  datasetRecordId: input.datasetRecordId,
333
+ projectName: this.#projectName,
284
334
  runId: input.runId ?? this.#runId,
285
335
  runIteration: input.runIteration ?? this.#runIteration,
286
336
  }, input.name ?? this.#name, mergeTags(this.#tags, input.tags))
@@ -324,6 +374,8 @@ class Experiment {
324
374
  log.warn('LLMObs experiments: skipping external metric %s because it has neither value nor error', metric.label)
325
375
  continue
326
376
  }
377
+ const metricTags = mergeTags(this.#tags, metric.tags)
378
+ if (this.#projectName !== undefined) metricTags.project_name = this.#projectName
327
379
  payload.push(toMetric(
328
380
  metric.label,
329
381
  metric.value,
@@ -332,7 +384,7 @@ class Experiment {
332
384
  span.traceId,
333
385
  timestampMs(metric.timestamp),
334
386
  experimentId,
335
- mergeTags(this.#tags, metric.tags),
387
+ metricTags,
336
388
  metric.source ?? 'custom'
337
389
  ))
338
390
  }
@@ -359,10 +411,12 @@ class Experiment {
359
411
  maxRetries = 0,
360
412
  retryDelay = (attempt) => 100 * (attempt + 1),
361
413
  throwOnErrors = false,
414
+ concurrency = 10,
362
415
  } = options
363
416
 
364
417
  if (maxRetries < 0) throw new Error('maxRetries must be >= 0')
365
418
  if (typeof retryDelay !== 'function') throw new TypeError('retryDelay must be a function')
419
+ const concurrencyLimit = normalizePositiveInteger(concurrency, 'concurrency')
366
420
 
367
421
  const projectId = await this.#client.ensureProjectId()
368
422
 
@@ -380,13 +434,12 @@ class Experiment {
380
434
  dataset_id: datasetId,
381
435
  description: this.#description,
382
436
  ensure_unique: true,
383
- run_count: 1,
437
+ run_count: this.#runs,
384
438
  metadata: { tags: buildTags(this.#tags, {}) },
385
439
  }
386
440
  const datasetVersion = this.#dataset.version()
387
441
  if (datasetVersion !== null) attributes.dataset_version = datasetVersion
388
- // eslint-disable-next-line no-restricted-syntax -- faster than tracking entries while copying arbitrary config
389
- if (Object.keys(this.#config).length > 0) attributes.config = this.#config
442
+ if (hasEntries(this.#config)) attributes.config = this.#config
390
443
 
391
444
  let created
392
445
  try {
@@ -396,28 +449,21 @@ class Experiment {
396
449
  }
397
450
  this.#experimentId = created.experimentId
398
451
  const experimentId = this.#experimentId
399
- const runId = id().toString(16).padStart(16, '0')
400
- const runIteration = 0
401
452
 
402
453
  try {
403
454
  const records = this.#dataset.records()
404
455
  const recordIds = this.#dataset.recordIds()
405
- const rows = []
406
- const spans = []
407
- const metrics = []
408
- const evaluatorResults = {}
409
456
  const usesLLMObsTrace = Boolean(this.#llmobs?.enabled)
410
- let hasRowError = false
457
+ const runs = []
458
+ let hasRunError = false
411
459
 
412
- for (let i = 0; i < records.length; i++) {
413
- const record = records[i]
414
- const datasetRecordId = i < recordIds.length ? recordIds[i] : ''
415
- // 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
416
463
  // eslint-disable-next-line no-await-in-loop
417
- const row = await this.#processRecord({
418
- index: i,
419
- record,
420
- datasetRecordId,
464
+ const result = await this.#runSingle({
465
+ records,
466
+ recordIds,
421
467
  projectId,
422
468
  datasetId,
423
469
  experimentId,
@@ -426,80 +472,296 @@ class Experiment {
426
472
  maxRetries,
427
473
  retryDelay,
428
474
  throwOnErrors,
475
+ concurrency: concurrencyLimit,
476
+ usesLLMObsTrace,
429
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
+ }
430
485
 
431
- const timestampMs = Date.now()
432
- for (const [label, evaluator] of this.#evaluators) {
433
- if (!evaluatorResults[label]) evaluatorResults[label] = []
434
- if (row.isError) {
435
- const msg = 'task error; evaluation skipped'
436
- row.evaluationErrors[label] = msg
437
- evaluatorResults[label].push(null)
438
- metrics.push(toMetric(label, null, msg, row.spanId, row.traceId, timestampMs, experimentId, this.#tags))
439
- continue
440
- }
441
- try {
442
- // eslint-disable-next-line no-await-in-loop
443
- const value = await this.#runWithRetries(
444
- () => evaluator(record.input, row.output, record.expectedOutput),
445
- maxRetries,
446
- retryDelay
447
- )
448
- row.evaluations[label] = value
449
- evaluatorResults[label].push(value)
450
- metrics.push(toMetric(label, value, null, row.spanId, row.traceId, timestampMs, experimentId, this.#tags))
451
- } catch (err) {
452
- if (throwOnErrors) throw err
453
- const msg = err.message ?? String(err)
454
- row.evaluationErrors[label] = msg
455
- evaluatorResults[label].push(null)
456
- metrics.push(toMetric(label, null, msg, row.spanId, row.traceId, timestampMs, experimentId, this.#tags))
457
- }
458
- }
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
+ )
459
492
 
460
- rows.push(row)
461
- if (row.isError || hasEntries(row.evaluationErrors)) hasRowError = true
462
- if (!usesLLMObsTrace) {
463
- spans.push(toSpan(row, record.metadata, {
464
- experimentId,
465
- projectId,
466
- datasetId,
467
- datasetRecordId,
468
- datasetName: this.#dataset.name(),
469
- experimentName: this.#name,
470
- runId,
471
- runIteration,
472
- }, this.#task.name || this.#name, this.#tags, record.tags))
473
- }
474
- }
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
+ }
475
506
 
476
- 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,
477
534
  maxRetries,
478
535
  retryDelay,
479
536
  throwOnErrors,
480
- experimentId,
481
- metrics,
537
+ limit,
482
538
  })
483
- if (hasEntries(summaryEvaluations)) {
484
- for (const value of Object.values(summaryEvaluations)) {
485
- if (value?.error) hasRowError = true
486
- }
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))
487
570
  }
571
+ }
488
572
 
489
- await this.#postEvents(experimentId, spans, metrics)
490
- this.#llmobs?.flush?.()
491
- // A row error doesn't abort the run, but the experiment didn't succeed cleanly.
492
- 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,
493
662
  experimentId,
494
- hasRowError ? 'failed' : 'completed',
495
- hasRowError ? 'one or more rows failed' : null
496
- )
663
+ runId,
664
+ runIteration,
665
+ maxRetries,
666
+ retryDelay,
667
+ throwOnErrors,
668
+ limit,
669
+ })
670
+ }
671
+
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
+ }
497
721
 
498
- const run = new ExperimentRun({ runId, runIteration, rows, summaryEvaluations })
499
- return new ExperimentResult(experimentId, rows, this.url(), [run], summaryEvaluations)
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
+ }
500
745
  } catch (err) {
501
- await this.#updateStatus(experimentId, 'failed', err.message ?? String(err))
502
- 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
+ }
503
765
  }
504
766
  }
505
767
 
@@ -534,6 +796,7 @@ class Experiment {
534
796
  dataset_name: this.#dataset.name(),
535
797
  experiment_name: this.#name,
536
798
  }
799
+ if (this.#projectName !== undefined) autoTags.project_name = this.#projectName
537
800
  const tags = mergeTags(this.#tags, { ...recordTagsToObject(record.tags), ...autoTags })
538
801
 
539
802
  const execute = () => this.#runWithRetries(
@@ -628,17 +891,65 @@ class Experiment {
628
891
  const metadata = records.map(record => buildSpanMetadata(record.metadata, this.#config))
629
892
  const summaryEvaluations = {}
630
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
+ }
631
911
 
632
- for (const [label, evaluator] of this.#summaryEvaluators) {
633
- try {
634
- // eslint-disable-next-line no-await-in-loop
635
- const value = await this.#runWithRetries(
636
- () => evaluator(inputs, outputs, expectedOutputs, evaluatorResults, metadata),
637
- options.maxRetries,
638
- options.retryDelay
639
- )
640
- summaryEvaluations[label] = { value, error: null }
641
- 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(
642
953
  label,
643
954
  value,
644
955
  null,
@@ -647,13 +958,17 @@ class Experiment {
647
958
  timestampMs,
648
959
  options.experimentId,
649
960
  this.#tags,
650
- 'summary'
651
- ))
652
- } catch (err) {
653
- if (options.throwOnErrors) throw err
654
- const msg = err.message ?? String(err)
655
- summaryEvaluations[label] = { value: null, error: msg }
656
- 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(
657
972
  label,
658
973
  null,
659
974
  msg,
@@ -662,11 +977,11 @@ class Experiment {
662
977
  timestampMs,
663
978
  options.experimentId,
664
979
  this.#tags,
665
- 'summary'
666
- ))
980
+ 'summary',
981
+ { runId: options.runId, runIteration: options.runIteration }
982
+ ),
667
983
  }
668
984
  }
669
- return summaryEvaluations
670
985
  }
671
986
 
672
987
  async #postEvents (experimentId, spans, metrics) {