dd-trace 5.87.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 (77) 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 +225 -4
  5. package/package.json +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +54 -90
  7. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
  15. package/packages/datadog-instrumentations/src/jest.js +76 -12
  16. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  17. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  18. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  19. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  20. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  21. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  22. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -0
  24. package/packages/datadog-plugin-cypress/src/support.js +48 -8
  25. package/packages/datadog-plugin-jest/src/index.js +12 -2
  26. package/packages/datadog-plugin-jest/src/util.js +2 -1
  27. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  28. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  29. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  30. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  31. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  32. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  33. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  34. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  35. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  36. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  37. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  38. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  39. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  40. package/packages/dd-trace/src/azure_metadata.js +0 -2
  41. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  42. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
  43. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  44. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  45. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  46. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  47. package/packages/dd-trace/src/config/defaults.js +148 -197
  48. package/packages/dd-trace/src/config/helper.js +43 -1
  49. package/packages/dd-trace/src/config/index.js +36 -14
  50. package/packages/dd-trace/src/config/supported-configurations.json +4115 -512
  51. package/packages/dd-trace/src/constants.js +0 -2
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  53. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  54. package/packages/dd-trace/src/datastreams/processor.js +14 -1
  55. package/packages/dd-trace/src/encode/agentless-json.js +141 -0
  56. package/packages/dd-trace/src/exporter.js +2 -0
  57. package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
  58. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  59. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  60. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  61. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  62. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  63. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  64. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  65. package/packages/dd-trace/src/opentracing/span.js +6 -4
  66. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  67. package/packages/dd-trace/src/plugins/database.js +15 -2
  68. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  69. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  70. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  71. package/packages/dd-trace/src/proxy.js +4 -0
  72. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  73. package/packages/dd-trace/src/startup-log.js +1 -1
  74. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  75. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  76. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  77. package/packages/dd-trace/src/scope/noop/scope.js +0 -21
@@ -46,8 +46,6 @@ module.exports = {
46
46
  SCHEMA_TOPIC: 'schema.topic',
47
47
  SCHEMA_OPERATION: 'schema.operation',
48
48
  SCHEMA_NAME: 'schema.name',
49
- GRPC_CLIENT_ERROR_STATUSES: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
50
- GRPC_SERVER_ERROR_STATUSES: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
51
49
  DYNAMODB_PTR_KIND: 'aws.dynamodb.item',
52
50
  S3_PTR_KIND: 'aws.s3.object',
53
51
  WEBSOCKET_PTR_KIND: 'websocket',
@@ -52,6 +52,12 @@ class Crashtracker {
52
52
  #getConfig (config) {
53
53
  const url = getAgentUrl(config)
54
54
 
55
+ // Out-of-process symbolication currently (crashtracker 27.0.0) works on
56
+ // Linux only, does not work on Mac.
57
+ const resolveMode = require('os').platform === 'linux'
58
+ ? 'EnabledWithSymbolsInReceiver'
59
+ : 'EnabledWithInprocessSymbols'
60
+
55
61
  return {
56
62
  additional_files: [],
57
63
  create_alt_stack: true,
@@ -67,9 +73,10 @@ class Crashtracker {
67
73
  },
68
74
  timeout_ms: 3000,
69
75
  },
70
- timeout_ms: 5000,
71
- // TODO: Use `EnabledWithSymbolsInReceiver` instead for Linux when fixed.
72
- resolve_frames: 'EnabledWithInprocessSymbols',
76
+ timeout: { secs: 5, nanos: 0 },
77
+ demangle_names: false,
78
+ signals: [],
79
+ resolve_frames: resolveMode,
73
80
  }
74
81
  }
75
82
 
