dd-trace 6.12.0 → 6.14.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 (251) hide show
  1. package/LICENSE-3rdparty.csv +3 -1
  2. package/ci/init.js +8 -3
  3. package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
  4. package/ci/test-optimization-validation/ci-remediation.js +6 -6
  5. package/ci/test-optimization-validation/command-blocker.js +7 -5
  6. package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
  7. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  8. package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
  9. package/ci/test-optimization-validation/package-check.js +1 -1
  10. package/ci/test-optimization-validation/report-writer.js +7 -5
  11. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  12. package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
  13. package/ci/vitest-no-worker-init-setup.mjs +13 -13
  14. package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
  15. package/index.d.ts +127 -26
  16. package/package.json +22 -19
  17. package/packages/datadog-esbuild/index.js +3 -1
  18. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  19. package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
  20. package/packages/datadog-instrumentations/src/fs.js +24 -20
  21. package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
  22. package/packages/datadog-instrumentations/src/graphql.js +16 -16
  23. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  24. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
  25. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  26. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +266 -0
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  33. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
  34. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +13 -0
  35. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +279 -0
  36. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +12 -1
  37. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
  38. package/packages/datadog-instrumentations/src/jest.js +165 -38
  39. package/packages/datadog-instrumentations/src/knex.js +76 -0
  40. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  41. package/packages/datadog-instrumentations/src/mariadb.js +13 -0
  42. package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
  43. package/packages/datadog-instrumentations/src/mocha/main.js +25 -15
  44. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  45. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
  46. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
  47. package/packages/datadog-instrumentations/src/net.js +14 -17
  48. package/packages/datadog-instrumentations/src/next.js +212 -62
  49. package/packages/datadog-instrumentations/src/openai.js +42 -40
  50. package/packages/datadog-instrumentations/src/pg.js +15 -0
  51. package/packages/datadog-instrumentations/src/pino.js +15 -4
  52. package/packages/datadog-instrumentations/src/playwright.js +137 -27
  53. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
  54. package/packages/datadog-instrumentations/src/sequelize.js +94 -2
  55. package/packages/datadog-instrumentations/src/undici.js +53 -4
  56. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +44 -7
  57. package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
  58. package/packages/datadog-instrumentations/src/vitest-worker.js +82 -14
  59. package/packages/datadog-instrumentations/src/webdriverio.js +986 -1
  60. package/packages/datadog-instrumentations/src/winston.js +6 -3
  61. package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
  62. package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
  63. package/packages/datadog-plugin-amqp10/src/util.js +2 -1
  64. package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
  65. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
  66. package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
  67. package/packages/datadog-plugin-cucumber/src/index.js +0 -2
  68. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
  69. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  70. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
  71. package/packages/datadog-plugin-graphql/src/execute.js +998 -196
  72. package/packages/datadog-plugin-graphql/src/index.js +27 -7
  73. package/packages/datadog-plugin-graphql/src/jit.js +136 -0
  74. package/packages/datadog-plugin-graphql/src/request.js +18 -27
  75. package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
  76. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
  77. package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
  78. package/packages/datadog-plugin-graphql/src/utils.js +208 -120
  79. package/packages/datadog-plugin-graphql/src/validate.js +10 -23
  80. package/packages/datadog-plugin-http/src/client.js +2 -15
  81. package/packages/datadog-plugin-http2/src/client.js +2 -15
  82. package/packages/datadog-plugin-jest/src/index.js +0 -6
  83. package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
  84. package/packages/datadog-plugin-mocha/src/index.js +403 -75
  85. package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
  86. package/packages/datadog-plugin-mysql/src/index.js +41 -15
  87. package/packages/datadog-plugin-net/src/tcp.js +3 -1
  88. package/packages/datadog-plugin-next/src/index.js +63 -29
  89. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  90. package/packages/datadog-plugin-playwright/src/index.js +176 -28
  91. package/packages/datadog-plugin-undici/src/index.js +29 -56
  92. package/packages/datadog-plugin-vitest/src/index.js +6 -12
  93. package/packages/dd-trace/src/aiguard/evaluation.js +96 -21
  94. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  95. package/packages/dd-trace/src/aiguard/messages/anthropic.js +29 -10
  96. package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
  97. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
  98. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  99. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  100. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  101. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  102. package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
  103. package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
  104. package/packages/dd-trace/src/appsec/graphql.js +6 -2
  105. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  106. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
  107. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
  108. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +11 -36
  109. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
  110. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  111. package/packages/dd-trace/src/appsec/lambda.js +44 -26
  112. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  113. package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
  114. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
  115. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
  116. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
  117. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +7 -150
  118. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
  119. package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
  121. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +397 -234
  122. package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
  124. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
  125. package/packages/dd-trace/src/ci-visibility/final-flush.js +21 -0
  126. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
  127. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
  128. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
  129. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +176 -0
  130. package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
  131. package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
  132. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  133. package/packages/dd-trace/src/config/index.js +75 -14
  134. package/packages/dd-trace/src/config/parsers.js +11 -1
  135. package/packages/dd-trace/src/config/stable.js +3 -3
  136. package/packages/dd-trace/src/config/supported-configurations.json +47 -2
  137. package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
  138. package/packages/dd-trace/src/debugger/config.js +12 -1
  139. package/packages/dd-trace/src/debugger/constants.js +1 -0
  140. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
  141. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +33 -0
  142. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -39
  143. package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
  144. package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
  145. package/packages/dd-trace/src/debugger/index.js +23 -3
  146. package/packages/dd-trace/src/dogstatsd.js +532 -71
  147. package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
  148. package/packages/dd-trace/src/encode/0.4.js +5 -5
  149. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
  150. package/packages/dd-trace/src/evp_proxy/direct.js +27 -23
  151. package/packages/dd-trace/src/exporters/agent/index.js +24 -3
  152. package/packages/dd-trace/src/exporters/agent/writer.js +28 -11
  153. package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
  154. package/packages/dd-trace/src/exporters/agentless/writer.js +100 -118
  155. package/packages/dd-trace/src/exporters/common/agents.js +28 -6
  156. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
  157. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
  158. package/packages/dd-trace/src/exporters/common/request.js +31 -11
  159. package/packages/dd-trace/src/exporters/common/url.js +40 -1
  160. package/packages/dd-trace/src/exporters/common/writer.js +31 -7
  161. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  162. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  163. package/packages/dd-trace/src/flush.js +105 -0
  164. package/packages/dd-trace/src/id.js +14 -0
  165. package/packages/dd-trace/src/index.js +5 -2
  166. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  167. package/packages/dd-trace/src/llmobs/experiments/dataset.js +52 -2
  168. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  169. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  170. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  171. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  172. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  173. package/packages/dd-trace/src/llmobs/index.js +111 -48
  174. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  175. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
  176. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
  177. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  178. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  179. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
  180. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  181. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  182. package/packages/dd-trace/src/llmobs/util.js +0 -33
  183. package/packages/dd-trace/src/llmobs/writers/base.js +106 -53
  184. package/packages/dd-trace/src/log/index.js +7 -1
  185. package/packages/dd-trace/src/log-submission/log-submission-plugin.js +249 -0
  186. package/packages/dd-trace/src/noop/span.js +5 -0
  187. package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
  188. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  189. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  190. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  191. package/packages/dd-trace/src/opentelemetry/logs/index.js +9 -25
  192. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  193. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
  194. package/packages/dd-trace/src/opentelemetry/metrics/index.js +18 -23
  195. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  196. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +18 -6
  197. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +14 -2
  198. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  199. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  200. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
  201. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
  202. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  203. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
  204. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  205. package/packages/dd-trace/src/opentracing/propagation/text_map.js +494 -351
  206. package/packages/dd-trace/src/opentracing/span.js +28 -0
  207. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  208. package/packages/dd-trace/src/payload-tagging/index.js +2 -1
  209. package/packages/dd-trace/src/plugin_manager.js +2 -0
  210. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
  211. package/packages/dd-trace/src/plugins/tracing.js +12 -10
  212. package/packages/dd-trace/src/plugins/util/ci.js +3 -1
  213. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
  214. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  215. package/packages/dd-trace/src/plugins/util/test.js +157 -41
  216. package/packages/dd-trace/src/plugins/util/url.js +2 -2
  217. package/packages/dd-trace/src/plugins/util/web.js +6 -74
  218. package/packages/dd-trace/src/profiling/config.js +1 -0
  219. package/packages/dd-trace/src/profiling/oom.js +11 -5
  220. package/packages/dd-trace/src/profiling/profiler.js +30 -13
  221. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  222. package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
  223. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  224. package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
  225. package/packages/dd-trace/src/proxy.js +108 -10
  226. package/packages/dd-trace/src/remote_config/index.js +319 -101
  227. package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
  228. package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
  229. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  230. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +10 -1
  231. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +22 -7
  232. package/packages/dd-trace/src/sampling_rule.js +36 -9
  233. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +51 -0
  234. package/packages/dd-trace/src/serverless/vercel.js +155 -0
  235. package/packages/dd-trace/src/serverless.js +45 -21
  236. package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
  237. package/packages/dd-trace/src/span_stats.js +64 -31
  238. package/packages/dd-trace/src/startup-log.js +3 -2
  239. package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
  240. package/packages/dd-trace/src/telemetry/send-data.js +18 -16
  241. package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
  242. package/packages/dd-trace/src/tracer.js +26 -5
  243. package/packages/dd-trace/src/tracer_metadata.js +11 -3
  244. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  245. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  246. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  247. package/vendor/dist/protobufjs/index.js +1 -1
  248. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  249. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
  250. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +0 -53
  251. package/packages/dd-trace/src/encode/agentless-json.js +0 -209
