dd-trace 5.106.0 → 5.108.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 (105) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +9 -11
  3. package/packages/datadog-core/src/storage.js +47 -48
  4. package/packages/datadog-esbuild/index.js +6 -1
  5. package/packages/datadog-instrumentations/src/ai.js +12 -3
  6. package/packages/datadog-instrumentations/src/body-parser.js +5 -2
  7. package/packages/datadog-instrumentations/src/connect.js +3 -2
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
  9. package/packages/datadog-instrumentations/src/cucumber.js +7 -0
  10. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
  11. package/packages/datadog-instrumentations/src/express-session.js +12 -11
  12. package/packages/datadog-instrumentations/src/express.js +24 -20
  13. package/packages/datadog-instrumentations/src/fastify.js +18 -6
  14. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
  15. package/packages/datadog-instrumentations/src/http/client.js +9 -12
  16. package/packages/datadog-instrumentations/src/http/server.js +30 -16
  17. package/packages/datadog-instrumentations/src/http2/client.js +15 -12
  18. package/packages/datadog-instrumentations/src/http2/server.js +15 -8
  19. package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
  20. package/packages/datadog-instrumentations/src/jest.js +143 -73
  21. package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
  23. package/packages/datadog-instrumentations/src/multer.js +3 -2
  24. package/packages/datadog-instrumentations/src/mysql2.js +34 -0
  25. package/packages/datadog-instrumentations/src/net.js +8 -6
  26. package/packages/datadog-instrumentations/src/openai.js +19 -7
  27. package/packages/datadog-instrumentations/src/pg.js +19 -0
  28. package/packages/datadog-instrumentations/src/router.js +12 -10
  29. package/packages/datadog-instrumentations/src/vitest.js +29 -4
  30. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
  31. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
  32. package/packages/datadog-plugin-cucumber/src/index.js +2 -0
  33. package/packages/datadog-plugin-cypress/src/support.js +31 -1
  34. package/packages/datadog-plugin-http/src/client.js +0 -3
  35. package/packages/datadog-plugin-http/src/server.js +11 -1
  36. package/packages/datadog-plugin-mocha/src/index.js +2 -0
  37. package/packages/datadog-plugin-pg/src/index.js +10 -0
  38. package/packages/dd-trace/src/aiguard/index.js +34 -15
  39. package/packages/dd-trace/src/aiguard/sdk.js +34 -3
  40. package/packages/dd-trace/src/aiguard/tags.js +6 -0
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +3 -2
  42. package/packages/dd-trace/src/appsec/iast/index.js +3 -2
  43. package/packages/dd-trace/src/appsec/rasp/ssrf.js +2 -1
  44. package/packages/dd-trace/src/appsec/reporter.js +1 -1
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
  46. package/packages/dd-trace/src/config/defaults.js +14 -0
  47. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -1
  48. package/packages/dd-trace/src/config/helper.js +1 -0
  49. package/packages/dd-trace/src/config/index.js +5 -9
  50. package/packages/dd-trace/src/config/parsers.js +8 -0
  51. package/packages/dd-trace/src/config/supported-configurations.json +20 -6
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  53. package/packages/dd-trace/src/datastreams/writer.js +1 -2
  54. package/packages/dd-trace/src/debugger/config.js +1 -1
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
  56. package/packages/dd-trace/src/debugger/index.js +1 -2
  57. package/packages/dd-trace/src/dogstatsd.js +2 -3
  58. package/packages/dd-trace/src/encode/0.4.js +49 -41
  59. package/packages/dd-trace/src/encode/agentless-json.js +5 -1
  60. package/packages/dd-trace/src/encode/tags-processors.js +14 -0
  61. package/packages/dd-trace/src/exporters/agent/index.js +1 -2
  62. package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
  63. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
  64. package/packages/dd-trace/src/exporters/common/request.js +26 -0
  65. package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
  66. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
  67. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
  68. package/packages/dd-trace/src/llmobs/sdk.js +4 -1
  69. package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
  70. package/packages/dd-trace/src/llmobs/tagger.js +5 -3
  71. package/packages/dd-trace/src/llmobs/util.js +54 -0
  72. package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
  73. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  74. package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
  75. package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
  76. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
  77. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
  78. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
  79. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +3 -2
  80. package/packages/dd-trace/src/opentracing/span.js +23 -18
  81. package/packages/dd-trace/src/opentracing/tracer.js +16 -12
  82. package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
  83. package/packages/dd-trace/src/priority_sampler.js +6 -5
  84. package/packages/dd-trace/src/profiling/config.js +3 -2
  85. package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
  86. package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
  87. package/packages/dd-trace/src/proxy.js +13 -10
  88. package/packages/dd-trace/src/remote_config/index.js +1 -2
  89. package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
  90. package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
  91. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
  92. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
  93. package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
  94. package/packages/dd-trace/src/span_format.js +33 -25
  95. package/packages/dd-trace/src/span_stats.js +1 -1
  96. package/packages/dd-trace/src/startup-log.js +1 -2
  97. package/packages/dd-trace/src/telemetry/send-data.js +1 -1
  98. package/packages/dd-trace/src/tracer.js +1 -1
  99. package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
  100. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  101. package/vendor/dist/protobufjs/index.js +1 -1
  102. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  103. package/vendor/dist/shell-quote/index.js +1 -1
  104. package/packages/dd-trace/src/agent/url.js +0 -28
  105. package/scripts/preinstall.js +0 -34
