dd-trace 5.86.0 → 5.88.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/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/index.d.ts +243 -7
  5. package/package.json +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +54 -90
  7. package/packages/datadog-instrumentations/src/cucumber.js +14 -0
  8. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
  17. package/packages/datadog-instrumentations/src/http/client.js +119 -1
  18. package/packages/datadog-instrumentations/src/jest.js +179 -15
  19. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  20. package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
  21. package/packages/datadog-instrumentations/src/mysql2.js +131 -64
  22. package/packages/datadog-instrumentations/src/playwright.js +9 -1
  23. package/packages/datadog-instrumentations/src/stripe.js +92 -0
  24. package/packages/datadog-instrumentations/src/vitest.js +11 -0
  25. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  26. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  27. package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
  28. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  29. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  30. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +33 -0
  32. package/packages/datadog-plugin-cypress/src/support.js +48 -8
  33. package/packages/datadog-plugin-jest/src/index.js +12 -2
  34. package/packages/datadog-plugin-jest/src/util.js +2 -1
  35. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  36. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  37. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  38. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  39. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  40. package/packages/dd-trace/src/appsec/addresses.js +11 -0
  41. package/packages/dd-trace/src/appsec/channels.js +5 -1
  42. package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
  43. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  44. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  45. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  46. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  47. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  48. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  49. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  50. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  51. package/packages/dd-trace/src/appsec/index.js +103 -0
  52. package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
  53. package/packages/dd-trace/src/azure_metadata.js +0 -2
  54. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
  55. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  56. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
  57. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  58. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  59. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  60. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  61. package/packages/dd-trace/src/config/defaults.js +148 -195
  62. package/packages/dd-trace/src/config/helper.js +43 -1
  63. package/packages/dd-trace/src/config/index.js +42 -14
  64. package/packages/dd-trace/src/config/supported-configurations.json +4115 -510
  65. package/packages/dd-trace/src/constants.js +0 -2
  66. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  67. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  68. package/packages/dd-trace/src/datastreams/processor.js +14 -1
  69. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
  70. package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
  71. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
  72. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
  73. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
  74. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
  75. package/packages/dd-trace/src/encode/agentless-json.js +141 -0
  76. package/packages/dd-trace/src/exporter.js +2 -0
  77. package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
  78. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  79. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  80. package/packages/dd-trace/src/exporters/common/agents.js +1 -1
  81. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  82. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  83. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  84. package/packages/dd-trace/src/llmobs/sdk.js +34 -5
  85. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  86. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  87. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  88. package/packages/dd-trace/src/opentracing/span.js +6 -4
  89. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  90. package/packages/dd-trace/src/plugins/database.js +57 -45
  91. package/packages/dd-trace/src/plugins/outbound.js +27 -2
  92. package/packages/dd-trace/src/plugins/tracing.js +39 -4
  93. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
  94. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  95. package/packages/dd-trace/src/plugins/util/web.js +8 -7
  96. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  97. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  98. package/packages/dd-trace/src/proxy.js +4 -0
  99. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  100. package/packages/dd-trace/src/startup-log.js +3 -3
  101. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  102. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  103. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  104. package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
  105. package/packages/dd-trace/src/scope/noop/scope.js +0 -21
@@ -6,6 +6,24 @@ const { normalizeName, REDACTED_IDENTIFIERS } = require('./redaction')
6
6
 
7
7
  module.exports = {
8
8
  processRawState: processProperties,
9
+ processRemoteObject,
10
+ }
11
+
12
+ /**
13
+ * A RemoteObject with collected properties attached.
14
+ *
15
+ * @typedef {import('inspector').Runtime.RemoteObject & { properties?: object[] }} RemoteObjectWithProperties
16
+ */
17
+
18
+ /**
19
+ * Process a RemoteObject into the snapshot format.
20
+ *
21
+ * @param {RemoteObjectWithProperties} remoteObject
22
+ * @param {number} maxLength - Maximum string length
23
+ * @returns {object} The processed value in snapshot format
24
+ */
25
+ function processRemoteObject (remoteObject, maxLength) {
26
+ return getPropertyValueRaw({ value: remoteObject }, maxLength)
9
27
  }