@@ -2,11 +2,13 @@
2
2
 
3
3
  const log = require('../../log')
4
4
  const { ExperimentsClient } = require('./client')
5
- const { Dataset, DatasetRecord } = require('./dataset')
5
+ const { Dataset } = require('./dataset')
6
6
  const { Experiment, ExternalExperiment } = require('./experiment')
7
7
  const { validateTagsList } = require('./util')
8
8
  const NoopExperiments = require('./noop')
9
9
 
10
+ const DEFAULT_PROJECT_NAME = 'default-project'
11
+
10
12
  // Poll `attempt` with exponential backoff until it returns true or the time
11
13
  // budget is spent. Used for eventually-consistent reads (pullDataset).
12
14
  async function retryWithBackoff (attempt, { maxTotalMs = 30_000, baseDelayMs = 250, maxDelayMs = 8000 } = {}) {
@@ -33,8 +35,8 @@ class Experiments {
33
35
 
34
36
  constructor (config, llmobs) {
35
37
  this.#config = config
36
- this.#llmobs = llmobs
37
- this.#projectName = config.llmobs?.mlApp || config.service
38
+ this.#llmobs = config.llmobs?.mlApp || config.service ? llmobs : undefined
39
+ this.#projectName = config.llmobs?.projectName || DEFAULT_PROJECT_NAME
38
40
  this.#client = this.#clientForProject(this.#projectName)
39
41
  }
40
42
 
@@ -51,27 +53,26 @@ class Experiments {
51
53
  })
52
54
  }
