dd-trace 6.3.0 → 6.5.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 (207) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/index.electron.js +3 -0
  52. package/init.js +18 -0
  53. package/initialize.mjs +1 -1
  54. package/loader-hook.mjs +28 -18
  55. package/openfeature.d.ts +1 -0
  56. package/openfeature.js +4 -0
  57. package/package.json +15 -8
  58. package/packages/datadog-instrumentations/src/ai.js +37 -26
  59. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  60. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  61. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  62. package/packages/datadog-instrumentations/src/cucumber.js +24 -10
  63. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  64. package/packages/datadog-instrumentations/src/express.js +20 -2
  65. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  66. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  68. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  69. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  70. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  71. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  72. package/packages/datadog-instrumentations/src/jest.js +36 -4
  73. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  74. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  75. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  76. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  77. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  78. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  79. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  80. package/packages/datadog-instrumentations/src/next.js +24 -0
  81. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  82. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  83. package/packages/datadog-instrumentations/src/pg.js +2 -2
  84. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  85. package/packages/datadog-instrumentations/src/process.js +3 -0
  86. package/packages/datadog-instrumentations/src/router.js +191 -106
  87. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  88. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  89. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  90. package/packages/datadog-instrumentations/src/ws.js +2 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  92. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  94. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  95. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  96. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
  97. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
  99. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  100. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  101. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  102. package/packages/datadog-plugin-graphql/src/execute.js +177 -59
  103. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  104. package/packages/datadog-plugin-graphql/src/request.js +32 -1
  105. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  106. package/packages/datadog-plugin-graphql/src/validate.js +15 -3
  107. package/packages/datadog-plugin-http2/src/client.js +1 -1
  108. package/packages/datadog-plugin-http2/src/server.js +38 -6
  109. package/packages/datadog-plugin-jest/src/util.js +21 -6
  110. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  111. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  112. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  113. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  114. package/packages/datadog-plugin-router/src/index.js +11 -2
  115. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  116. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  117. package/packages/datadog-plugin-ws/src/close.js +2 -1
  118. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  119. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  120. package/packages/datadog-plugin-ws/src/server.js +2 -1
  121. package/packages/dd-trace/index.electron.js +3 -0
  122. package/packages/dd-trace/index.js +2 -37
  123. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  125. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  126. package/packages/dd-trace/src/azure_metadata.js +2 -1
  127. package/packages/dd-trace/src/bootstrap.js +39 -0
  128. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  129. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  132. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  135. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  137. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  138. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
  139. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
  140. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  141. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  142. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  143. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  144. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  145. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
  146. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  147. package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
  148. package/packages/dd-trace/src/config/helper.js +1 -0
  149. package/packages/dd-trace/src/config/index.js +10 -0
  150. package/packages/dd-trace/src/config/supported-configurations.json +83 -0
  151. package/packages/dd-trace/src/constants.js +7 -0
  152. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  153. package/packages/dd-trace/src/exporter.js +2 -0
  154. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  155. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  156. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  157. package/packages/dd-trace/src/feature-registry.js +29 -0
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  159. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  160. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  161. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  162. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  163. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  164. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  165. package/packages/dd-trace/src/llmobs/index.js +10 -2
  166. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  167. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  168. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  169. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  170. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  171. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  172. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  173. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  174. package/packages/dd-trace/src/llmobs/tagger.js +79 -7
  175. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  176. package/packages/dd-trace/src/llmobs/util.js +32 -0
  177. package/packages/dd-trace/src/noop/proxy.js +5 -4
  178. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  179. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  180. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  181. package/packages/dd-trace/src/openfeature/index.js +0 -2
  182. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  183. package/packages/dd-trace/src/openfeature/register.js +35 -0
  184. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  185. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  186. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  187. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  188. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  189. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  190. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  191. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  192. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  193. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  194. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  195. package/packages/dd-trace/src/plugins/util/test.js +65 -8
  196. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  197. package/packages/dd-trace/src/proxy.js +99 -10
  198. package/packages/dd-trace/src/span_processor.js +5 -0
  199. package/packages/dd-trace/src/standalone/index.js +0 -36
  200. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  201. package/packages/dd-trace/src/util.js +26 -0
  202. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  203. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const { VERSION } = require('../../../../../version')