10
28
 
11
29
  // Matches classes in source code, no matter how it's written:
@@ -0,0 +1,141 @@
1
+ 'use strict'
2
+
3
+ const log = require('../log')
4
+ const { truncateSpan, normalizeSpan } = require('./tags-processors')
5
+
6
+ /**
7
+ * Formats a span for JSON encoding.
8
+ * @param {object} span - The span to format
9
+ * @returns {object} The formatted span
10
+ */
11
+ function formatSpan (span) {
12
+ span = normalizeSpan(truncateSpan(span, false))
13
+
14
+ if (span.span_events) {
15
+ span.meta.events = JSON.stringify(span.span_events)
16
+ delete span.span_events
17
+ }
18
+
19
+ return span
20
+ }
21
+
22
+ /**
23
+ * Converts a span to JSON-serializable format.
24
+ * IDs are converted to lowercase hex strings. Start time is converted from
25
+ * nanoseconds to seconds for the intake format.
26
+ * @param {object} span - The formatted span
27
+ * @returns {object} JSON-serializable span object
28
+ */
29
+ function spanToJSON (span) {
30
+ const result = {
31
+ trace_id: span.trace_id.toString(16).toLowerCase(),
32
+ span_id: span.span_id.toString(16).toLowerCase(),
33
+ parent_id: span.parent_id.toString(16).toLowerCase(),
34
+ name: span.name,
35
+ resource: span.resource,
36
+ service: span.service,
37
+ error: span.error,
38
+ start: Math.floor(span.start / 1e9),
39
+ duration: span.duration,
40
+ meta: span.meta,
41
+ metrics: span.metrics,
42
+ }
43
+
44
+ if (span.type) {
45
+ result.type = span.type
46
+ }
47
+
48
+ if (span.meta_struct) {
49
+ result.meta_struct = span.meta_struct
50
+ }
51
+
52
+ if (span.links && span.links.length > 0) {
53
+ result.links = span.links
54
+ }
55
+
56
+ return result
57
+ }
58
+
59
+ /**
60
+ * JSON encoder for agentless span intake.
61
+ * Encodes a single trace as JSON with the payload format: {"spans": [...]}
62
+ *
63
+ * This encoder handles one trace at a time since each trace must be sent as a
64
+ * separate request to the intake. -- bengl
65
+ */
66
+ class AgentlessJSONEncoder {
67
+ constructor () {
68
+ this._reset()
69
+ }
70
+
71
+ /**
72
+ * Returns the number of spans encoded.
73
+ * @returns {number}
74
+ */
75
+ count () {
76
+ return this._spanCount
77
+ }
78
+
79
+ /**
80
+ * Encodes a trace (array of spans) into the buffer.
81
+ * @param {object[]} trace - Array of spans to encode
82
+ */
83
+ encode (trace) {
84
+ for (const span of trace) {
85
+ try {
86
+ const formattedSpan = formatSpan(span)
87
+ const jsonSpan = spanToJSON(formattedSpan)
88
+
89
+ this._spans.push(jsonSpan)
90
+ this._spanCount++
91
+ } catch (err) {
92
+ log.error(
93
+ 'Failed to encode span (name: %s, service: %s). Span will be dropped. Error: %s\n%s',
94
+ span?.name || 'unknown',
95
+ span?.service || 'unknown',
96
+ err.message,
97
+ err.stack
98
+ )
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Creates the JSON payload for the encoded trace.
105
+ * @returns {Buffer} JSON payload as a buffer, or empty buffer if no spans
106
+ */
107
+ makePayload () {
108
+ if (this._spans.length === 0) {
109
+ this._reset()
110
+ return Buffer.alloc(0)
111
+ }
112
+
113
+ try {
114
+ const payload = JSON.stringify({ spans: this._spans })
115
+ this._reset()
116
+ return Buffer.from(payload, 'utf8')
117
+ } catch (err) {
118
+ log.error(
119
+ 'Failed to encode trace as JSON (%d spans). Trace will be dropped. Error: %s',
120
+ this._spans.length,
121
+ err.message
122
+ )
123
+ this._reset()
124
+ return Buffer.alloc(0)
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Resets the encoder state.
130
+ */
131
+ reset () {
132
+ this._reset()
133
+ }
134
+
135
+ _reset () {
136
+ this._spans = []
137
+ this._spanCount = 0
138
+ }
139
+ }
140
+
141
+ module.exports = { AgentlessJSONEncoder }
@@ -11,6 +11,8 @@ module.exports = function getExporter (name) {
11
11
  return require('./exporters/log')
12
12
  case exporters.AGENT:
13
13
  return require('./exporters/agent')
14
+ case exporters.AGENTLESS:
15
+ return require('./exporters/agentless')
14
16
  case exporters.DATADOG:
15
17
  return require('./ci-visibility/exporters/agentless')
16
18
  case exporters.AGENT_PROXY:
@@ -1,13 +1,12 @@
1
1
  'use strict'
2
2
 
3
- const { inspect } = require('util')
4
-
5
3
  const request = require('../common/request')
6
4
  const { startupLog } = require('../../startup-log')
7
5
  const runtimeMetrics = require('../../runtime_metrics')
8
6
  const log = require('../../log')
9
7
  const tracerVersion = require('../../../../../package.json').version
10
8
  const BaseWriter = require('../common/writer')
9
+ const propagationHash = require('../../propagation-hash')
11
10
 
12
11
  const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
13
12
 
@@ -29,10 +28,7 @@ class AgentWriter extends BaseWriter {
29
28
  runtimeMetrics.increment(`${METRIC_PREFIX}.requests`, true)
30
29
 
31
30
  const { _headers, _lookup, _protocolVersion, _url } = this
32
- makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, (err, res, status) => {
33
- // Note that logging will only happen once, regardless of how many times this is called.
34
- startupLog(status !== 404 && status !== 200 ? { status, message: err?.message ?? inspect(err) } : undefined)
35
-
31
+ makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, true, (err, res, status, headers) => {
36
32
  if (status) {
37
33
  runtimeMetrics.increment(`${METRIC_PREFIX}.responses`, true)
38
34
  runtimeMetrics.increment(`${METRIC_PREFIX}.responses.by.status`, `status:${status}`, true)
@@ -53,6 +49,16 @@ class AgentWriter extends BaseWriter {
53
49
 
54
50
  log.debug('Response from the agent: %s', res)
55
51
 
52
+ // Capture container tags hash from agent response headers
53
+ // The hash is sent by the agent only when Datadog-Container-ID is present in the request
54
+ // (Datadog-Container-ID is automatically injected by docker.inject() in exporters/common/request.js)
55
+ if (headers) {
56
+ const containerTagsHash = headers['Datadog-Container-Tags-Hash']
57
+ if (containerTagsHash) {
58
+ propagationHash.updateContainerTagsHash(containerTagsHash)
59
+ }
60
+ }
61
+
56
62
  try {
57
63
  this._prioritySampler.update(JSON.parse(res).rate_by_service)
58
64
  } catch (e) {
@@ -72,7 +78,7 @@ function getEncoder (protocolVersion) {
72
78
  : require('../../encode/0.4').AgentEncoder
73
79
  }
74
80
 
75
- function makeRequest (version, data, count, url, headers, lookup, cb) {
81
+ function makeRequest (version, data, count, url, headers, lookup, needsStartupLog, cb) {
76
82
  const options = {
77
83
  path: `/v${version}/traces`,
78
84
  method: 'PUT',
@@ -91,7 +97,15 @@ function makeRequest (version, data, count, url, headers, lookup, cb) {
91
97
 
92
98
  log.debug('Request to the agent: %j', options)
93
99
 
94
- request(data, options, cb)
100
+ request(data, options, (err, res, status, headers) => {
101
+ if (needsStartupLog) {
102
+ // Note that logging will only happen once, regardless of how many times this is called.
103
+ startupLog({
104
+ agentError: status !== 404 && status !== 200 ? err : undefined,
105
+ })
106
+ }
107
+ cb(err, res, status, headers)
108
+ })
95
109
  }
96
110
 
97
111
  module.exports = AgentWriter
@@ -0,0 +1,89 @@
1
+ 'use strict'
2
+
3
+ const { URL } = require('node:url')
4
+
5
+ const log = require('../../log')
6
+ const Writer = require('./writer')
7
+
8
+ /**
9
+ * Agentless exporter for APM span intake.
10
+ * Sends spans directly to the Datadog intake without requiring a local agent.
11
+ *
12
+ * Each trace is sent immediately as a separate request. The intake only accepts one trace
13
+ * per request - requests with spans from different traces return HTTP 200 but silently
14
+ * drop all spans. By flushing immediately after each export (which contains one trace),
15
+ * we avoid this limitation entirely. -- bengl
16
+ */
17
+ class AgentlessExporter {
18
+ /**
19
+ * @param {object} config - Configuration object
20
+ * @param {string} [config.site='datadoghq.com'] - The Datadog site
21
+ * @param {string} [config.url] - Override intake URL
22
+ */
23
+ constructor (config) {
24
+ this._config = config
25
+ const { site = 'datadoghq.com', url } = config
26
+
27
+ try {
28
+ this._url = url ? new URL(url) : new URL(`https://public-trace-http-intake.logs.${site}`)
29
+ } catch (err) {
30
+ log.error(
31
+ 'Invalid URL configuration for agentless exporter. url=%s, site=%s. Error: %s',
32
+ url || 'not set',
33
+ site,
34
+ err.message
35
+ )
36
+ this._url = null
37
+ }
38
+
39
+ this._writer = new Writer({
40
+ url: this._url,
41
+ site,
42
+ })
43
+
44
+ const ddTrace = globalThis[Symbol.for('dd-trace')]
45
+ if (ddTrace?.beforeExitHandlers) {
46
+ ddTrace.beforeExitHandlers.add(this.flush.bind(this))
47
+ } else {
48
+ log.error('dd-trace global not properly initialized. beforeExit handler not registered for agentless exporter.')
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Sets the intake URL.
54
+ * @param {string} urlString - The new intake URL
55
+ * @returns {boolean} True if URL was set successfully
56
+ */
57
+ setUrl (urlString) {
58
+ try {
59
+ const url = new URL(urlString)
60
+ this._url = url
61
+ this._writer.setUrl(url)
62
+ return true
63
+ } catch {
64
+ log.error('Invalid URL for agentless exporter: %s. Using previous URL: %s', urlString, this._url?.href || 'none')
65
+ return false
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Exports a trace to the intake. Flushes immediately since each trace must be
71
+ * sent as a separate request.
72
+ * @param {object[]} spans - Array of spans (all from the same trace)
73
+ */
74
+ export (spans) {
75
+ this._writer.append(spans)
76
+ this._writer.flush()
77
+ }
78
+
79
+ /**
80
+ * Flushes any pending spans. With immediate flush per trace, this is mainly
81
+ * used for the beforeExit handler to ensure nothing is left unsent.
82
+ * @param {Function} [done] - Callback when flush is complete
83
+ */
84
+ flush (done = () => {}) {
85
+ this._writer.flush(done)
86
+ }
87
+ }
88
+
89
+ module.exports = AgentlessExporter
@@ -0,0 +1,184 @@
1
+ 'use strict'
2
+
3
+ const { getValueFromEnvSources } = require('../../config/helper')
4
+ const log = require('../../log')
5
+ const request = require('../common/request')
6
+ const tracerVersion = require('../../../../../package.json').version
7
+
8
+ const BaseWriter = require('../common/writer')
9
+ const { AgentlessJSONEncoder } = require('../../encode/agentless-json')
10
+
11
+ /**
12
+ * Writer for agentless APM span intake.
13
+ * Sends spans directly to the Datadog intake endpoint without an agent.
14
+ */
15
+ class AgentlessWriter extends BaseWriter {
16
+ #apiKeyMissing = false
17
+
18
+ /**
19
+ * @param {object} options - Writer options
20
+ * @param {URL} [options.url] - The intake URL. If not provided, constructed from site.
21
+ * @param {string} [options.site='datadoghq.com'] - The Datadog site
22
+ */
23
+ constructor ({ url, site = 'datadoghq.com' }) {
24
+ super({ url })
25
+ this._encoder = new AgentlessJSONEncoder()
26
+
27
+ if (!url) {
28
+ try {
29
+ this._url = new URL(`https://public-trace-http-intake.logs.${site}`)
30
+ } catch (err) {
31
+ log.error(
32
+ 'Invalid site value for agentless intake: %s. Cannot construct URL. Error: %s',
33
+ site,
34
+ err.message
35
+ )
36
+ this._url = null
37
+ }
38
+ }
39
+
40
+ if (!getValueFromEnvSources('DD_API_KEY')) {
41
+ this.#apiKeyMissing = true
42
+ log.error('DD_API_KEY is required for agentless span intake. Set DD_API_KEY. Spans will not be sent.')
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Flushes the current trace. Since we flush after each trace, this sends
48
+ * a single request.
49
+ * @param {Function} [done] - Callback when send completes
50
+ */
51
+ flush (done = () => {}) {
52
+ if (!request.writable) {
53
+ this._encoder.reset()
54
+ done()
55
+ return
56
+ }
57
+
58
+ const count = this._encoder.count()
59
+
60
+ if (count === 0) {
61
+ done()
62
+ return
63
+ }
64
+
65
+ const payload = this._encoder.makePayload()
66
+
67
+ if (payload.length === 0) {
68
+ log.debug('Skipping send of empty payload')
69
+ done()
70
+ return
71
+ }
72
+
73
+ this._sendPayload(payload, count, done)
74
+ }
75
+
76
+ /**
77
+ * Sends the encoded payload to the intake endpoint.
78
+ * @param {Buffer} data - The encoded JSON payload
79
+ * @param {number} count - Number of spans in the payload
80
+ * @param {Function} done - Callback when complete
81
+ */
82
+ _sendPayload (data, count, done) {
83
+ if (!data || data.length === 0) {
84
+ log.debug('Skipping send of empty payload')
85
+ done()
86
+ return
87
+ }
88
+
89
+ if (!this._url) {
90
+ log.debug('Skipping send due to invalid URL configuration')
91
+ done()
92
+ return
93
+ }
94
+
95
+ const apiKey = getValueFromEnvSources('DD_API_KEY')
96
+ if (!apiKey) {
97
+ if (!this.#apiKeyMissing) {
98
+ this.#apiKeyMissing = true
99
+ log.error('DD_API_KEY is required for agentless span intake. Set DD_API_KEY. Spans will not be sent.')
100
+ }
101
+ log.debug('Dropping %d span(s) due to missing DD_API_KEY', count)
102
+ done()
103
+ return
104
+ }
105
+ this.#apiKeyMissing = false
106
+
107
+ const options = {
108
+ path: '/v1/input',
109
+ method: 'POST',
110
+ headers: {
111
+ 'Content-Type': 'application/json',
112
+ 'dd-api-key': apiKey,
113
+ 'Datadog-Meta-Lang': 'nodejs',
114
+ 'Datadog-Meta-Lang-Version': process.version,
115
+ 'Datadog-Meta-Lang-Interpreter': process.versions.bun ? 'JavaScriptCore' : 'v8',
116
+ 'Datadog-Meta-Tracer-Version': tracerVersion,
117
+ },
118
+ timeout: 15_000,
119
+ url: this._url,
120
+ }
121
+
122
+ log.debug('Request to the agentless intake: %j', options)
123
+
124
+ request(data, options, (err, res, statusCode) => {
125
+ if (err) {
126
+ this._logRequestError(err, statusCode, count)
127
+ done()
128
+ return
129
+ }
130
+
131
+ log.debug('Response from the agentless intake: %s', res)
132
+ done()
133
+ })
134
+ }
135
+
136
+ /**
137
+ * Logs request errors with status-specific guidance.
138
+ * @param {Error} err - The error object
139
+ * @param {number} statusCode - HTTP status code (if available)
140
+ * @param {number} count - Number of spans that were being sent
141
+ */
142
+ _logRequestError (err, statusCode, count) {
143
+ if (statusCode === 401 || statusCode === 403) {
144
+ log.error(
145
+ 'Authentication failed sending %d span(s) (status %s). Verify DD_API_KEY is valid.',
146
+ count,
147
+ statusCode
148
+ )
149
+ } else if (statusCode === 404) {
150
+ log.error(
151
+ 'Span intake endpoint not found (status %s). Verify DD_SITE is correctly configured. %d span(s) dropped.',
152
+ statusCode,
153
+ count
154
+ )
155
+ } else if (statusCode === 429) {
156
+ log.error(
157
+ 'Rate limited by span intake (status 429). %d span(s) dropped.',
158
+ count
159
+ )
160
+ } else if (statusCode >= 500) {
161
+ log.error(
162
+ 'Span intake server error (status %s). %d span(s) dropped. This may be transient.',
163
+ statusCode,
164
+ count
165
+ )
166
+ } else if (statusCode) {
167
+ log.error(
168
+ 'Error sending agentless payload (status %s): %s. %d span(s) dropped.',
169
+ statusCode,
170
+ err.message,
171
+ count
172
+ )
173
+ } else {
174
+ log.error(
175
+ 'Network error sending %d span(s) to %s: %s',
176
+ count,
177
+ this._url?.hostname || 'unknown',
178
+ err.message
179
+ )
180
+ }
181
+ }
182
+ }
183
+
184
+ module.exports = AgentlessWriter
@@ -38,5 +38,5 @@ const HttpsAgent = createAgentClass(https.Agent)
38
38
 
39
39
  module.exports = {
40
40
  httpAgent: new HttpAgent(),
41
- HttpsAgent: new HttpsAgent(),
41
+ httpsAgent: new HttpsAgent(),
42
42
  }
@@ -88,13 +88,13 @@ function request (data, options, callback) {
88
88
  zlib.gunzip(buffer, (err, result) => {
89
89
  if (err) {
90
90
  log.error('Could not gunzip response: %s', err.message)
91
- callback(null, '', res.statusCode)
91
+ callback(null, '', res.statusCode, res.headers)
92
92
  } else {
93
- callback(null, result.toString(), res.statusCode)
93
+ callback(null, result.toString(), res.statusCode, res.headers)
94
94
  }
95
95
  })
96
96
  } else {
97
- callback(null, buffer.toString(), res.statusCode)
97
+ callback(null, buffer.toString(), res.statusCode, res.headers)
98
98
  }
99
99
  } else {
100
100
  let errorMessage = ''
@@ -115,7 +115,7 @@ function request (data, options, callback) {
115
115
  const error = new log.NoTransmitError(errorMessage)
116
116
  error.status = res.statusCode
117
117
 
118
- callback(error, null, res.statusCode)
118
+ callback(error, null, res.statusCode, res.headers)
119
119
  }
120
120
  })
121
121
  }
@@ -10,7 +10,7 @@ module.exports = {
10
10
 
11
11
  EVALUATIONS_INTAKE: 'api',
12
12
  EVALUATIONS_EVENT_TYPE: 'evaluation_metric',
13
- EVALUATIONS_ENDPOINT: '/api/intake/llm-obs/v1/eval-metric',
13
+ EVALUATIONS_ENDPOINT: '/api/intake/llm-obs/v2/eval-metric',
14
14
 
15
15
  EVP_PAYLOAD_SIZE_LIMIT: 5 << 20, // 5MB (actual limit is 5.1MB)
16
16
  EVP_EVENT_SIZE_LIMIT: (1 << 20) - 1024, // 999KB (actual limit is 1MB)
@@ -4,7 +4,7 @@ const { channel } = require('dc-polyfill')
4
4
  const BaseLLMObsPlugin = require('../base')
5
5
  const { getModelProvider } = require('../../../../../datadog-plugin-ai/src/utils')
6
6
 
7
- const toolCreationCh = channel('dd-trace:vercel-ai:tool')
7
+ const toolCreationCh = channel('tracing:orchestrion:ai:tool:start')
8
8
  const setAttributesCh = channel('dd-trace:vercel-ai:span:setAttributes')
9
9
 
10
10
  const { MODEL_NAME, MODEL_PROVIDER, NAME } = require('../../constants/tags')
@@ -94,8 +94,10 @@ class VercelAILLMObsPlugin extends BaseLLMObsPlugin {
94
94
 
95
95
  this.#toolCallIdsToName = {}
96
96
  this.#availableTools = new Set()
97
- toolCreationCh.subscribe(toolArgs => {
98
- this.#availableTools.add(toolArgs)
97
+ toolCreationCh.subscribe(ctx => {
98
+ const toolArgs = ctx.arguments
99
+ const tool = toolArgs[0] ?? {}
100
+ this.#availableTools.add(tool)
99
101
  })
100
102
 
101
103
  setAttributesCh.subscribe(({ ctx, attributes }) => {
@@ -359,15 +359,15 @@ class LLMObs extends NoopLLMObs {
359
359
  throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
360
360
  }
361
361
 
362
- const { label, value, tags } = options
362
+ const { label, value, tags, reasoning, assessment, metadata } = options
363
363
  const metricType = options.metricType?.toLowerCase()
364
364
  if (!label) {
365
365
  err = 'invalid_metric_label'
366
366
  throw new Error('label must be the specified name of the evaluation metric')
367
367
  }
368
- if (!metricType || !['categorical', 'score', 'boolean'].includes(metricType)) {
368
+ if (!metricType || !['categorical', 'score', 'boolean', 'json'].includes(metricType)) {
369
369
  err = 'invalid_metric_type'
370
- throw new Error('metricType must be one of "categorical" or "score"')
370
+ throw new Error('metricType must be one of "categorical", "score", "boolean" or "json"')
371
371
  }
372
372
  if (metricType === 'categorical' && typeof value !== 'string') {
373
373
  err = 'invalid_metric_value'
@@ -381,6 +381,22 @@ class LLMObs extends NoopLLMObs {
381
381
  err = 'invalid_metric_value'
382
382
  throw new Error('value must be a boolean for a boolean metric')
383
383
  }
384
+ if (metricType === 'json' && !(typeof value === 'object' && value != null && !Array.isArray(value))) {
385
+ err = 'invalid_metric_value'
386
+ throw new Error('value must be a JSON object for a json metric')
387
+ }
388
+ if (assessment != null && assessment !== 'pass' && assessment !== 'fail') {
389
+ err = 'invalid_assessment'
390
+ throw new Error('assessment must be pass or fail')
391
+ }
392
+ if (reasoning != null && typeof reasoning !== 'string') {
393
+ err = 'invalid_reasoning'
394
+ throw new Error('reasoning must be a string')
395
+ }
396
+ if (metadata != null && (typeof metadata !== 'object' || Array.isArray(metadata))) {
397
+ err = 'invalid_metadata'
398
+ throw new Error('metadata must be a JSON object')
399
+ }
384
400
 
385
401
  const evaluationTags = {
386
402
  'ddtrace.version': tracerVersion,
@@ -412,8 +428,12 @@ class LLMObs extends NoopLLMObs {
412
428
  }
413
429
 
414
430
  const payload = {
415
- span_id: spanId,
416
- trace_id: traceId,
431
+ join_on: {
432
+ span: {
433
+ span_id: spanId,
434
+ trace_id: traceId,
435
+ },
436
+ },
417
437
  label,
418
438
  metric_type: metricType,
419
439
  ml_app: mlApp,
@@ -421,6 +441,15 @@ class LLMObs extends NoopLLMObs {
421
441
  timestamp_ms: timestampMs,
422
442
  tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`),
423
443
  }
444
+ if (reasoning != null) {
445
+ payload.reasoning = reasoning
446
+ }
447
+ if (metadata != null) {
448
+ payload.metadata = metadata
449
+ }
450
+ if (assessment != null) {
451
+ payload.assessment = assessment
452
+ }
424
453
  const currentStore = storage.getStore()
425
454
  const routing = currentStore?.routingContext
426
455
  evalMetricAppendCh.publish({ payload, routing })