@@ -3,7 +3,7 @@
3
3
  const getConfig = require('../config')
4
4
  const { MsgpackChunk } = require('../msgpack')
5
5
  const log = require('../log')
6
- const { normalizeSpan } = require('./tags-processors')
6
+ const { normalizeSpan, eventTimeNano } = require('./tags-processors')
7
7
 
8
8
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
9
9
  // Values longer than this byte threshold skip the `_stringMap` lookup and
@@ -114,8 +114,10 @@ const ATTR_PAYLOAD_BOOL_FALSE = Buffer.concat([ATTR_PREFIX_BOOL, Buffer.from([0x
114
114
  function formatSpanWithLegacyEvents (span) {
115
115
  span = normalizeSpan(span)
116
116
  if (span.span_events) {
117
- // TODO: this is currently a main cost driver. By unifying it with the formatter
118
- // it should be possible to improve performance significantly overall.
117
+ // Reads the raw `_events` array directly (no formatter pre-reshape) and
118
+ // serializes to the legacy meta.events JSON string. The serialization is
119
+ // still the main cost on the legacy path; the native span_events slot
120
+ // (`#encodeSpanEvents`) avoids it entirely.
119
121
  span.meta.events = stringifySpanEvents(span.span_events)
120
122
  // `= undefined` over `delete` to keep the span's hidden class — `delete`
121
123
  // would push every event-bearing span into V8 dictionary mode.
@@ -125,14 +127,15 @@ function formatSpanWithLegacyEvents (span) {
125
127
  }
126
128
 
127
129
  /**
128
- * Hand-written stringifier for `span.span_events`. The shape is fixed by
129
- * `extractSpanEvents` (`{ name, time_unix_nano, attributes? }`) and attribute
130
- * values are pre-sanitized to primitives or arrays of primitives, so we can
131
- * skip everything `JSON.stringify` does for the generic case (toJSON probing,
132
- * key iteration over the prototype chain, replacer hooks). Output matches
133
- * `JSON.stringify(spanEvents)` byte-for-byte for the post-sanitization shape.
130
+ * Hand-written stringifier for `span.span_events`. Events arrive in their raw
131
+ * `{ name, startTime, attributes? }` shape; `time_unix_nano` is derived per
132
+ * event via `eventTimeNano` and empty attribute objects are dropped, matching
133
+ * what the formatter used to precompute. Attribute values are pre-sanitized to
134
+ * primitives or arrays of primitives, so we skip everything `JSON.stringify`
135
+ * does for the generic case (toJSON probing, prototype-chain key iteration,
136
+ * replacer hooks).
134
137
  *
135
- * @param {Array<{ name: string, time_unix_nano: number, attributes?: object }>} spanEvents
138
+ * @param {Array<{ name: unknown, startTime: number, attributes?: object }>} spanEvents
136
139
  * @returns {string}
137
140
  */
138
141
  function stringifySpanEvents (spanEvents) {
@@ -140,17 +143,21 @@ function stringifySpanEvents (spanEvents) {
140
143
  for (let index = 0; index < spanEvents.length; index++) {
141
144
  if (index > 0) result += ','
142
145
  const event = spanEvents[index]
146
+ // `_sanitizeEventAttributes` leaves `attributes` undefined when empty, so a
147
+ // present value always has entries — no emptiness probe here.
148
+ const attributes = event.attributes
143
149
  // `addEvent` does not type-check `name`; defer the unusual cases to
144
- // `JSON.stringify` so non-string names match the prior behaviour
145
- // instead of throwing in `escapeJsonString`.
150
+ // `JSON.stringify` so non-string names match the prior behaviour instead
151
+ // of throwing in `escapeJsonString`. Build the wire-shaped object so the
152
+ // emitted key stays `time_unix_nano`, not the raw `startTime`.
146
153
  if (typeof event.name !== 'string') {
147
- result += JSON.stringify(event)
154
+ result += JSON.stringify({ name: event.name, time_unix_nano: eventTimeNano(event), attributes })
148
155
  continue
149
156
  }
150
157
  result += '{"name":' + escapeJsonString(event.name) +
151
- ',"time_unix_nano":' + jsonNumber(event.time_unix_nano)
152
- if (event.attributes) {
153
- result += ',"attributes":' + stringifyAttributes(event.attributes)
158
+ ',"time_unix_nano":' + jsonNumber(eventTimeNano(event))
159
+ if (attributes) {
160
+ result += ',"attributes":' + stringifyAttributes(attributes)
154
161
  }
155
162
  result += '}'
156
163
  }
@@ -241,8 +248,8 @@ class AgentEncoder {
241
248
  this.#config = getConfig()
242
249
  this.#debugEncoding = this.#config.DD_TRACE_ENCODING_DEBUG
243
250
  // Pick the per-span formatter once so the hot loop pays no per-span
244
- // config check. The native path doesn't need to reshape `span_events`
245
- // because `#encodeSpanEvents` works directly on the raw attributes.
251
+ // config check. The native path keeps the raw `span_events` slot for
252
+ // `#encodeSpanEvents`; the legacy path serializes it into meta.events.
246
253
  this.#formatSpan = this.#config.DD_TRACE_NATIVE_SPAN_EVENTS
247
254
  ? normalizeSpan
248
255
  : formatSpanWithLegacyEvents
@@ -320,16 +327,18 @@ class AgentEncoder {
320
327
  const resourceLen = resourceEntry.length
321
328
  const serviceLen = serviceEntry.length
322
329
 
323
- // Almost every span carries `error: 0` or `error: 1` AND a nanosecond
324
- // `start` timestamp ≥ 2³² (so `start` always encodes as a u64). When
325
- // both hold, the block fuses error key+value, the start key + 0xCF
326
- // type byte + 8-byte timestamp, and the duration key into the per-span
327
- // reserve. The fallback path covers synthetic/test inputs with small
328
- // starts and rare non-binary error flags by keeping per-field emits so
329
- // each integer picks the shortest msgpack encoding.
330
- const errorIsFixint = span.error === 0 || span.error === 1
331
- const startFitsU64 = span.start >= 0x1_00_00_00_00
332
- const fuseTail = errorIsFixint && startFitsU64
330
+ // `error` is `0` or `1` on nearly every span, and `start` is a
331
+ // nanosecond timestamp ≥ 2³² (always a msgpack u64). Decide the fused
332
+ // error key+value up front (`KEY_ERROR_0` / `KEY_ERROR_1`, or `undefined`
333
+ // for the rare non-binary flag) so the tail fuses without re-deciding the
334
+ // error shape twice. The fused tail also needs `start` as a u64; when
335
+ // either misses (synthetic small `start`, non-binary error) the tail
336
+ // routes each integer through `writeIntOrFloat` for the shortest
337
+ // encoding.
338
+ const errorEntry = span.error === 0
339
+ ? KEY_ERROR_0
340
+ : span.error === 1 ? KEY_ERROR_1 : undefined
341
+ const fuseTail = errorEntry !== undefined && span.start >= 0x1_00_00_00_00
333
342
 
334
343
  let blockSize = 1 +
335
344
  KEY_TRACE_ID_PREFIX.length + 8 +
@@ -340,7 +349,7 @@ class AgentEncoder {
340
349
  KEY_SERVICE.length + serviceLen
341
350
  if (typeEntry) blockSize += KEY_TYPE.length + typeEntry.length
342
351
  if (fuseTail) {
343
- blockSize += KEY_ERROR_0.length + KEY_START_PREFIX.length + 8 + KEY_DURATION.length
352
+ blockSize += errorEntry.length + KEY_START_PREFIX.length + 8 + KEY_DURATION.length
344
353
  }
345
354
 
346
355
  const blockOffset = bytes.length
@@ -377,8 +386,8 @@ class AgentEncoder {
377
386
  cursor += serviceLen
378
387
 
379
388
  if (fuseTail) {
380
- target.set(span.error === 0 ? KEY_ERROR_0 : KEY_ERROR_1, cursor)
381
- cursor += KEY_ERROR_0.length
389
+ target.set(errorEntry, cursor)
390
+ cursor += errorEntry.length
382
391
 
383
392
  target.set(KEY_START_PREFIX, cursor)
384
393
  cursor += KEY_START_PREFIX.length
@@ -389,15 +398,14 @@ class AgentEncoder {
389
398
  cursor += 8
390
399
 
391
400
  target.set(KEY_DURATION, cursor)
401
+ } else if (errorEntry) {
402
+ bytes.set(errorEntry)
403
+ bytes.set(KEY_START)
404
+ bytes.writeIntOrFloat(span.start)
405
+ bytes.set(KEY_DURATION)
392
406
  } else {
393
- if (span.error === 0) {
394
- bytes.set(KEY_ERROR_0)
395
- } else if (span.error === 1) {
396
- bytes.set(KEY_ERROR_1)
397
- } else {
398
- bytes.set(KEY_ERROR)
399
- bytes.writeIntOrFloat(span.error)
400
- }
407
+ bytes.set(KEY_ERROR)
408
+ bytes.writeIntOrFloat(span.error)
401
409
  bytes.set(KEY_START)
402
410
  bytes.writeIntOrFloat(span.start)
403
411
  bytes.set(KEY_DURATION)
@@ -763,7 +771,7 @@ class AgentEncoder {
763
771
  * values — no `formatSpanEvents` pre-pass and no recursive generic walk.
764
772
  *
765
773
  * @param {MsgpackChunk} bytes
766
- * @param {Array<{ name: string, time_unix_nano: number, attributes?: object }>} spanEvents
774
+ * @param {Array<{ name: unknown, startTime: number, attributes?: object }>} spanEvents
767
775
  */
768
776
  #encodeSpanEvents (bytes, spanEvents) {
769
777
  const offset = bytes.length
@@ -784,7 +792,7 @@ class AgentEncoder {
784
792
  bytes.set(KEY_NAME)
785
793
  this._encodeString(bytes, event.name)
786
794
  bytes.set(KEY_EVENT_TIME)
787
- bytes.writeFloat(event.time_unix_nano)
795
+ bytes.writeFloat(eventTimeNano(event))
788
796
 
789
797
  const attributes = event.attributes
790
798
  if (attributes !== null && typeof attributes === 'object') {
@@ -3,6 +3,7 @@
3
3
  const log = require('../log')
4
4
  const { TOP_LEVEL_KEY } = require('../constants')
5
5
  const { normalizeSpan } = require('./tags-processors')
6
+ const { stringifySpanEvents } = require('./0.4')
6
7
 
7
8
  // Soft limit for estimated payload size. Triggers an early flush to stay under intake request size limits.
8
9
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
@@ -20,7 +21,10 @@ function formatSpan (span, isFirstSpan) {
20
21
  delete span.meta['_dd.p.tid']
21
22
 
22
23
  if (span.span_events) {
23
- span.meta.events = JSON.stringify(span.span_events)
24
+ // Events arrive raw (`{ name, startTime, attributes? }`); stringifySpanEvents
25
+ // derives `time_unix_nano` and drops empty attributes, matching the JSON the
26
+ // reshaped array used to produce.
27
+ span.meta.events = stringifySpanEvents(span.span_events)
24
28
  delete span.span_events
25
29
  }
26
30
 
@@ -46,6 +46,19 @@ function truncateSpanTestOpt (span) {
46
46
  return span
47
47
  }
48
48
 
49
+ /**
50
+ * Convert a raw span event's `startTime` (milliseconds, sub-millisecond
51
+ * precision) to the wire `time_unix_nano`. Single source of truth for the
52
+ * formula so the four encoders that consume `span_events` stay in lockstep;
53
+ * the formatter no longer reshapes events, it hands the raw array through.
54
+ *
55
+ * @param {{ startTime: number }} event
56
+ * @returns {number}
57
+ */
58
+ function eventTimeNano (event) {
59
+ return Math.round(event.startTime * 1e6)
60
+ }
61
+
49
62
  function normalizeSpan (span) {
50
63
  span.service = span.service || DEFAULT_SERVICE_NAME
51
64
  if (span.service.length > MAX_SERVICE_LENGTH) {
@@ -69,6 +82,7 @@ module.exports = {
69
82
  truncateSpan,
70
83
  truncateSpanTestOpt,
71
84
  normalizeSpan,
85
+ eventTimeNano,
72
86
  MAX_META_KEY_LENGTH,
73
87
  MAX_META_VALUE_LENGTH,
74
88
  MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION,
@@ -2,7 +2,6 @@
2
2
 
3
3
  const { URL } = require('url')
4
4
  const log = require('../../log')
5
- const { getAgentUrl } = require('../../agent/url')
6
5
  const Writer = require('./writer')
7
6
 
8
7
  class AgentExporter {
@@ -11,7 +10,7 @@ class AgentExporter {
11
10
  constructor (config, prioritySampler) {
12
11
  this._config = config
13
12
  const { lookup, protocolVersion, stats = {}, apmTracingEnabled } = config
14
- this._url = getAgentUrl(config)
13
+ this._url = config.url
15
14
 
16
15
  const headers = {}
17
16
  if (stats.enabled || apmTracingEnabled === false) {
@@ -18,25 +18,21 @@ class AgentlessExporter {
18
18
 
19
19
  /**
20
20
  * @param {object} config - Configuration object
21
- * @param {string} [config.site] - The Datadog site. Defaults to 'datadoghq.com'.
22
- * @param {string} [config.url] - Override intake URL
21
+ * @param {string} [config.site] - The Datadog site. Defaults to 'datadoghq.com'.
23
22
  * @param {number} [config.flushInterval] - Batch flush interval in ms
24
23
  * @param {string} [config.env] - Environment name
25
24
  * @param {object} [config.tags] - Tags including runtime-id
26
25
  */
27
26
  constructor (config) {
28
27
  this._config = config
29
- const { site = 'datadoghq.com', url } = config
28
+ const site = config.site ?? 'datadoghq.com'
30
29
 
31
30
  try {
32
- this._url = url ? new URL(url) : new URL(`https://public-trace-http-intake.logs.${site}`)
31
+ // Agentless traffic carries the Datadog API key, so the intake is always the public https
32
+ // endpoint; never derive it from config.url (the agent's cleartext http) or the key leaks.
33
+ this._url = new URL(`https://public-trace-http-intake.logs.${site}`)
33
34
  } catch (err) {
34
- log.error(
35
- 'Invalid URL configuration for agentless exporter. url=%s, site=%s. Error: %s',
36
- url || 'not set',
37
- site,
38
- err.message
39
- )
35
+ log.error('Invalid site for agentless exporter. site=%s. Error: %s', site, err.message)
40
36
  this._url = null
41
37
  }
42
38
 
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { incrementCountMetric, TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION } = require('../../ci-visibility/telemetry')
4
- const { getAgentUrl } = require('../../agent/url')
5
4
 
6
5
  /**
7
6
  * Base exporter that buffers traces until a writer is initialized.
@@ -14,7 +13,7 @@ class BufferingExporter {
14
13
 
15
14
  constructor (tracerConfig) {
16
15
  this._config = tracerConfig
17
- this._url = getAgentUrl(tracerConfig)
16
+ this._url = tracerConfig.url
18
17
  }
19
18
 
20
19
  export (trace) {
@@ -6,6 +6,7 @@
6
6
  const { Readable } = require('stream')
7
7
  const http = require('http')
8
8
  const https = require('https')
9
+ const net = require('net')
9
10
  const zlib = require('zlib')
10
11
 
11
12
  const { storage } = require('../../../../datadog-core')
@@ -45,6 +46,17 @@ function parseUrl (urlObjOrString) {
45
46
  return url
46
47
  }
47
48
 
49
+ /**
50
+ * @param {string} hostname Host as resolved by {@link parseUrl}; IPv6 is unbracketed (`::1`).
51
+ */
52
+ function isLoopbackHost (hostname) {
53
+ // The 127.0.0.0/8 block is loopback, but only when the host is an actual IPv4 literal: a
54
+ // hostname like `127.evil.com` shares the prefix yet resolves anywhere, so net.isIPv4 gates it.
55
+ return hostname === 'localhost' ||
56
+ hostname === '::1' ||
57
+ (hostname.startsWith('127.') && net.isIPv4(hostname))
58
+ }
59
+
48
60
  /**
49
61
  * @param {Buffer|string|Readable|Array<Buffer|string>} data
50
62
  * @param {object} options
@@ -67,6 +79,20 @@ function request (data, options, callback) {
67
79
  }
68
80
  }
69
81
 
82
+ // Never put the Datadog API key on a cleartext connection to a non-loopback host; that would
83
+ // expose it on the wire. Loopback (local agent, dev proxy, tests) is exempt. Strip the key
84
+ // rather than drop the request: the agent proxies telemetry with its own key, while an https
85
+ // intake URL is required to authenticate agentless traffic.
86
+ const hasApiKey = options.headers['dd-api-key'] !== undefined || options.headers['DD-API-KEY'] !== undefined
87
+ if (hasApiKey && options.protocol === 'http:' && !isLoopbackHost(options.hostname)) {
88
+ log.error(
89
+ 'Not sending the Datadog API key over a non-TLS connection to %s. Configure an https intake URL.',
90
+ options.hostname
91
+ )
92
+ delete options.headers['dd-api-key']
93
+ delete options.headers['DD-API-KEY']
94
+ }
95
+
70
96
  if (data instanceof Readable) {
71
97
  const chunks = []
72
98
 
@@ -1,11 +1,10 @@
1
1
  'use strict'
2
2
 
3
- const { getAgentUrl } = require('../../agent/url')
4
3
  const { Writer } = require('./writer')
5
4
 
6
5
  class SpanStatsExporter {
7
6
  constructor (config) {
8
- this._url = getAgentUrl(config)
7
+ this._url = config.url
9
8
  this._writer = new Writer({ url: this._url })
10
9
  }
11
10
 
@@ -5,6 +5,7 @@ const {
5
5
  getOperation,
6
6
  extractMetrics,
7
7
  extractMetadata,
8
+ extractToolDefinitions,
8
9
  aggregateStreamingChunks,
9
10
  formatInputMessages,
10
11
  formatEmbeddingInput,
@@ -79,6 +80,9 @@ class GenAiLLMObsPlugin extends LLMObsPlugin {
79
80
  const metadata = extractMetadata(config)
80
81
  this._tagger.tagMetadata(span, metadata)
81
82
 
83
+ const toolDefinitions = extractToolDefinitions(config)
84
+ if (toolDefinitions.length > 0) this._tagger.tagToolDefinitions(span, toolDefinitions)
85
+
82
86
  if (error) {
83
87
  this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
84
88
  return
@@ -155,6 +155,50 @@ function extractMetadata (config) {
155
155
  return metadata
156
156
  }
157
157
 
158
+ /**
159
+ * Extract tool definitions from config
160
+ * @param {object} config
161
+ * @returns {Array}
162
+ */
163
+ function extractToolDefinitions (config) {
164
+ const toolDefinitions = []
165
+
166
+ if (!Array.isArray(config?.tools)) {
167
+ return toolDefinitions
168
+ }
169
+
170
+ for (const tool of config.tools) {
171
+ // Only extract tools with valid function declarations
172
+ if (!Array.isArray(tool?.functionDeclarations)) {
173
+ continue
174
+ }
175
+
176
+ for (const currDeclaration of tool.functionDeclarations) {
177
+ // A valid declaration must have a name
178
+ if (!currDeclaration?.name) {
179
+ continue
180
+ }
181
+
182
+ const toolDef = { name: currDeclaration.name }
183
+
184
+ if (currDeclaration.description !== undefined) {
185
+ toolDef.description = currDeclaration.description
186
+ }
187
+
188
+ // Parameters can be in two different fields depending on user input
189
+ if (currDeclaration.parameters !== undefined) {
190
+ toolDef.schema = currDeclaration.parameters
191
+ } else if (currDeclaration.parametersJsonSchema !== undefined) {
192
+ toolDef.schema = currDeclaration.parametersJsonSchema
193
+ }
194
+
195
+ toolDefinitions.push(toolDef)
196
+ }
197
+ }
198
+
199
+ return toolDefinitions
200
+ }
201
+
158
202
  /**
159
203
  * Format function call message
160
204
  * @param {Array} parts
@@ -498,6 +542,7 @@ module.exports = {
498
542
  getOperation,
499
543
  extractMetrics,
500
544
  extractMetadata,
545
+ extractToolDefinitions,
501
546
  aggregateStreamingChunks,
502
547
  formatInputMessages,
503
548
  formatEmbeddingInput,
@@ -259,7 +259,7 @@ class LLMObs extends NoopLLMObs {
259
259
  throw new Error('LLMObs span must have a span kind specified')
260
260
  }
261
261
 
262
- const { inputData, outputData, metadata, metrics, tags, prompt, costTags } = options
262
+ const { inputData, outputData, metadata, metrics, tags, prompt, costTags, toolDefinitions } = options
263
263
 
264
264
  if (inputData || outputData) {
265
265
  if (spanKind === 'llm') {
@@ -289,6 +289,9 @@ class LLMObs extends NoopLLMObs {
289
289
  if (prompt) {
290
290
  this._tagger.tagPrompt(span, prompt)
291
291
  }
292
+ if (toolDefinitions != null) {
293
+ this._tagger.tagToolDefinitions(span, toolDefinitions)
294
+ }
292
295
  } catch (e) {
293
296
  if (e.ddErrorTag) {
294
297
  err = e.ddErrorTag
@@ -332,8 +332,24 @@ class LLMObsSpanProcessor {
332
332
  return tags
333
333
  }
334
334
 
335
+ /**
336
+ * @param {Record<string, unknown>} tags
337
+ */
335
338
  #objectTagsToStringArrayTags (tags) {
336
- return Object.entries(tags).map(([key, value]) => `${key}:${value ?? ''}`)
339
+ const out = []
340
+ for (const [key, value] of Object.entries(tags)) {
341
+ // Comma is the intake-side tag delimiter, so a single `"key:v1,v2"`
342
+ // entry fans into two orphan tags. One-per-element keeps each value
343
+ // addressable; empty arrays fall through to the scalar branch and
344
+ // still emit `key:` so `_dd.cost_tags` references keep finding a
345
+ // wire entry.
346
+ if (Array.isArray(value) && value.length > 0) {
347
+ for (const item of value) out.push(`${key}:${item ?? ''}`)
348
+ } else {
349
+ out.push(`${key}:${value ?? ''}`)
350
+ }
351
+ }
352
+ return out
337
353
  }
338
354
 
339
355
  /**
@@ -43,7 +43,7 @@ const {
43
43
  INSTRUMENTATION_METHOD_ANNOTATED,
44
44
  } = require('./constants/tags')
45
45
  const { storage } = require('./storage')
46
- const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags } = require('./util')
46
+ const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags, validateToolDefinitions } = require('./util')
47
47
 
48
48
  // global registry of LLMObs spans
49
49
  // maps LLMObs spans to their annotations
@@ -176,8 +176,10 @@ class LLMObsTagger {
176
176
  }
177
177
 
178
178
  tagToolDefinitions (span, toolDefinitions) {
179
- if (Array.isArray(toolDefinitions) && toolDefinitions.length > 0) {
180
- this._setTag(span, TOOL_DEFINITIONS, toolDefinitions)
179
+ const validatedToolDefinitions = validateToolDefinitions(toolDefinitions)
180
+
181
+ if (validatedToolDefinitions.length > 0) {
182
+ this._setTag(span, TOOL_DEFINITIONS, validatedToolDefinitions)
181
183
  } else {
182
184
  this.#handleFailure('Tool definitions must be a non-empty array.', 'invalid_tool_definitions')
183
185
  }
@@ -89,6 +89,59 @@ function validateCostTags (span, costTags, source, spanTags) {
89
89
  return [...validatedCostTags]
90
90
  }
91
91
 
92
+ // Validates tool definition entires
93
+ function validateToolDefinitions (toolDefinitions) {
94
+ if (!Array.isArray(toolDefinitions)) {
95
+ log.warn('toolDefinitions must be an array.')
96
+ return []
97
+ }
98
+ const validated = []
99
+
100
+ for (let i = 0; i < toolDefinitions.length; i++) {
101
+ const currToolDef = toolDefinitions[i]
102
+ if (!currToolDef || typeof currToolDef !== 'object') {
103
+ log.warn('Tool definition at index %d must be an object. Skipping.', i)
104
+ continue
105
+ }
106
+
107
+ // Name is not optional
108
+ if (!currToolDef.name || typeof currToolDef.name !== 'string' || currToolDef.name.length <= 0) {
109
+ log.warn('Tool definition at index %d must have a non empty string "name". Skipping.', i)
110
+ continue
111
+ }
112
+ const validatedToolDef = { name: currToolDef.name }
113
+
114
+ // Description, Schema, and Version are optional types
115
+ if (currToolDef.description !== undefined) {
116
+ if (typeof currToolDef.description === 'string') {
117
+ validatedToolDef.description = currToolDef.description
118
+ } else {
119
+ log.warn('Tool definition "description" at index %d must be a string. Skipping field.', i)
120
+ }
121
+ }
122
+
123
+ if (currToolDef.schema !== undefined) {
124
+ if (currToolDef.schema !== null && typeof currToolDef.schema === 'object' && !Array.isArray(currToolDef.schema)) {
125
+ validatedToolDef.schema = currToolDef.schema
126
+ } else {
127
+ log.warn('Tool definition "schema" at index %d must be a plain object. Skipping field.', i)
128
+ }
129
+ }
130
+
131
+ if (currToolDef.version !== undefined) {
132
+ if (typeof currToolDef.version === 'string') {
133
+ validatedToolDef.version = currToolDef.version
134
+ } else {
135
+ log.warn('Tool definition "version" at index %d must be a string. Skipping field.', i)
136
+ }
137
+ }
138
+
139
+ validated.push(validatedToolDef)
140
+ }
141
+
142
+ return validated
143
+ }
144
+
92
145
  // extracts the argument names from a function string
93
146
  function parseArgumentNames (str) {
94
147
  const result = []
@@ -318,4 +371,5 @@ module.exports = {
318
371
  safeJsonParse,
319
372
  spanHasError,
320
373
  writeBridgeTags,
374
+ validateToolDefinitions,
321
375
  }
@@ -14,7 +14,6 @@ const {
14
14
  EVP_SUBDOMAIN_HEADER_NAME,
15
15
  EVP_PROXY_AGENT_BASE_PATH,
16
16
  } = require('../constants/writers')
17
- const { getAgentUrl } = require('../../agent/url')
18
17
  const { parseResponseAndLog } = require('./util')
19
18
 
20
19
  class LLMObsBuffer {
@@ -210,7 +209,7 @@ class BaseLLMObsWriter {
210
209
 
211
210
  const overrideOriginEnv = getEnvironmentVariable('_DD_LLMOBS_OVERRIDE_ORIGIN')
212
211
  const overrideOriginUrl = overrideOriginEnv && new URL(overrideOriginEnv)
213
- const base = overrideOriginUrl ?? getAgentUrl(this._config)
212
+ const base = overrideOriginUrl ?? this._config.url
214
213
 
215
214
  return {
216
215
  url: base,
@@ -4,7 +4,6 @@ const logger = require('../../log')
4
4
  const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/writers')
5
5
  const telemetry = require('../telemetry')
6
6
  const { fetchAgentInfo } = require('../../agent/info')
7
- const { getAgentUrl } = require('../../agent/url')
8
7
 
9
8
  /**
10
9
  * @param {import('../../config/config-base')} config
@@ -17,7 +16,7 @@ function setAgentStrategy (config, setWritersAgentlessValue) {
17
16
  return
18
17
  }
19
18
 
20
- fetchAgentInfo(getAgentUrl(config), (err, agentInfo) => {
19
+ fetchAgentInfo(config.url, (err, agentInfo) => {
21
20
  if (err) {
22
21
  setWritersAgentlessValue(true)
23
22
  return
@@ -2,7 +2,6 @@
2
2
 
3
3
  const request = require('../../exporters/common/request')
4
4
  const { safeJSONStringify } = require('../../exporters/common/util')
5
- const { getAgentUrl } = require('../../agent/url')
6
5
 
7
6
  const log = require('../../log')
8
7
 
@@ -37,7 +36,7 @@ class BaseFFEWriter {
37
36
 
38
37
  this._config = config
39
38
  this._endpoint = endpoint
40
- this._baseUrl = agentUrl ?? this._getAgentUrl()
39
+ this._baseUrl = agentUrl ?? config.url
41
40
  this._payloadSizeLimit = payloadSizeLimit
42
41
  this._eventSizeLimit = eventSizeLimit
43
42
  this._headers = headers || {}
@@ -154,14 +153,6 @@ class BaseFFEWriter {
154
153
  }
155
154
  }
156
155
 
157
- /**
158
- * @private
159
- * @returns {URL} Constructs agent URL from config
160
- */
161
- _getAgentUrl () {
162
- return getAgentUrl(this._config)
163
- }
164
-
165
156
  /**
166
157
  * @private
167
158
  * @param {Array<object>} payload - Payload to encode
@@ -3,7 +3,6 @@
3
3
  const logger = require('../../log')
4
4
  const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/constants')
5
5
  const { fetchAgentInfo } = require('../../agent/info')
6
- const { getAgentUrl } = require('../../agent/url')
7
6
 
8
7
  /**
9
8
  * Determines if the agent supports EVP proxy and sets the writer enabled state accordingly
@@ -11,7 +10,7 @@ const { getAgentUrl } = require('../../agent/url')
11
10
  * @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
12
11
  */
13
12
  function setAgentStrategy (config, setWriterEnabledValue) {
14
- fetchAgentInfo(getAgentUrl(config), (err, agentInfo) => {
13
+ fetchAgentInfo(config.url, (err, agentInfo) => {
15
14
  if (err) {
16
15
  logger.debug('FFE Writer disabled - error getting agent info:', err.message)
17
16
  setWriterEnabledValue(false)