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
@@ -3,7 +3,7 @@
3
3
  const { inspect } = require('node:util')
4
4
  const { channel } = require('dc-polyfill')
5
5
 
6
- const request = require('../common/request')
6
+ const commonRequest = require('../common/request')
7
7
  const { logIntegrations, logAgentError } = require('../../startup-log')
8
8
  const runtimeMetrics = require('../../runtime_metrics')
9
9
  const log = require('../../log')
@@ -15,12 +15,15 @@ const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
15
15
  const firstFlushChannel = channel('dd-trace:exporter:first-flush')
16
16
 
17
17
  class AgentWriter extends BaseWriter {
18
+ #request = commonRequest
19
+ #requestTracker
20
+
18
21
  constructor (...args) {
19
22
  super({
20
23
  ...args[0],
21
24
  beforeFirstFlush: () => firstFlushChannel.publish(),
22
25
  })
23
- const { prioritySampler, lookup, protocolVersion, headers } = args[0]
26
+ const { prioritySampler, lookup, protocolVersion, headers, isTestOptimization } = args[0]
24
27
  const AgentEncoder = getEncoder(protocolVersion)
25
28
 
26
29
  this._prioritySampler = prioritySampler
@@ -28,13 +31,33 @@ class AgentWriter extends BaseWriter {
28
31
  this._protocolVersion = protocolVersion
29
32
  this._headers = headers
30
33
  this._encoder = new AgentEncoder(this)
34
+ if (isTestOptimization) {
35
+ this.#request = require('../../ci-visibility/exporters/request')
36
+ const TestOptimizationRequestTracker = require('../../ci-visibility/exporters/agentless/request-tracker')
37
+ this.#requestTracker = new TestOptimizationRequestTracker(this)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Flushes payloads, including requests already in flight during Test Optimization finalization.
43
+ *
44
+ * @param {(error?: Error) => void} [done]
45
+ * @param {{ deadline?: number }} [options]
46
+ * @returns {void}
47
+ */
48
+ flush (done, options) {
49
+ if (this.#requestTracker) {
50
+ this.#requestTracker.flush(done, options)
51
+ return
52
+ }
53
+ super.flush(done, options)
31
54
  }
32
55
 
33
- _sendPayload (data, count, done) {
56
+ _sendPayload (data, count, done, flushOptions) {
34
57
  runtimeMetrics.increment(`${METRIC_PREFIX}.requests`, true)
35
58
 
36
59
  const { _headers, _lookup, _protocolVersion, _url } = this
37
- makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, (err, res, status, headers) => {
60
+ const onResponse = (err, res, status, headers) => {
38
61
  if (status) {
39
62
  runtimeMetrics.increment(`${METRIC_PREFIX}.responses`, true)
40
63
  runtimeMetrics.increment(`${METRIC_PREFIX}.responses.by.status`, `status:${status}`, true)
@@ -49,7 +72,7 @@ class AgentWriter extends BaseWriter {
49
72
 
50
73
  if (err) {
51
74
  log.errorWithoutTelemetry('Error sending payload to the agent (status code: %s)', err.status, err)
52
- done()
75
+ done(flushOptions?.deadline === undefined ? undefined : err)
53
76
  return
54
77
  }
55
78
 
@@ -74,7 +97,19 @@ class AgentWriter extends BaseWriter {
74
97
  runtimeMetrics.increment(`${METRIC_PREFIX}.errors.by.name`, `name:${e.name}`, true)
75
98
  }
76
99
  done()
77
- })
100
+ }
101
+ makeRequest(
102
+ _protocolVersion,
103
+ data,
104
+ count,
105
+ _url,
106
+ _headers,
107
+ _lookup,
108
+ flushOptions,
109
+ this.#request,
110
+ this.#requestTracker,
111
+ onResponse
112
+ )
78
113
  }
79
114
  }
80
115
 
@@ -84,7 +119,7 @@ function getEncoder (protocolVersion) {
84
119
  : require('../../encode/0.4').AgentEncoder
85
120
  }
86
121
 
87
- function makeRequest (version, data, count, url, headers, lookup, cb) {
122
+ function makeRequest (version, data, count, url, headers, lookup, flushOptions, request, requestTracker, cb) {
88
123
  const options = {
89
124
  path: `/v${version}/traces`,
90
125
  method: 'PUT',
@@ -100,16 +135,21 @@ function makeRequest (version, data, count, url, headers, lookup, cb) {
100
135
  lookup,
101
136
  url,
102
137
  }
138
+ if (flushOptions?.deadline !== undefined) {
139
+ options.deadline = flushOptions.deadline
140
+ }
103
141
 
104
142
  log.debug('Request to the agent: %j', options)
105
143
 
106
- request(data, options, (err, res, status, headers) => {
144
+ const onResponse = (err, res, status, headers) => {
107
145
  logIntegrations()
108
146
  if (status !== 404 && status !== 200 && err) {
109
147
  logAgentError({ status, message: err.message ?? inspect(err) })
110
148
  }
111
149
  cb(err, res, status, headers)
112
- })
150
+ }
151
+ if (requestTracker) requestTracker.send(request, data, options, onResponse)
152
+ else request(data, options, onResponse)
113
153
  }
114
154
 
115
155
  module.exports = AgentWriter
@@ -24,23 +24,25 @@ class BufferingExporter {
24
24
  this._export(trace)
25
25
  }
26
26
 
27
- _export (payload, writer = this._writer, timerKey = '_timer') {
27
+ _export (payload, writer = this._writer, timerKey = '_timer', deferImmediateFlush = false) {
28
28
  if (this._config.isCiVisibility) {
29
29
  incrementCountMetric(TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION, {}, payload.length)
30
30
  }
31
- writer.append(payload)
31
+ const appended = writer.append(payload)
32
32
 
33
33
  const { flushInterval } = this._config
34
34
 
35
- if (flushInterval === 0) {
35
+ if (flushInterval === 0 && !deferImmediateFlush) {
36
36
  writer.flush()
37
- } else if (this[timerKey] === undefined) {
37
+ } else if (flushInterval !== 0 && this[timerKey] === undefined) {
38
38
  this[timerKey] = setTimeout(() => {
39
39
  writer.flush()
40
40
  this[timerKey] = undefined
41
41
  }, flushInterval)
42
42
  this[timerKey].unref?.()
43
43
  }
44
+
45
+ return appended
44
46
  }
45
47
 
46
48
  getUncodedTraces () {
@@ -17,10 +17,10 @@ class Writer {
17
17
 
18
18
  #isFirstFlush = true
19
19
 
20
- flush (done = () => {}) {
20
+ flush (done = () => {}, options) {
21
21
  const count = this._encoder.count()
22
22
 
23
- if (!request.writable) {
23
+ if (!request.writable && options?.deadline === undefined) {
24
24
  this._encoder.reset()
25
25
  done()
26
26
  } else if (count > 0) {
@@ -44,23 +44,28 @@ class Writer {
44
44
  done()
45
45
  return
46
46
  }
47
- this._sendPayload(payload, count, done)
47
+ if (options === undefined) {
48
+ this._sendPayload(payload, count, done)
49
+ } else {
50
+ this._sendPayload(payload, count, done, options)
51
+ }
48
52
  } else {
49
53
  done()
50
54
  }
51
55
  }
52
56
 
53
- append (payload) {
54
- if (!request.writable) {
57
+ append (payload, options) {
58
+ if (!request.writable && options?.deadline === undefined) {
55
59
  // eslint-disable-next-line eslint-rules/eslint-log-printf-style
56
60
  log.debug(() => `Maximum number of active requests reached. Payload discarded: ${safeJSONStringify(payload)}`)
57
- return
61
+ return false
58
62
  }
59
63
 
60
64
  // eslint-disable-next-line eslint-rules/eslint-log-printf-style
61
65
  log.debug(() => `Encoding payload: ${safeJSONStringify(payload)}`)
62
66
 
63
67
  this._encode(payload)
68
+ return true
64
69
  }
65
70
 
66
71
  _encode (payload) {
@@ -16,6 +16,11 @@ module.exports = {
16
16
  PROPAGATED_ML_APP_KEY: '_dd.p.llmobs_ml_app',
17
17
  PROPAGATED_SESSION_ID_KEY: '_dd.p.llmobs_sid',
18
18
  PARENT_ID_KEY: '_ml_obs.llmobs_parent_id',
19
+
20
+ PARENT_AGENT_NAME: '_ml_obs.parent_agent_name',
21
+ PARENT_AGENT_SPAN_ID: '_ml_obs.parent_agent_span_id',
22
+ PROPAGATED_PARENT_AGENT_ID_KEY: '_dd.p.llmobs_pagent_span_id',
23
+ PROPAGATED_PARENT_AGENT_NAME_KEY: '_dd.p.llmobs_pagent_name',
19
24
  PROPAGATED_SAMPLE_RATE_KEY: '_dd.p.llmobs_sr',
20
25
  PROPAGATED_SAMPLING_DECISION_KEY: '_dd.p.llmobs_sd',
21
26
  SAMPLE_RATE: '_ml_obs.sample_rate',
@@ -3,6 +3,9 @@
3
3
  // Control-plane HTTP client for LLM Obs Experiments. Uses the global `fetch`,
4
4
  // so this module adds no new dependency; credentials and site come from config.
5
5
 
6
+ const { Dataset, DatasetRecord } = require('./dataset')
7
+ const { ExperimentResult } = require('./result')
8
+
6
9
  const API_BASE_PATH = '/api/v2/llm-obs/v1'
7
10
 
8
11
  // Control-plane host for a Datadog site, e.g.
@@ -22,12 +25,67 @@ function appHost (site) {
22
25
  return site.split('.').length === 2 ? `app.${site}` : site
23
26
  }
24
27
 
28
+ function datasetRecordFromResource (resource) {
29
+ const attrs = resource?.attributes ?? resource ?? {}
30
+ const id = String(resource?.id ?? attrs.id ?? '')
31
+ if (id === '') throw new Error('Dataset record response is missing an id')
32
+ return new DatasetRecord(
33
+ attrs.input ?? null,
34
+ attrs.expected_output ?? null,
35
+ attrs.metadata ?? {},
36
+ id
37
+ )
38
+ }
39
+
40
+ function datasetVersionFromResource (resource) {
41
+ const attrs = resource?.attributes ?? resource ?? {}
42
+ return attrs.valid_from_version ?? attrs.version ?? null
43
+ }
44
+
45
+ function datasetVersionFromResources (resources) {
46
+ const versions = resources
47
+ .map(datasetVersionFromResource)
48
+ .filter(version => version != null)
49
+ .map(Number)
50
+ .filter(Number.isFinite)
51
+ if (versions.length === 0) return null
52
+ return Math.max(...versions)
53
+ }
54
+
55
+ function datasetMutationResultFromResources (resources) {
56
+ return {
57
+ records: resources.map(datasetRecordFromResource),
58
+ version: datasetVersionFromResources(resources),
59
+ }
60
+ }
61
+
62
+ function datasetFromResource (client, projectId, resource) {
63
+ const attrs = resource?.attributes ?? resource ?? {}
64
+ const version = attrs.current_version ?? null
65
+ return Dataset.fromExisting(
66
+ client,
67
+ String(attrs.name ?? ''),
68
+ String(attrs.description ?? ''),
69
+ resource?.id ?? attrs.id ?? null,
70
+ projectId,
71
+ [],
72
+ version,
73
+ version
74
+ )
75
+ }
76
+
77
+ function experimentFromResource (client, resource) {
78
+ const id = resource?.id
79
+ return new ExperimentResult(id, [], id == null ? null : `${client.appBase}/llm/experiments/${id}`)
80
+ }
81
+
25
82
  class ExperimentsClient {
26
83
  #apiKey
27
84
  #appKey
28
85
  #site
29
86
  #projectName
30
87
  #timeout
88
+ apiBase
31
89
  #cachedProjectId
32
90
 
33
91
  constructor ({ apiKey, appKey, site, projectName, timeout = 30_000 } = {}) {
@@ -36,6 +94,7 @@ class ExperimentsClient {
36
94
  this.#site = site
37
95
  this.#projectName = projectName
38
96
  this.#timeout = timeout
97
+ this.apiBase = `https://${apiHost(this.#site)}`
39
98
  this.#cachedProjectId = null
40
99
  }
41
100
 
@@ -61,7 +120,7 @@ class ExperimentsClient {
61
120
  // Low-level request. Builds https://api.<site><path>, attaches both keys, and
62
121
  // returns the parsed JSON body. Throws with status + body on a non-2xx.
63
122
  async request (method, path, body) {
64
- const url = `https://${apiHost(this.#site)}${path}`
123
+ const url = `${this.apiBase}${path}`
65
124
  const headers = {
66
125
  'DD-API-KEY': this.#apiKey,
67
126
  'DD-APPLICATION-KEY': this.#appKey,
@@ -92,6 +151,107 @@ class ExperimentsClient {
92
151
  return text ? JSON.parse(text) : {}
93
152
  }
94
153
 
154
+ jsonApiRequest (method, path, type, attributes) {
155
+ return this.request(method, path, {
156
+ data: { type, attributes },
157
+ })
158
+ }
159
+
160
+ async createProject (name) {
161
+ const response = await this.jsonApiRequest('POST', `${API_BASE_PATH}/projects`, 'projects', { name })
162
+ return response?.data ?? null
163
+ }
164
+
165
+ async createDataset (projectId, attributes) {
166
+ const response = await this.jsonApiRequest('POST', `${API_BASE_PATH}/${projectId}/datasets`, 'datasets', attributes)
167
+ return datasetFromResource(this, projectId, response?.data ?? null)
168
+ }
169
+
170
+ deleteDataset (projectId, datasetId) {
171
+ return this.jsonApiRequest('POST', `${API_BASE_PATH}/${projectId}/datasets/delete`, 'datasets', {
172
+ type: 'soft',
173
+ dataset_ids: [datasetId],
174
+ })
175
+ }
176
+
177
+ async listDatasets (projectId, options = {}) {
178
+ const query = new URLSearchParams()
179
+ if (options.name !== undefined) query.set('filter[name]', options.name)
180
+ const response = await this.request('GET', `${API_BASE_PATH}/${projectId}/datasets?${query.toString()}`)
181
+ const resources = Array.isArray(response?.data) ? response.data : []
182
+ return resources.map(resource => datasetFromResource(this, projectId, resource))
183
+ }
184
+
185
+ async appendDatasetRecords (projectId, datasetId, records) {
186
+ const response = await this.jsonApiRequest(
187
+ 'POST',
188
+ `${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records`,
189
+ 'datasets',
190
+ { records }
191
+ )
192
+ // The append-records response has used both a top-level `records` array
193
+ // and JSON:API `data` resources. Accept either so generated/custom record
194
+ // ids are preserved for experiment row tagging.
195
+ const resources = Array.isArray(response?.records)
196
+ ? response.records
197
+ : (Array.isArray(response?.data) ? response.data : [])
198
+ return resources.map(datasetRecordFromResource)
199
+ }
200
+
201
+ async batchUpdateDatasetRecords (projectId, datasetId, attributes) {
202
+ const response = await this.request(
203
+ 'POST',
204
+ `${API_BASE_PATH}/${projectId}/datasets/${datasetId}/batch_update`,
205
+ {
206
+ data: {
207
+ type: 'datasets',
208
+ id: datasetId,
209
+ attributes: {
210
+ insert_records: attributes.insert_records ?? [],
211
+ update_records: attributes.update_records ?? [],
212
+ delete_records: attributes.delete_records ?? [],
213
+ deduplicate: attributes.deduplicate !== false,
214
+ create_new_version: attributes.create_new_version !== false,
215
+ },
216
+ },
217
+ }
218
+ )
219
+ const resources = Array.isArray(response?.records)
220
+ ? response.records
221
+ : (Array.isArray(response?.data) ? response.data : [])
222
+ return datasetMutationResultFromResources(resources)
223
+ }
224
+
225
+ async listDatasetRecords (projectId, datasetId, options = {}) {
226
+ const query = new URLSearchParams()
227
+ if (options.cursor) query.set('page[cursor]', options.cursor)
228
+ if (options.version !== undefined && options.version !== null) query.set('filter[version]', String(options.version))
229
+ const response = await this.request(
230
+ 'GET',
231
+ `${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records?${query.toString()}`
232
+ )
233
+ const records = Array.isArray(response?.data) ? response.data.map(datasetRecordFromResource) : []
234
+ return { records, after: response?.meta?.after ?? '' }
235
+ }
236
+
237
+ async createExperiment (attributes) {
238
+ const response = await this.jsonApiRequest('POST', `${API_BASE_PATH}/experiments`, 'experiments', attributes)
239
+ return experimentFromResource(this, response?.data ?? null)
240
+ }
241
+
242
+ postExperimentEvents (experimentId, attributes) {
243
+ return this.jsonApiRequest(
244
+ 'POST',
245
+ `${API_BASE_PATH}/experiments/${experimentId}/events`,
246
+ 'experiments',
247
+ attributes
248
+ )
249
+ }
250
+
251
+ updateExperiment (experimentId, attributes) {
252
+ return this.jsonApiRequest('PATCH', `${API_BASE_PATH}/experiments/${experimentId}`, 'experiments', attributes)
253
+ }
254
+
95
255
  // Resolve the project id for `name`, creating it if absent. The create
96
256
  // endpoint is get-or-create on name, so repeated calls return the same id.
97
257
  // Cached after the first resolution.
@@ -100,14 +260,12 @@ class ExperimentsClient {
100
260
 
101
261
  let response
102
262
  try {
103
- response = await this.request('POST', `${API_BASE_PATH}/projects`, {
104
- data: { type: 'projects', attributes: { name } },
105
- })
263
+ response = await this.createProject(name)
106
264
  } catch (err) {
107
265
  throw new Error(`Failed to create or get project '${name}': ${err.message}`)
108
266
  }
109
267
 
110
- this.#cachedProjectId = response?.data?.id ?? null
268
+ this.#cachedProjectId = response?.id ?? null
111
269
  return this.#cachedProjectId
112
270
  }
113
271
  }