@@ -26,17 +26,36 @@ function shaHash (checkpointString) {
26
26
  * @param {string} env
27
27
  * @param {string[]} edgeTags
28
28
  * @param {Buffer} parentHash
29
+ * @param {bigint | null} propagationHashBigInt - Optional propagation hash for process/container tags
29
30
  */
30
- function computeHash (service, env, edgeTags, parentHash) {
31
+ function computeHash (service, env, edgeTags, parentHash, propagationHashBigInt = null) {
31
32
  edgeTags.sort()
32
33
  const hashableEdgeTags = edgeTags.filter(item => item !== 'manual_checkpoint:true')
33
34
 
34
- const key = `${service}${env}${hashableEdgeTags.join('')}${parentHash}`
35
+ // Cache key includes parentHash to handle fan-in/fan-out scenarios where the same
36
+ // service+env+tags+propagationHash can have different parents. This ensures we cache
37
+ // the complete pathway context, not just the current node's identity.
38
+ const propagationPart = propagationHashBigInt ? `:${propagationHashBigInt.toString(16)}` : ''
39
+ const key = `${service}${env}${hashableEdgeTags.join('')}${parentHash}${propagationPart}`
40
+
35
41
  let value = cache.get(key)
36
42
  if (value) {
37
43
  return value
38
44
  }
39
- const currentHash = shaHash(`${service}${env}` + hashableEdgeTags.join(''))
45
+
46
+ // Key vs hashInput distinction:
47
+ // - 'key' (above) is used for caching and includes parentHash to differentiate pathways
48
+ // with the same node but different parents (e.g., multiple queues feeding one consumer)
49
+ // - 'hashInput' (below) excludes parentHash to compute only the current node's identity hash,
50
+ // which is then XORed with parentHash (line 54) to build the complete pathway hash
51
+ // This two-step approach (hash current node independently, then combine with parent) is
52
+ // required for proper pathway construction in the DSM protocol.
53
+ const baseString = `${service}${env}` + hashableEdgeTags.join('')
54
+ const hashInput = propagationHashBigInt
55
+ ? `${baseString}:${propagationHashBigInt.toString(16)}`
56
+ : baseString
57
+
58
+ const currentHash = shaHash(hashInput)
40
59
  const buf = Buffer.concat([currentHash, parentHash], 16)
41
60
  value = shaHash(buf.toString())
42
61
  cache.set(key, value)
@@ -6,6 +6,8 @@ const pkg = require('../../../../package.json')
6
6
  const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js')
7
7
  const { PATHWAY_HASH } = require('../../../../ext/tags')
8
8
  const log = require('../log')
9
+ const processTags = require('../process-tags')
10
+ const propagationHash = require('../propagation-hash')
9
11
  const { DsmPathwayCodec } = require('./pathway')
10
12
  const { DataStreamsWriter } = require('./writer')
11
13
  const { computePathwayHash } = require('./pathway')
@@ -162,6 +164,7 @@ class DataStreamsProcessor {
162
164
  onInterval () {
163
165
  const { Stats } = this._serializeBuckets()
164
166
  if (Stats.length === 0) return
167
+
165
168
  const payload = {
166
169
  Env: this.env,
167
170
  Service: this.service,
@@ -171,6 +174,12 @@ class DataStreamsProcessor {
171
174
  Lang: 'javascript',
172
175
  Tags: Object.entries(this.tags).map(([key, value]) => `${key}:${value}`),
173
176
  }
177
+
178
+ // Add ProcessTags only if feature is enabled and process tags exist
179
+ if (propagationHash.isEnabled() && processTags.serialized) {
180
+ payload.ProcessTags = processTags.serialized.split(',')
181
+ }
182
+
174
183
  this.writer.flush(payload)
175
184
  }
176
185
 
@@ -234,7 +243,11 @@ class DataStreamsProcessor {
234
243
  edgeTags
235
244
  )
236
245
  }
237
- const hash = computePathwayHash(this.service, this.env, edgeTags, parentHash)
246
+
247
+ // Get propagation hash if enabled
248
+ const propagationHashValue = propagationHash.isEnabled() ? propagationHash.getHash() : null
249
+
250
+ const hash = computePathwayHash(this.service, this.env, edgeTags, parentHash, propagationHashValue)
238
251
  const edgeLatencyNs = nowNs - edgeStartNs
239
252
  const pathwayLatencyNs = nowNs - pathwayStartNs
240
253
  const dataStreamsContext = {
@@ -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
@@ -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
  }
@@ -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 }) => {