4
+
5
+ const LANGUAGE = 'nodejs'
6
+
7
+ /**
8
+ * Returns the canonical client-library identification headers.
9
+ *
10
+ * @param {string} [language] - Client library language name.
11
+ * @param {string} [version] - Client library version.
12
+ * @returns {Record<string, string>} Client library identification headers.
13
+ */
14
+ function getClientLibraryHeaders (language = LANGUAGE, version = VERSION) {
15
+ return {
16
+ 'DD-Client-Library-Language': language,
17
+ 'DD-Client-Library-Version': version,
18
+ }
19
+ }
20
+
21
+ module.exports = { getClientLibraryHeaders }
@@ -6,12 +6,11 @@
6
6
  const { Readable } = require('stream')
7
7
  const http = require('http')
8
8
  const https = require('https')
9
- const net = require('net')
10
9
  const zlib = require('zlib')
11
10
 
12
11
  const { storage } = require('../../../../datadog-core')
13
12
  const log = require('../../log')
14
- const { parseUrl } = require('./url')
13
+ const { isLoopbackHost, parseUrl } = require('./url')
15
14
  const docker = require('./docker')
16
15
  const { httpAgent, httpsAgent } = require('./agents')
17
16
  const {
@@ -27,21 +26,11 @@ const maxActiveBufferSize = 1024 * 1024 * 64
27
26
 
28
27
  let activeBufferSize = 0
29
28
 
30
- /**
31
- * @param {string} hostname Host as resolved by {@link parseUrl}; IPv6 is unbracketed (`::1`).
32
- */
33
- function isLoopbackHost (hostname) {
34
- // The 127.0.0.0/8 block is loopback, but only when the host is an actual IPv4 literal: a
35
- // hostname like `127.evil.com` shares the prefix yet resolves anywhere, so net.isIPv4 gates it.
36
- return hostname === 'localhost' ||
37
- hostname === '::1' ||
38
- (hostname.startsWith('127.') && net.isIPv4(hostname))
39
- }
40
-
41
29
  /**
42
30
  * @param {Buffer|string|Readable|Array<Buffer|string>} data
43
31
  * @param {object} options
44
- * @param {(error: Error|null, result: string, statusCode: number) => void} callback
32
+ * @param {(error: Error|null, result?: string|null, statusCode?: number,
33
+ * headers?: import('node:http').IncomingHttpHeaders) => void} callback
45
34
  */
46
35
  function request (data, options, callback) {
47
36
  if (!options.headers) {
@@ -106,34 +95,50 @@ function request (data, options, callback) {
106
95
 
107
96
  options.agent = isSecure ? httpsAgent : httpAgent
108
97
 
109
- const onResponse = (res, finalize) => {
98
+ /**
99
+ * @param {import('node:http').IncomingMessage} res
100
+ * @param {(error: Error|null, result?: string|null, statusCode?: number,
101
+ * headers?: import('node:http').IncomingHttpHeaders) => void} complete
102
+ * @param {(error: Error) => void} handleError
103
+ */
104
+ const onResponse = (res, complete, handleError) => {
110
105
  markEndpointReached(options)
111
106
 
112
107
  const chunks = []
113
108
 
114
109
  res.setTimeout(timeout)
115
110
 
111
+ res.once('aborted', () => {
112
+ handleError(Object.assign(new Error('Response aborted'), { code: 'ECONNRESET' }))
113
+ })
114
+ res.once('error', handleError)
115
+ res.once('timeout', () => {
116
+ const error = Object.assign(new Error('Response timed out'), { code: 'ETIMEDOUT' })
117
+ res.destroy(error)
118
+ handleError(error)
119
+ })
120
+
116
121
  res.on('data', chunk => {
117
122
  chunks.push(chunk)
118
123
  })
119
124
 
120
125
  res.once('end', () => {
121
- finalize()
122
126
  const buffer = Buffer.concat(chunks)
123
127
 
124
128
  if (res.statusCode >= 200 && res.statusCode <= 299) {
125
- const isGzip = res.headers['content-encoding'] === 'gzip'
129
+ const contentEncoding = res.headers['content-encoding']
130
+ const isGzip = typeof contentEncoding === 'string' && contentEncoding.toLowerCase() === 'gzip'
126
131
  if (isGzip) {
127
132
  zlib.gunzip(buffer, (err, result) => {
128
133
  if (err) {
129
134
  log.error('Could not gunzip response: %s', err.message)
130
- callback(null, '', res.statusCode, res.headers)
135
+ complete(null, '', res.statusCode, res.headers)
131
136
  } else {
132
- callback(null, result.toString(), res.statusCode, res.headers)
137
+ complete(null, result.toString(), res.statusCode, res.headers)
133
138
  }
134
139
  })
135
140
  } else {
136
- callback(null, buffer.toString(), res.statusCode, res.headers)
141
+ complete(null, buffer.toString(), res.statusCode, res.headers)
137
142
  }
138
143
  } else {
139
144
  let errorMessage = ''
@@ -154,7 +159,7 @@ function request (data, options, callback) {
154
159
  const error = new log.NoTransmitError(errorMessage)
155
160
  error.status = res.statusCode
156
161
 
157
- callback(error, null, res.statusCode, res.headers)
162
+ complete(error, null, res.statusCode, res.headers)
158
163
  }
159
164
  })
160
165
  }
@@ -162,6 +167,7 @@ function request (data, options, callback) {
162
167
  // Retries always run via setTimeout so the AsyncLocalStorage store survives
163
168
  // the gap before socket.connect(); ALS.run() does not call ALS.enterWith()
164
169
  // outside AsyncContextFrame, so a synchronous re-entry would lose the store.
170
+ /** @param {number} attemptIndex */
165
171
  const attempt = attemptIndex => {
166
172
  if (!request.writable) {
167
173
  log.debug('Maximum number of active requests reached: payload is discarded.')
@@ -172,28 +178,51 @@ function request (data, options, callback) {
172
178
 
173
179
  legacyStorage.run({ noop: true }, () => {
174
180
  let finished = false
181
+ let settled = false
175
182
  const finalize = () => {
176
183
  if (finished) return
177
184
  finished = true
178
185
  activeBufferSize -= options.headers['Content-Length'] ?? 0
179
186
  }
180
187
 
181
- const req = client.request(options, (res) => onResponse(res, finalize))
182
-
183
- req.once('close', finalize)
184
- req.once('timeout', finalize)
185
-
186
- req.once('error', error => {
188
+ /**
189
+ * @param {Error | null} error
190
+ * @param {string | null} [result]
191
+ * @param {number} [statusCode]
192
+ * @param {import('node:http').IncomingHttpHeaders} [headers]
193
+ */
194
+ const complete = (error, result, statusCode, headers) => {
195
+ if (settled) return
196
+ settled = true
187
197
  finalize()
188
- if (attemptIndex < getMaxAttempts(options) && isRetriableNetworkError(error)) {
198
+ callback(error, result, statusCode, headers)
199
+ }
200
+
201
+ /**
202
+ * @param {Error} error
203
+ */
204
+ const handleError = (error) => {
205
+ if (settled) return
206
+
207
+ if (options.retry !== false &&
208
+ attemptIndex < getMaxAttempts(options) &&
209
+ isRetriableNetworkError(error)) {
210
+ settled = true
211
+ finalize()
189
212
  // Unref so a pending retry never keeps the host process alive past
190
213
  // its natural exit point; long-running apps still retry because the
191
214
  // event loop is held open by their own work.
192
215
  setTimeout(attempt, getRetryDelay(options, attemptIndex), attemptIndex + 1).unref?.()
193
216
  } else {
194
- callback(error)
217
+ complete(error)
195
218
  }
196
- })
219
+ }
220
+
221
+ const req = client.request(options, (res) => onResponse(res, complete, handleError))
222
+
223
+ req.once('close', finalize)
224
+ req.once('timeout', finalize)
225
+ req.once('error', handleError)
197
226
 
198
227
  req.setTimeout(timeout, () => {
199
228
  try {
@@ -1,7 +1,21 @@
1
1
  'use strict'
2
2
 
3
+ const net = require('node:net')
4
+
3
5
  const { urlToHttpOptions } = require('./url-to-http-options-polyfill')
4
6
 
7
+ /**
8
+ * @param {string} hostname
9
+ * @returns {boolean}
10
+ */
11
+ function isLoopbackHost (hostname) {
12
+ // Gate the 127/8 prefix on an IPv4 literal so names such as 127.example.com cannot pass.
13
+ return hostname === 'localhost' ||
14
+ hostname === '::1' ||
15
+ hostname === '[::1]' ||
16
+ (hostname.startsWith('127.') && net.isIPv4(hostname))
17
+ }
18
+
5
19
  /**
6
20
  * Convert an agent/intake URL into Node http(s) request options.
7
21
  *
@@ -31,4 +45,4 @@ function parseUrl (urlObjOrString) {
31
45
  return url
32
46
  }
33
47
 
34
- module.exports = { parseUrl }
48
+ module.exports = { isLoopbackHost, parseUrl }
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @typedef {{ enable: (config: import('./config/config-base')) => void, disable: () => void }} FeatureModule
5
+ * @typedef {new (tracer: import('./tracer'), config: import('./config/config-base')) => object} FeatureProvider
6
+ */
7
+
8
+ /**
9
+ * @typedef {object} Feature
10
+ * @property {string} name
11
+ * @property {object} noop
12
+ * @property {() => FeatureModule} factory
13
+ * @property {(config: import('./config/config-base')) => boolean} isEnabled
14
+ * @property {() => FeatureProvider} provider
15
+ * @property {(rc: import('./remote_config'), config: import('./config/config-base'),
16
+ * proxy: import('./proxy')) => void} [remoteConfig]
17
+ */
18
+
19
+ /** @type {{ [name: string]: Feature }} */
20
+ const features = {}
21
+
22
+ /**
23
+ * @param {Feature} feature
24
+ */
25
+ function registerFeature (feature) {
26
+ features[feature.name] = feature
27
+ }
28
+
29
+ module.exports = { features, registerFeature }
@@ -4,6 +4,7 @@ module.exports = {
4
4
  SPAN_KINDS: ['llm', 'agent', 'workflow', 'task', 'tool', 'embedding', 'retrieval'],
5
5
  SPAN_KIND: '_ml_obs.meta.span.kind',
6
6
  SESSION_ID: '_ml_obs.session_id',
7
+ SESSION_ID_TRACE_DEFAULT_KEY: '_ml_obs.trace_session_id',
7
8
  DECORATOR: '_ml_obs.decorator',
8
9
  INTEGRATION: '_ml_obs.integration',
9
10
  METADATA: '_ml_obs.meta.metadata',
@@ -0,0 +1,113 @@
1
+ 'use strict'
2
+
3
+ // Control-plane HTTP client for LLM Obs Experiments. Uses the global `fetch`,
4
+ // so this module adds no new dependency; credentials and site come from config.
5
+
6
+ const API_BASE_PATH = '/api/v2/llm-obs/v1'
7
+
8
+ // Control-plane host for a Datadog site, e.g.
9
+ // datadoghq.com -> api.datadoghq.com
10
+ // us3.datadoghq.com -> api.us3.datadoghq.com
11
+ // datad0g.com (staging)-> api.datad0g.com
12
+ function apiHost (site) {
13
+ return `api.${site}`
14
+ }
15
+
16
+ // Web-app host for dashboard URLs. Single-level sites (datadoghq.com,
17
+ // ddog-gov.com) are served from the `app.` subdomain; regional sites
18
+ // (us3.datadoghq.com, ap1.datadoghq.com) are used as-is.
19
+ function appHost (site) {
20
+ return site.split('.').length === 2 ? `app.${site}` : site
21
+ }
22
+
23
+ class ExperimentsClient {
24
+ #apiKey
25
+ #appKey
26
+ #site
27
+ #projectName
28
+ #timeout
29
+ #cachedProjectId
30
+
31
+ constructor ({ apiKey, appKey, site, projectName, timeout = 30_000 } = {}) {
32
+ this.#apiKey = apiKey
33
+ this.#appKey = appKey
34
+ this.#site = site
35
+ this.#projectName = projectName
36
+ this.#timeout = timeout
37
+ this.#cachedProjectId = null
38
+ }
39
+
40
+ // Whether the client has everything it needs to talk to the control plane.
41
+ get configured () {
42
+ return Boolean(this.#apiKey && this.#appKey && this.#site)
43
+ }
44
+
45
+ get site () {
46
+ return this.#site
47
+ }
48
+
49
+ // Dashboard URL base for the configured site, e.g. https://app.datadoghq.com
50
+ get appBase () {
51
+ return `https://${appHost(this.#site)}`
52
+ }
53
+
54
+ // Resolve the configured project's id (get-or-create), cached.
55
+ ensureProjectId () {
56
+ return this.getOrCreateProject(this.#projectName)
57
+ }
58
+
59
+ // Low-level request. Builds https://api.<site><path>, attaches both keys, and
60
+ // returns the parsed JSON body. Throws with status + body on a non-2xx.
61
+ async request (method, path, body) {
62
+ const url = `https://${apiHost(this.#site)}${path}`
63
+ const headers = {
64
+ 'DD-API-KEY': this.#apiKey,
65
+ 'DD-APPLICATION-KEY': this.#appKey,
66
+ }
67
+
68
+ let payload
69
+ if (body !== undefined) {
70
+ payload = JSON.stringify(body)
71
+ headers['Content-Type'] = 'application/json'
72
+ }
73
+
74
+ let response
75
+ try {
76
+ response = await fetch(url, {
77
+ method,
78
+ headers,
79
+ body: payload,
80
+ signal: AbortSignal.timeout(this.#timeout),
81
+ })
82
+ } catch (err) {
83
+ throw new Error(`${method} ${path} failed: ${err.message}`)
84
+ }
85
+
86
+ const text = await response.text()
87
+ if (!response.ok) {
88
+ throw new Error(`${method} ${path} failed: HTTP ${response.status} ${text}`)
89
+ }
90
+ return text ? JSON.parse(text) : {}
91
+ }
92
+
93
+ // Resolve the project id for `name`, creating it if absent. The create
94
+ // endpoint is get-or-create on name, so repeated calls return the same id.
95
+ // Cached after the first resolution.
96
+ async getOrCreateProject (name) {
97
+ if (this.#cachedProjectId) return this.#cachedProjectId
98
+
99
+ let response
100
+ try {
101
+ response = await this.request('POST', `${API_BASE_PATH}/projects`, {
102
+ data: { type: 'projects', attributes: { name } },
103
+ })
104
+ } catch (err) {
105
+ throw new Error(`Failed to create or get project '${name}': ${err.message}`)
106
+ }
107
+
108
+ this.#cachedProjectId = response?.data?.id ?? null
109
+ return this.#cachedProjectId
110
+ }
111
+ }
112
+
113
+ module.exports = { ExperimentsClient, apiHost, appHost, API_BASE_PATH }
@@ -0,0 +1,154 @@
1
+ 'use strict'
2
+
3
+ const { API_BASE_PATH } = require('./client')
4
+
5
+ // Immutable dataset record: { input, expectedOutput?, metadata? }.
6
+ class DatasetRecord {
7
+ constructor (input, expectedOutput = null, metadata = {}) {
8
+ this.input = input
9
+ this.expectedOutput = expectedOutput ?? null
10
+ this.metadata = metadata ?? {}
11
+ }
12
+ }
13
+
14
+ // A local buffer of dataset records, created remotely and pushed on first run
15
+ // (or eagerly via push()). Pushes are incremental.
16
+ class Dataset {
17
+ #client
18
+ #name
19
+ #description
20
+ #records
21
+ #recordIds
22
+ #id
23
+ #projectId
24
+ #pushedCount
25
+
26
+ constructor (client, name, description = '') {
27
+ this.#client = client
28
+ this.#name = name
29
+ this.#description = description
30
+ this.#records = []
31
+ this.#recordIds = []
32
+ this.#id = null
33
+ this.#projectId = null
34
+ this.#pushedCount = 0
35
+ }
36
+
37
+ // Build a Dataset that already exists remotely (used by pullDataset).
38
+ static fromExisting (client, name, description, id, projectId, records, recordIds) {
39
+ const dataset = new Dataset(client, name, description)
40
+ dataset.#id = id
41
+ dataset.#projectId = projectId
42
+ dataset.#records.push(...records)
43
+ dataset.#recordIds.push(...recordIds)
44
+ dataset.#pushedCount = records.length
45
+ return dataset
46
+ }
47
+
48
+ // Append a record. Accepts a DatasetRecord or (input, expectedOutput?, metadata?).
49
+ addRecord (recordOrInput, expectedOutput, metadata) {
50
+ const record = recordOrInput instanceof DatasetRecord
51
+ ? recordOrInput
52
+ : new DatasetRecord(recordOrInput, expectedOutput, metadata)
53
+ this.#records.push(record)
54
+ return this
55
+ }
56
+
57
+ name () {
58
+ return this.#name
59
+ }
60
+
61
+ records () {
62
+ return [...this.#records]
63
+ }
64
+
65
+ recordIds () {
66
+ return [...this.#recordIds]
67
+ }
68
+
69
+ id () {
70
+ return this.#id
71
+ }
72
+
73
+ projectId () {
74
+ return this.#projectId
75
+ }
76
+
77
+ // Dashboard URL for this dataset, or null until pushed/pulled.
78
+ url () {
79
+ if (this.#id === null) return null
80
+ return `${this.#client.appBase}/llm/datasets/${this.#id}`
81
+ }
82
+
83
+ // Eagerly create the dataset (if needed) and push any unpushed records.
84
+ async push () {
85
+ const projectId = await this.#client.ensureProjectId()
86
+ return this.ensureCreatedAndPushed(projectId)
87
+ }
88
+
89
+ // Create the remote dataset if needed, then push records added since the last
90
+ // push. Idempotent and incremental. Resolves to { pushedCount, totalCount } for
91
+ // the records attempted in this call, so callers can confirm the push landed.
92
+ async ensureCreatedAndPushed (projectId) {
93
+ if (this.#id === null) {
94
+ let response
95
+ try {
96
+ response = await this.#client.request('POST', `${API_BASE_PATH}/${projectId}/datasets`, {
97
+ data: { type: 'datasets', attributes: { name: this.#name, description: this.#description } },
98
+ })
99
+ } catch (err) {
100
+ throw new Error(`Failed to create dataset '${this.#name}': ${err.message}`)
101
+ }
102
+ this.#id = response?.data?.id ?? null
103
+ this.#projectId = projectId
104
+ }
105
+
106
+ if (this.#pushedCount >= this.#records.length) return { pushedCount: 0, totalCount: 0 }
107
+
108
+ const pending = this.#records.slice(this.#pushedCount)
109
+ const records = pending.map((rec) => {
110
+ const out = { input: rec.input }
111
+ if (rec.expectedOutput !== null && rec.expectedOutput !== undefined) {
112
+ out.expected_output = rec.expectedOutput
113
+ }
114
+ if (rec.metadata && Object.keys(rec.metadata).length > 0) {
115
+ out.metadata = rec.metadata
116
+ }
117
+ return out
118
+ })
119
+
120
+ let response
121
+ try {
122
+ response = await this.#client.request(
123
+ 'POST',
124
+ `${API_BASE_PATH}/${projectId}/datasets/${this.#id}/records`,
125
+ { data: { type: 'datasets', attributes: { records } } }
126
+ )
127
+ } catch (err) {
128
+ throw new Error(`Failed to push records to dataset '${this.#name}': ${err.message}`)
129
+ }
130
+
131
+ // The append-records response returns created records under a top-level
132
+ // `records` field, not the usual `data` envelope.
133
+ const created = response?.records
134
+ let pushedCount = 0
135
+ if (Array.isArray(created)) {
136
+ for (const node of created) {
137
+ const recordId = String(node?.id ?? '')
138
+ if (recordId !== '') pushedCount++
139
+ this.#recordIds.push(recordId)
140
+ }
141
+ for (let i = created.length; i < pending.length; i++) this.#recordIds.push('')
142
+ } else {
143
+ for (let i = 0; i < pending.length; i++) this.#recordIds.push('')
144
+ }
145
+
146
+ // Advance by the snapshotted pending count, not the live records length,
147
+ // so records added while this push was in flight aren't skipped by the next push.
148
+ this.#pushedCount += pending.length
149
+
150
+ return { pushedCount, totalCount: pending.length }
151
+ }
152
+ }
153
+
154
+ module.exports = { Dataset, DatasetRecord }