53
55
 
56
+ /**
57
+ * @param {string | undefined} projectName
58
+ * @returns {ExperimentsClient}
59
+ */
60
+ #clientForOperation (projectName) {
61
+ if (projectName !== undefined && projectName !== this.#projectName) {
62
+ return this.#clientForProject(projectName)
63
+ }
64
+ if (this.#client === undefined) this.#client = this.#clientForProject(projectName)
65
+ return this.#client
66
+ }
67
+
54
68
  // Create a local dataset buffer. Pushed remotely on first experiment run.
55
69
  createDataset (name, descriptionOrOptions = '') {
56
70
  const options = typeof descriptionOrOptions === 'string'
57
71
  ? { description: descriptionOrOptions }
58
72
  : (descriptionOrOptions ?? {})
59
- const dataset = new Dataset(this.#client, name, options.description ?? '')
60
- const recordIds = new Set()
61
- if ((options.records) != null) {
62
- for (const record of options.records) {
63
- if (record.id !== undefined && (typeof record.id !== 'string' || record.id.length === 0)) {
64
- throw new Error('record id must be a non-empty string')
65
- }
66
- if (record.id !== undefined) {
67
- if (recordIds.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
68
- recordIds.add(record.id)
69
- }
70
- dataset.addRecord(
71
- new DatasetRecord(record.inputData, record.expectedOutput, record.metadata, record.id, record.tags)
72
- )
73
- }
74
- }
73
+ const client = this.#clientForOperation(options.projectName)
74
+ const dataset = new Dataset(client, name, options.description ?? '')
75
+ if ((options.records) != null) dataset.addRecords(options.records)
75
76
  return dataset
76
77
  }
77
78
 
@@ -80,9 +81,11 @@ class Experiments {
80
81
  // wait until that many records are readable. Pass `tags` to filter records by
81
82
  // dataset record tags.
82
83
  async pullDataset (name, options = {}) {
83
- const { expectedRecordCount, maxWaitMs = 30_000, tags, version } = options
84
+ const { expectedRecordCount, maxWaitMs = 30_000, projectName, tags, version } = options
84
85
  const filterTags = validateTagsList(tags)
85
- const projectId = await this.#client.ensureProjectId()
86
+ const client = this.#clientForOperation(projectName)
87
+ const resolvedProjectName = projectName ?? this.#projectName
88
+ const projectId = await client.ensureProjectId()
86
89
 
87
90
  let pulledDataset = null
88
91
  let records = []
@@ -93,7 +96,7 @@ class Experiments {
93
96
  const succeeded = await retryWithBackoff(async () => {
94
97
  try {
95
98
  if (pulledDataset === null) {
96
- const datasets = await this.#client.listDatasets(projectId, { name })
99
+ const datasets = await client.listDatasets(projectId, { name })
97
100
  for (const dataset of datasets) {
98
101
  if (dataset.name() === name) {
99
102
  pulledDataset = dataset
@@ -109,7 +112,7 @@ class Experiments {
109
112
  // Follow the meta.after / page[cursor] pagination until the last page.
110
113
  for (;;) {
111
114
  // eslint-disable-next-line no-await-in-loop
112
- const page = await this.#client.listDatasetRecords(projectId, pulledDataset.id(), {
115
+ const page = await client.listDatasetRecords(projectId, pulledDataset.id(), {
113
116
  cursor,
114
117
  tags: filterTags,
115
118
  version: datasetVersion,
@@ -129,13 +132,13 @@ class Experiments {
129
132
  }, { maxTotalMs: maxWaitMs })
130
133
 
131
134
  if (pulledDataset === null && lastError) {
132
- throw new Error(`Failed to list datasets in project '${this.#projectName}': ${lastError}`)
135
+ throw new Error(`Failed to list datasets in project '${resolvedProjectName}': ${lastError}`)
133
136
  }
134
137
  if (pulledDataset === null) {
135
- throw new Error(`Dataset '${name}' not found in project '${this.#projectName}' (after ${maxWaitMs}ms)`)
138
+ throw new Error(`Dataset '${name}' not found in project '${resolvedProjectName}' (after ${maxWaitMs}ms)`)
136
139
  }
137
140
  if (!succeeded && lastError) {
138
- throw new Error(`Failed to fetch records for dataset '${name}' in project '${this.#projectName}': ${lastError}`)
141
+ throw new Error(`Failed to fetch records for dataset '${name}' in project '${resolvedProjectName}': ${lastError}`)
139
142
  }
140
143
  if (!succeeded && expectedRecordCount != null) {
141
144
  throw new Error(
@@ -151,7 +154,7 @@ class Experiments {
151
154
  }
152
155
 
153
156
  return Dataset.fromExisting(
154
- this.#client,
157
+ client,
155
158
  name,
156
159
  pulledDataset.description(),
157
160
  pulledDataset.id(),
@@ -163,9 +166,26 @@ class Experiments {
163
166
  )
164
167
  }
165
168
 
166
- // Build an experiment: { name, dataset, task, evaluators, description?, config?, tags? }.
169
+ // Build an experiment with a dataset, task, evaluators, and optional project/config/tags.
167
170
  experiment (options) {
168
- return new Experiment(this.#client, options, this.#llmobs)
171
+ const datasetProjectName = options?.dataset?.projectName?.()
172
+ if (options?.projectName !== undefined &&
173
+ datasetProjectName !== undefined &&
174
+ options.projectName !== datasetProjectName) {
175
+ throw new Error(
176
+ `Experiment project '${options.projectName}' does not match dataset project '${datasetProjectName}'`
177
+ )
178
+ }
179
+ const projectName = options?.projectName ?? datasetProjectName
180
+ const client = this.#clientForOperation(projectName)
181
+ const usesDatasetOverride = datasetProjectName !== undefined && datasetProjectName !== this.#projectName
182
+ const resolvedProjectName = projectName ?? this.#config.llmobs?.projectName
183
+ const experimentOptions = options?.projectName === undefined &&
184
+ (usesDatasetOverride || this.#config.llmobs?.projectName !== undefined) &&
185
+ resolvedProjectName !== undefined
186
+ ? { ...options, projectName: resolvedProjectName }
187
+ : options
188
+ return new Experiment(client, experimentOptions, this.#llmobs)
169
189
  }
170
190
 
171
191
  /**
@@ -177,10 +197,11 @@ class Experiments {
177
197
  * @returns {Promise<ExternalExperiment>}
178
198
  */
179
199
  startExperiment (options) {
180
- const client = options?.projectName === undefined || options.projectName === this.#projectName
181
- ? this.#client
182
- : this.#clientForProject(options.projectName)
183
- return new Experiment(client, { ...options, external: true }).start()
200
+ const client = this.#clientForOperation(options?.projectName)
201
+ const experimentOptions = options?.projectName === undefined && this.#config.llmobs?.projectName !== undefined
202
+ ? { ...options, projectName: this.#config.llmobs.projectName }
203
+ : options
204
+ return new Experiment(client, { ...experimentOptions, external: true }).start()
184
205
  .then(experiment => new ExternalExperiment(experiment))
185
206
  }
186
207
  }
@@ -195,15 +216,6 @@ function createExperiments (config, llmobs) {
195
216
  log.warn('LLMObs experiments: missing api and/or app keys, set DD_API_KEY and DD_APP_KEY')
196
217
  return new NoopExperiments('DD_API_KEY and DD_APP_KEY are required for experiments')
197
218
  }
198
- if (!config.llmobs?.mlApp && !config.service) {
199
- const reason = 'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
200
- 'tracer.init()) to name the LLM Obs project, or DD_SERVICE (or service in tracer.init()) as a fallback, ' +
201
- 'then retry'
202
- const experiments = new Experiments(config, llmobs)
203
- return new NoopExperiments(reason, {
204
- startExperiment: (options) => experiments.startExperiment(options),
205
- })
206
- }
207
219
  return new Experiments(config, llmobs)
208
220
  }
209
221
 
@@ -7,6 +7,15 @@ const NOOP_EXPERIMENT_ID = '00000000-0000-0000-0000-000000000000'
7
7
  const NOOP_SPAN_ID = '0000000000000000'
8
8
  const NOOP_TRACE_ID = '00000000000000000000000000000000'
9
9
 
10
+ /**
11
+ * @typedef {object} DatasetRecordNew
12
+ * @property {string} [id]
13
+ * @property {unknown} inputData
14
+ * @property {unknown} [expectedOutput]
15
+ * @property {Record<string, unknown>} [metadata]
16
+ * @property {string[]} [tags]
17
+ */
18
+
10
19
  class NoopDataset {
11
20
  #name
12
21
  #description
@@ -37,6 +46,24 @@ class NoopDataset {
37
46
  return this
38
47
  }
39
48
 
49
+ /**
50
+ * Add multiple records to a dataset.
51
+ * @param {DatasetRecordNew[]} records
52
+ * @returns {NoopDataset} This dataset for chaining.
53
+ */
54
+ addRecords (records) {
55
+ for (const record of records) {
56
+ this.#records.push({
57
+ id: record.id ?? null,
58
+ input: record.inputData,
59
+ expectedOutput: record.expectedOutput ?? null,
60
+ metadata: record.metadata ?? {},
61
+ tags: [...(record.tags ?? [])],
62
+ })
63
+ }
64
+ return this
65
+ }
66
+
40
67
  update (index, fields) {
41
68
  const record = this.#records[index]
42
69
  if (record == null) return this
@@ -114,6 +141,10 @@ class NoopDataset {
114
141
  return null
115
142
  }
116
143
 
144
+ projectName () {
145
+ return null
146
+ }
147
+
117
148
  version () {
118
149
  return null
119
150
  }
@@ -165,7 +196,13 @@ class NoopExperiment {
165
196
  }
166
197
 
167
198
  run () {
168
- return Promise.resolve({ experimentId: null, rows: [], url: null })
199
+ return Promise.resolve({
200
+ experimentId: null,
201
+ rows: [],
202
+ summaryEvaluations: {},
203
+ runs: [],
204
+ url: null,
205
+ })
169
206
  }
170
207
 
171
208
  /**
@@ -200,11 +237,9 @@ class NoopExperiment {
200
237
  // throwing, so intentionally disabled experiments remain graceful.
201
238
  class NoopExperiments {
202
239
  #reason
203
- #startExperiment
204
240
 
205
- constructor (reason, options = {}) {
241
+ constructor (reason) {
206
242
  this.#reason = reason || 'LLMObs experiments are not available'
207
- this.#startExperiment = options.startExperiment
208
243
  }
209
244
 
210
245
  #warn () {
@@ -231,10 +266,6 @@ class NoopExperiments {
231
266
  * @returns {Promise<ExternalExperiment>}
232
267
  */
233
268
  startExperiment (options = {}) {
234
- if (this.#startExperiment !== undefined && options.projectName) {
235
- return this.#startExperiment(options)
236
- }
237
-
238
269
  this.#warn()
239
270
  return Promise.resolve(new ExternalExperiment(new NoopExperiment(options.name, true)))
240
271
  }
@@ -27,6 +27,7 @@ class ExperimentRun {
27
27
  constructor (fields) {
28
28
  this.runId = fields.runId
29
29
  this.runIteration = fields.runIteration
30
+ this.hasError = fields.hasError
30
31
  this.rows = fields.rows
31
32
  this.summaryEvaluations = fields.summaryEvaluations
32
33
  }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { randomUUID } = require('node:crypto')
4
+
3
5
  const log = require('../../log')
4
6
 
5
7
  // Matches the backend and dd-trace-py evaluator metric label contract.
@@ -42,6 +44,23 @@ function tagOperationsAreEmpty (operations) {
42
44
  )
43
45
  }
44
46
 
47
+ /**
48
+ * @param {unknown} value
49
+ * @param {string} name
50
+ * @returns {number}
51
+ */
52
+ function normalizePositiveInteger (value, name) {
53
+ if (!Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer`)
54
+ return value
55
+ }
56
+
57
+ /**
58
+ * @returns {string}
59
+ */
60
+ function generateRunId () {
61
+ return randomUUID()
62
+ }
63
+
45
64
  /**
46
65
  * @param {string} name
47
66
  */
@@ -297,11 +316,13 @@ module.exports = {
297
316
  buildSpanMetadata,
298
317
  buildTags,
299
318
  durationNs,
319
+ generateRunId,
300
320
  hasEntries,
301
321
  inferMetricType,
302
322
  mergeTags,
303
323
  normalizeEvaluators,
304
324
  normalizeJsonMetricValue,
325
+ normalizePositiveInteger,
305
326
  recordTagsToObject,
306
327
  sleep,
307
328
  stringify,
@@ -2,8 +2,9 @@
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
- const { readDatadogTags, writeDatadogTags } = require('../carrier')
5
+ const { registerTelemetryFlusher } = require('../flush')
6
6
  const log = require('../log')
7
+ const { createServerlessDeliveryTracker } = require('../serverless')
7
8
  const { DD_MAJOR } = require('../../../../version')
8
9
  const startupLogs = require('../startup-log')
9
10
  const {
@@ -23,7 +24,7 @@ const {
23
24
  PROPAGATED_TRACE_ID_KEY,
24
25
  } = require('./constants/tags')
25
26
  const { storage } = require('./storage')
26
- const { agentNameWireSafe, appendOptionalPropagatedTag, resolveAgentAttribution, stripTagsetEntry } = require('./util')
27
+ const { agentNameWireSafe, resolveAgentAttribution } = require('./util')
27
28
  const telemetry = require('./telemetry')
28
29
  const LLMObsSpanProcessor = require('./span_processor')
29
30
  const LLMObsEvalMetricsWriter = require('./writers/evaluations')
@@ -57,32 +58,54 @@ let spanWriter
57
58
  /** @type {LLMObsEvalMetricsWriter | null} */
58
59
  let evalWriter
59
60
 
61
+ let unregisterTelemetryFlusher
62
+
60
63
  /** @type {import('../config/config-base')} */
61
64
  let globalTracerConfig
62
65
 
66
+ /**
67
+ * @typedef {object} TraceTagInjection
68
+ * @property {import('../opentracing/span_context')} spanContext
69
+ * @property {Array<string | undefined>} [traceTagReplacements]
70
+ * @property {number} [optionalTraceTagCount]
71
+ */
72
+
63
73
  /**
64
74
  * @param {@type import('../config/config-base')} config
65
75
  */
66
76
  function enable (config) {
67
77
  globalTracerConfig = config
68
78
 
79
+ const retiredSpanWriter = spanWriter
80
+ const retiredEvalWriter = evalWriter
81
+ const isReinitializing = Boolean(retiredSpanWriter || retiredEvalWriter)
82
+ unregisterTelemetryFlusher?.()
83
+ retireWriters(retiredSpanWriter, retiredEvalWriter)
84
+
69
85
  const startTime = performance.now()
70
86
  // create writers and eval writer append and flush channels
71
87
  // span writer append is handled by the span processor
72
88
  evalWriter = new LLMObsEvalMetricsWriter(config)
73
89
  spanWriter = new LLMObsSpanWriter(config)
90
+ const currentEvalWriter = evalWriter
91
+ const currentSpanWriter = spanWriter
92
+ unregisterTelemetryFlusher = registerTelemetryFlusher(done => {
93
+ flushWriters(done, currentSpanWriter, currentEvalWriter)
94
+ })
74
95
 
75
- evalMetricAppendCh.subscribe(handleEvalMetricAppend)
76
- flushCh.subscribe(handleFlush)
77
- registerUserSpanProcessorCh.subscribe(handleRegisterProcessor)
96
+ if (!isReinitializing) {
97
+ evalMetricAppendCh.subscribe(handleEvalMetricAppend)
98
+ flushCh.subscribe(handleFlush)
99
+ registerUserSpanProcessorCh.subscribe(handleRegisterProcessor)
100
+ }
78
101
 
79
102
  // span processing
80
103
  spanProcessor = new LLMObsSpanProcessor(config)
81
104
  spanProcessor.setWriter(spanWriter)
82
- spanFinishCh.subscribe(handleSpanProcess)
105
+ if (!isReinitializing) spanFinishCh.subscribe(handleSpanProcess)
83
106
 
84
107
  // distributed tracing for llmobs
85
- injectCh.subscribe(handleLLMObsInjection)
108
+ if (!isReinitializing) injectCh.subscribe(handleLLMObsInjection)
86
109
 
87
110
  setAgentStrategy(config, useAgentless => {
88
111
  if (useAgentless && !(config.DD_API_KEY && config.site)) {
@@ -94,8 +117,10 @@ function enable (config) {
94
117
  }
95
118
  }
96
119
 
97
- evalWriter?.setAgentless(useAgentless)
98
- spanWriter?.setAgentless(useAgentless)
120
+ // A disable can happen while transport selection is still pending. Keep
121
+ // configuring these writers so their queued lifecycle flushes can drain.
122
+ currentEvalWriter.setAgentless(useAgentless)
123
+ currentSpanWriter.setAgentless(useAgentless)
99
124
 
100
125
  telemetry.recordLLMObsEnabled(startTime, config)
101
126
  log.debug('[LLMObs] Enabled LLM Observability with configuration: %o', config.llmobs)
@@ -109,19 +134,43 @@ function disable () {
109
134
  if (injectCh.hasSubscribers) injectCh.unsubscribe(handleLLMObsInjection)
110
135
  if (registerUserSpanProcessorCh.hasSubscribers) registerUserSpanProcessorCh.unsubscribe(handleRegisterProcessor)
111
136
 
112
- spanWriter?.destroy()
113
- evalWriter?.destroy()
137
+ const retiredSpanWriter = spanWriter
138
+ const retiredEvalWriter = evalWriter
114
139
  spanProcessor?.setWriter(null)
140
+ unregisterTelemetryFlusher?.()
141
+ unregisterTelemetryFlusher = undefined
115
142
 
116
143
  spanWriter = null
117
144
  evalWriter = null
118
145
 
146
+ retireWriters(retiredSpanWriter, retiredEvalWriter)
147
+
119
148
  log.debug('[LLMObs] Disabled LLM Observability')
120
149
  }
121
150
 
151
+ /**
152
+ * Keeps retired writers reachable until their destroy-triggered deliveries complete.
153
+ * @param {LLMObsSpanWriter | null} retiredSpanWriter
154
+ * @param {LLMObsEvalMetricsWriter | null} retiredEvalWriter
155
+ * @returns {void}
156
+ */
157
+ function retireWriters (retiredSpanWriter, retiredEvalWriter) {
158
+ const retiredWriters = [retiredSpanWriter, retiredEvalWriter].filter(Boolean)
159
+ if (retiredWriters.length === 0) return
160
+ let remainingWriters = retiredWriters.length
161
+ const unregisterRetiredFlusher = registerTelemetryFlusher(done => {
162
+ flushWriters(done, retiredSpanWriter, retiredEvalWriter)
163
+ })
164
+ function onWriterDestroyed () {
165
+ if (--remainingWriters === 0) unregisterRetiredFlusher?.()
166
+ }
167
+ for (const writer of retiredWriters) writer.destroy(onWriterDestroyed)
168
+ }
169
+
122
170
  // since LLMObs traces can extend between services and be the same trace,
123
171
  // we need to propagate the parent id, mlApp, session id, and sampling rate/decision.
124
- function handleLLMObsInjection ({ carrier }) {
172
+ /** @param {TraceTagInjection} injection */
173
+ function handleLLMObsInjection (injection) {
125
174
  // Respect the standard propagator's gate: when trace tag propagation is
126
175
  // disabled, don't write `x-datadog-tags` for LLMObs either.
127
176
  if (globalTracerConfig.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH === 0) return
@@ -159,48 +208,62 @@ function handleLLMObsInjection ({ carrier }) {
159
208
  mlObsSpanTags, parent
160
209
  )
161
210
 
162
- // `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*`
163
- // tags, so it may be undefined here — coalesce before appending.
164
- const existing = readDatadogTags(carrier)
165
- let tags = existing || ''
166
- if (parentId) tags += `${tags ? ',' : ''}${PROPAGATED_PARENT_ID_KEY}=${parentId}`
167
- if (mlApp) tags += `${tags ? ',' : ''}${PROPAGATED_ML_APP_KEY}=${mlApp}`
168
- if (sessionId) tags += `${tags ? ',' : ''}${PROPAGATED_SESSION_ID_KEY}=${sessionId}`
169
- if (sampleRate != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLE_RATE_KEY}=${sampleRate}`
170
- if (samplingDecision != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLING_DECISION_KEY}=${samplingDecision}`
171
- if (propagatedTraceId != null) tags += `${tags ? ',' : ''}${PROPAGATED_TRACE_ID_KEY}=${propagatedTraceId}`
172
- // When a local agent attribution is resolved, strip any stale upstream pagent entries that
173
- // `_injectTags` may have already written into the carrier (it propagates all `_dd.p.*` from
174
- // `_trace.tags`). This ensures the downstream sees a consistent id-only or id+name pair
175
- // rather than a mix from different hops. The id is always digit-safe; an unsafe name is wiped.
211
+ const traceTagReplacements = []
212
+ if (parentId) traceTagReplacements.push(PROPAGATED_PARENT_ID_KEY, parentId)
213
+ if (mlApp) traceTagReplacements.push(PROPAGATED_ML_APP_KEY, mlApp)
214
+ if (sessionId) traceTagReplacements.push(PROPAGATED_SESSION_ID_KEY, sessionId)
215
+ if (sampleRate != null) traceTagReplacements.push(PROPAGATED_SAMPLE_RATE_KEY, sampleRate.toString())
216
+ if (samplingDecision != null) {
217
+ traceTagReplacements.push(PROPAGATED_SAMPLING_DECISION_KEY, samplingDecision.toString())
218
+ }
219
+ if (propagatedTraceId != null) traceTagReplacements.push(PROPAGATED_TRACE_ID_KEY, propagatedTraceId)
220
+
221
+ let optionalTraceTagCount = 0
176
222
  if (parentAgentSpanId) {
177
- tags = stripTagsetEntry(tags, PROPAGATED_PARENT_AGENT_ID_KEY)
178
- tags = stripTagsetEntry(tags, PROPAGATED_PARENT_AGENT_NAME_KEY)
223
+ traceTagReplacements.push(PROPAGATED_PARENT_AGENT_ID_KEY, parentAgentSpanId)
224
+ optionalTraceTagCount++
225
+ if (parentAgentName && agentNameWireSafe(parentAgentName)) {
226
+ traceTagReplacements.push(PROPAGATED_PARENT_AGENT_NAME_KEY, parentAgentName)
227
+ } else {
228
+ traceTagReplacements.push(PROPAGATED_PARENT_AGENT_NAME_KEY, undefined)
229
+ }
230
+ optionalTraceTagCount++
179
231
  }
180
- const maxLength = globalTracerConfig.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH
181
- const tagsWithId = appendOptionalPropagatedTag(
182
- tags, PROPAGATED_PARENT_AGENT_ID_KEY, parentAgentSpanId, null, maxLength
183
- )
184
- // Only append the name when the id fit: a name without an id is unresolvable by the backend.
185
- if (tagsWithId === tags) {
186
- tags = tagsWithId
187
- } else {
188
- tags = appendOptionalPropagatedTag(
189
- tagsWithId, PROPAGATED_PARENT_AGENT_NAME_KEY, parentAgentName, agentNameWireSafe, maxLength
190
- )
232
+
233
+ injection.traceTagReplacements = traceTagReplacements
234
+ injection.optionalTraceTagCount = optionalTraceTagCount
235
+ }
236
+
237
+ /**
238
+ * Flushes the specified LLMObs writers and joins deliveries active at the boundary.
239
+ * @param {Function} [done]
240
+ * @param {LLMObsSpanWriter | null} [currentSpanWriter]
241
+ * @param {LLMObsEvalMetricsWriter | null} [currentEvalWriter]
242
+ * @returns {boolean} `true` when a writer throws synchronously.
243
+ */
244
+ function flushWriters (done, currentSpanWriter = spanWriter, currentEvalWriter = evalWriter) {
245
+ let failed = false
246
+ const deliveryTracker = createServerlessDeliveryTracker()
247
+ const flush = writer => {
248
+ try {
249
+ if (deliveryTracker && writer) deliveryTracker.track(complete => writer.flush(complete))
250
+ // Non-serverless flushes retain the existing writer behavior.
251
+ else writer?.flush()
252
+ } catch (error) {
253
+ failed = true
254
+ log.warn('Failed to flush LLMObs writer:', error.message)
255
+ }
191
256
  }
192
- if (tags !== existing) writeDatadogTags(carrier, tags)
257
+
258
+ flush(currentSpanWriter)
259
+ flush(currentEvalWriter)
260
+ deliveryTracker?.waitForIdle(done)
261
+ if (!deliveryTracker) done?.()
262
+ return failed
193
263
  }
194
264
 
195
265
  function handleFlush () {
196
- let err = ''
197
- try {
198
- spanWriter.flush()
199
- evalWriter.flush()
200
- } catch (e) {
201
- err = 'writer_flush_error'
202
- log.warn('Failed to flush LLMObs spans and evaluation metrics:', e.message)
203
- }
266
+ const err = flushWriters() ? 'writer_flush_error' : ''
204
267
  telemetry.recordUserFlush(err)
205
268
  }
206
269
 
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const NoopExperiments = require('./experiments/noop')
3
+ let NoopExperiments
4
4
 
5
5
  class NoopLLMObs {
6
6
  constructor (noopTracer) {
@@ -12,6 +12,7 @@ class NoopLLMObs {
12
12
  }
13
13
 
14
14
  get experiments () {
15
+ NoopExperiments ??= require('./experiments/noop')
15
16
  return new NoopExperiments('LLM Observability is not enabled')
16
17
  }
17
18