dd-trace 5.87.0 → 5.89.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 (119) 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/ext/tags.js +2 -0
  5. package/index.d.ts +234 -4
  6. package/package.json +18 -11
  7. package/packages/datadog-instrumentations/src/ai.js +54 -90
  8. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +27 -110
  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/orchestrion/compiler.js +74 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/index.js +43 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/matcher.js +49 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/transformer.js +121 -0
  17. package/packages/datadog-instrumentations/src/helpers/rewriter/{transforms.js → orchestrion/transforms.js} +143 -17
  18. package/packages/datadog-instrumentations/src/jest.js +176 -54
  19. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  20. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  21. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  22. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  23. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  24. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  25. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  26. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +62 -5
  27. package/packages/datadog-plugin-cypress/src/source-map-utils.js +297 -0
  28. package/packages/datadog-plugin-cypress/src/support.js +52 -9
  29. package/packages/datadog-plugin-jest/src/index.js +12 -2
  30. package/packages/datadog-plugin-jest/src/util.js +2 -1
  31. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  32. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  33. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  34. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  35. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  36. package/packages/dd-trace/src/aiguard/sdk.js +5 -1
  37. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  38. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  39. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  40. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  41. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  42. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  45. package/packages/dd-trace/src/azure_metadata.js +0 -2
  46. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  47. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +3 -0
  48. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  49. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  50. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  51. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  52. package/packages/dd-trace/src/config/defaults.js +148 -197
  53. package/packages/dd-trace/src/config/helper.js +43 -1
  54. package/packages/dd-trace/src/config/index.js +38 -14
  55. package/packages/dd-trace/src/config/supported-configurations.json +4125 -512
  56. package/packages/dd-trace/src/constants.js +0 -2
  57. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  58. package/packages/dd-trace/src/datastreams/checkpointer.js +13 -0
  59. package/packages/dd-trace/src/datastreams/index.js +3 -0
  60. package/packages/dd-trace/src/datastreams/manager.js +9 -0
  61. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  62. package/packages/dd-trace/src/datastreams/processor.js +140 -4
  63. package/packages/dd-trace/src/encode/agentless-json.js +155 -0
  64. package/packages/dd-trace/src/exporter.js +2 -0
  65. package/packages/dd-trace/src/exporters/agent/writer.js +21 -8
  66. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  67. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  68. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  69. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  70. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  71. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  72. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  73. package/packages/dd-trace/src/opentracing/span.js +6 -4
  74. package/packages/dd-trace/src/pkg.js +1 -1
  75. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  76. package/packages/dd-trace/src/plugins/database.js +15 -2
  77. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  78. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  79. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  80. package/packages/dd-trace/src/proxy.js +6 -1
  81. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  82. package/packages/dd-trace/src/startup-log.js +53 -19
  83. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  84. package/vendor/dist/@datadog/source-map/index.js +1 -1
  85. package/vendor/dist/@isaacs/ttlcache/index.js +1 -1
  86. package/vendor/dist/@opentelemetry/core/index.js +1 -1
  87. package/vendor/dist/@opentelemetry/resources/index.js +1 -1
  88. package/vendor/dist/astring/index.js +1 -1
  89. package/vendor/dist/crypto-randomuuid/index.js +1 -1
  90. package/vendor/dist/escape-string-regexp/index.js +1 -1
  91. package/vendor/dist/esquery/index.js +1 -1
  92. package/vendor/dist/ignore/index.js +1 -1
  93. package/vendor/dist/istanbul-lib-coverage/index.js +1 -1
  94. package/vendor/dist/jest-docblock/index.js +1 -1
  95. package/vendor/dist/jsonpath-plus/index.js +1 -1
  96. package/vendor/dist/limiter/index.js +1 -1
  97. package/vendor/dist/lodash.sortby/index.js +1 -1
  98. package/vendor/dist/lru-cache/index.js +1 -1
  99. package/vendor/dist/meriyah/index.js +1 -1
  100. package/vendor/dist/module-details-from-path/index.js +1 -1
  101. package/vendor/dist/mutexify/promise/index.js +1 -1
  102. package/vendor/dist/opentracing/index.js +1 -1
  103. package/vendor/dist/path-to-regexp/index.js +1 -1
  104. package/vendor/dist/pprof-format/index.js +1 -1
  105. package/vendor/dist/protobufjs/index.js +1 -1
  106. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  107. package/vendor/dist/retry/index.js +1 -1
  108. package/vendor/dist/rfdc/index.js +1 -1
  109. package/vendor/dist/semifies/index.js +1 -1
  110. package/vendor/dist/shell-quote/index.js +1 -1
  111. package/vendor/dist/source-map/index.js +1 -1
  112. package/vendor/dist/source-map/lib/util/index.js +1 -1
  113. package/vendor/dist/tlhunter-sorted-set/index.js +1 -1
  114. package/vendor/dist/ttl-set/index.js +1 -1
  115. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +0 -33
  116. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  117. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  118. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  119. 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
 
@@ -60,6 +60,19 @@ class DataStreamsCheckpointer {
60
60
 
61
61
  return ctx
62
62
  }
63
+
64
+ /**
65
+ * Records a transaction ID at a named checkpoint without pathway propagation.
66
+ * Tags the active span (or the provided span) with the transaction ID and checkpoint name.
67
+ * @param {string} transactionId - The transaction identifier to track.
68
+ * @param {string} checkpointName - The logical checkpoint name.
69
+ * @param {object|null} [span=null] - Span to tag. Defaults to the currently active span.
70
+ */
71
+ trackTransaction (transactionId, checkpointName, span = null) {
72
+ if (!this.config.dsmEnabled) return
73
+ const activeSpan = span ?? this.tracer.scope().active()
74
+ this.dsmProcessor.trackTransaction(transactionId, checkpointName, activeSpan)
75
+ }
63
76
  }
64
77
 
65
78
  module.exports = {
@@ -71,6 +71,7 @@ const DsmPathwayCodec = lazyClass(() => require('./pathway').DsmPathwayCodec, []
71
71
  const DataStreamsCheckpointer = lazyClass(() => require('./checkpointer').DataStreamsCheckpointer, [
72
72
  'setProduceCheckpoint',
73
73
  'setConsumeCheckpoint',
74
+ 'trackTransaction',
74
75
  ])
75
76
 
76
77
  /**
@@ -79,6 +80,7 @@ const DataStreamsCheckpointer = lazyClass(() => require('./checkpointer').DataSt
79
80
  const DataStreamsManager = lazyClass(() => require('./manager').DataStreamsManager, [
80
81
  'setCheckpoint',
81
82
  'decodeDataStreamsContext',
83
+ 'trackTransaction',
82
84
  ])
83
85
 
84
86
  // TODO: Are all those methods actually public?
@@ -92,6 +94,7 @@ const DataStreamsProcessor = lazyClass(() => require('./processor').DataStreamsP
92
94
  'setCheckpoint',
93
95
  'recordOffset',
94
96
  'setOffset',
97
+ 'trackTransaction',
95
98
  'setUrl',
96
99
  'trySampleSchema',
97
100
  'canSampleSchema',
@@ -22,6 +22,15 @@ class DataStreamsManager {
22
22
  DataStreamsContext.setDataStreamsContext(ctx)
23
23
  return ctx
24
24
  }
25
+
26
+ /**
27
+ * @param {string} transactionId
28
+ * @param {string} checkpointName
29
+ * @param {object|null} [span=null]
30
+ */
31
+ trackTransaction (transactionId, checkpointName, span = null) {
32
+ this._dataStreamsProcessor.trackTransaction(transactionId, checkpointName, span)
33
+ }
25
34
  }
26
35
 
27
36
  module.exports = { DataStreamsManager }
@@ -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)
@@ -4,8 +4,10 @@ const os = require('os')
4
4
  const pkg = require('../../../../package.json')
5
5
 
6
6
  const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js')
7
- const { PATHWAY_HASH } = require('../../../../ext/tags')
7
+ const { PATHWAY_HASH, DSM_TRANSACTION_ID, DSM_TRANSACTION_CHECKPOINT } = 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')
@@ -67,10 +69,66 @@ class Backlog {
67
69
  }
68
70
  }
69
71
 
72
+ /**
73
+ * Maps checkpoint name strings to single-byte IDs (1–254).
74
+ * ID 0 is reserved; 254 is the maximum number of unique names.
75
+ * Scope is per-processor so IDs are stable across bucket boundaries within a process lifetime.
76
+ */
77
+ class CheckpointRegistry {
78
+ /** @type {Map<string, number>} */
79
+ #nameToId = new Map()
80
+ #nextId = 1
81
+ /** @type {Buffer[]} Pre-built [id uint8][nameLen uint8][name bytes] entries, one per registered name. */
82
+ #entryBuffers = []
83
+ /** @type {Buffer | null} Cached concat of #entryBuffers; reset when a new name is added. */
84
+ #encodedKeysCache = null
85
+
86
+ /**
87
+ * Returns the byte ID for the given checkpoint name, assigning one if not seen before.
88
+ * Returns undefined when registry is full (254 entries exhausted).
89
+ * @param {string} name
90
+ * @returns {number | undefined}
91
+ */
92
+ getId (name) {
93
+ const existing = this.#nameToId.get(name)
94
+ if (existing !== undefined) return existing
95
+ if (this.#nextId > 254) return
96
+ const id = this.#nextId++
97
+ this.#nameToId.set(name, id)
98
+ // Build the wire entry now with a bounded write so long names never materialise
99
+ // their full UTF-8 encoding — buf.write() stops at the supplied byte limit.
100
+ const nameBuf = Buffer.allocUnsafe(255)
101
+ const nameByteLen = nameBuf.write(name, 0, 255, 'utf8')
102
+ const entry = Buffer.allocUnsafe(2 + nameByteLen)
103
+ entry.writeUInt8(id, 0)
104
+ entry.writeUInt8(nameByteLen, 1)
105
+ nameBuf.copy(entry, 2, 0, nameByteLen)
106
+ this.#entryBuffers.push(entry)
107
+ this.#encodedKeysCache = null
108
+ return id
109
+ }
110
+
111
+ /**
112
+ * Returns a Buffer encoding all registered names as [id uint8][nameLen uint8][name bytes].
113
+ * Names are truncated to 255 UTF-8 bytes.
114
+ * Result is cached and only recomputed when new names are registered.
115
+ * @returns {Buffer}
116
+ */
117
+ get encodedKeys () {
118
+ if (this.#encodedKeysCache !== null) return this.#encodedKeysCache
119
+ this.#encodedKeysCache = this.#entryBuffers.length > 0
120
+ ? Buffer.concat(this.#entryBuffers)
121
+ : Buffer.alloc(0)
122
+ return this.#encodedKeysCache
123
+ }
124
+ }
125
+
70
126
  class StatsBucket {
71
127
  constructor () {
72
128
  this._checkpoints = new Map()
73
129
  this._backlogs = new Map()
130
+ /** @type {Buffer[]} Accumulated transaction byte chunks, concatenated lazily. */
131
+ this._transactionChunks = []
74
132
  }
75
133
 
76
134
  get checkpoints () {
@@ -81,6 +139,16 @@ class StatsBucket {
81
139
  return this._backlogs
82
140
  }
83
141
 
142
+ /**
143
+ * Returns the concatenated transaction bytes, or null if no transactions have been added.
144
+ * Concatenation is deferred to read time to avoid O(N²) copies during accumulation.
145
+ * @returns {Buffer | null}
146
+ */
147
+ get transactions () {
148
+ if (this._transactionChunks.length === 0) return null
149
+ return Buffer.concat(this._transactionChunks)
150
+ }
151
+
84
152
  forCheckpoint ({ hash, parentHash, edgeTags }) {
85
153
  let checkpoint = this._checkpoints.get(hash)
86
154
  if (!checkpoint) {
@@ -91,6 +159,14 @@ class StatsBucket {
91
159
  return checkpoint
92
160
  }
93
161
 
162
+ /**
163
+ * Appends pre-encoded transaction bytes to this bucket.
164
+ * @param {Buffer} bytes
165
+ */
166
+ addTransaction (bytes) {
167
+ this._transactionChunks.push(bytes)
168
+ }
169
+
94
170
  /**
95
171
  * Conditionally add a backlog to the bucket. If there is currently an offset
96
172
  * matching the backlog's tags, overwrite the offset IFF the backlog's offset
@@ -151,6 +227,7 @@ class DataStreamsProcessor {
151
227
  this.sequence = 0
152
228
  this.flushInterval = flushInterval
153
229
  this._schemaSamplers = {}
230
+ this._checkpointRegistry = new CheckpointRegistry()
154
231
 
155
232
  if (this.enabled) {
156
233
  this.timer = setInterval(this.onInterval.bind(this), flushInterval)
@@ -162,6 +239,7 @@ class DataStreamsProcessor {
162
239
  onInterval () {
163
240
  const { Stats } = this._serializeBuckets()
164
241
  if (Stats.length === 0) return
242
+
165
243
  const payload = {
166
244
  Env: this.env,
167
245
  Service: this.service,
@@ -171,6 +249,12 @@ class DataStreamsProcessor {
171
249
  Lang: 'javascript',
172
250
  Tags: Object.entries(this.tags).map(([key, value]) => `${key}:${value}`),
173
251
  }
252
+
253
+ // Add ProcessTags only if feature is enabled and process tags exist
254
+ if (propagationHash.isEnabled() && processTags.serialized) {
255
+ payload.ProcessTags = processTags.serialized.split(',')
256
+ }
257
+
174
258
  this.writer.flush(payload)
175
259
  }
176
260
 
@@ -234,7 +318,11 @@ class DataStreamsProcessor {
234
318
  edgeTags
235
319
  )
236
320
  }
237
- const hash = computePathwayHash(this.service, this.env, edgeTags, parentHash)
321
+
322
+ // Get propagation hash if enabled
323
+ const propagationHashValue = propagationHash.isEnabled() ? propagationHash.getHash() : null
324
+
325
+ const hash = computePathwayHash(this.service, this.env, edgeTags, parentHash, propagationHashValue)
238
326
  const edgeLatencyNs = nowNs - edgeStartNs
239
327
  const pathwayLatencyNs = nowNs - pathwayStartNs
240
328
  const dataStreamsContext = {
@@ -281,9 +369,47 @@ class DataStreamsProcessor {
281
369
  this.recordOffset(backlogData)
282
370
  }
283
371
 
372
+ /**
373
+ * Records a transaction ID at a named checkpoint using the binary wire format shared with Go/Java tracers.
374
+ *
375
+ * Wire format per entry: [checkpointId uint8][timestamp int64 big-endian 8 bytes][idLen uint8][id bytes]
376
+ *
377
+ * @param {string} transactionId - Truncated to 255 UTF-8 bytes.
378
+ * @param {string} checkpointName - Mapped to a stable 1-byte ID; silently dropped if registry full.
379
+ * @param {import('../opentelemetry/span').Span|null} [span=null] - Active span to tag with DSM transaction metadata.
380
+ */
381
+ trackTransaction (transactionId, checkpointName, span = null) {
382
+ if (!this.enabled) {
383
+ log.warn('trackTransaction called but DD_DATA_STREAMS_ENABLED is not set. Transaction will not be tracked.')
384
+ return
385
+ }
386
+
387
+ const checkpointId = this._checkpointRegistry.getId(checkpointName)
388
+ if (checkpointId === undefined) return
389
+
390
+ const idBytes = Buffer.from(transactionId, 'utf8').subarray(0, 255)
391
+ // Multiply as BigInt to avoid precision loss past MAX_SAFE_INTEGER
392
+ const timestampNs = BigInt(Date.now()) * 1_000_000n
393
+
394
+ const entry = Buffer.alloc(1 + 8 + 1 + idBytes.length)
395
+ entry.writeUInt8(checkpointId, 0)
396
+ entry.writeBigInt64BE(timestampNs, 1)
397
+ entry.writeUInt8(idBytes.length, 9)
398
+ idBytes.copy(entry, 10)
399
+
400
+ // Number() cast is safe here: 10s bucket granularity tolerates ~0.5ns precision loss
401
+ this.bucketFromTimestamp(Number(timestampNs)).addTransaction(entry)
402
+
403
+ if (span) {
404
+ span.setTag(DSM_TRANSACTION_ID, transactionId)
405
+ span.setTag(DSM_TRANSACTION_CHECKPOINT, checkpointName)
406
+ }
407
+ }
408
+
284
409
  _serializeBuckets () {
285
410
  // TimeBuckets
286
411
  const serializedBuckets = []
412
+ const registrySnapshot = this._checkpointRegistry.encodedKeys
287
413
 
288
414
  for (const [timeNs, bucket] of this.buckets.entries()) {
289
415
  const points = []
@@ -298,12 +424,21 @@ class DataStreamsProcessor {
298
424
  for (const backlog of bucket._backlogs.values()) {
299
425
  backlogs.push(backlog.encode())
300
426
  }
301
- serializedBuckets.push({
427
+
428
+ const serializedBucket = {
302
429
  Start: BigInt(timeNs),
303
430
  Duration: BigInt(this.bucketSizeNs),
304
431
  Stats: points,
305
432
  Backlogs: backlogs,
306
- })
433
+ }
434
+
435
+ const transactions = bucket.transactions
436
+ if (transactions !== null) {
437
+ serializedBucket.Transactions = transactions
438
+ serializedBucket.TransactionCheckpointIds = registrySnapshot
439
+ }
440
+
441
+ serializedBuckets.push(serializedBucket)
307
442
  }
308
443
 
309
444
  this.buckets.clear()
@@ -345,6 +480,7 @@ class DataStreamsProcessor {
345
480
  }
346
481
 
347
482
  module.exports = {
483
+ CheckpointRegistry,
348
484
  DataStreamsProcessor,
349
485
  StatsPoint,
350
486
  StatsBucket,
@@ -0,0 +1,155 @@
1
+ 'use strict'
2
+
3
+ const log = require('../log')
4
+ const { TOP_LEVEL_KEY } = require('../constants')
5
+ const { truncateSpan, normalizeSpan } = require('./tags-processors')
6
+
7
+ /**
8
+ * Formats a span for JSON encoding.
9
+ * @param {object} span - The span to format
10
+ * @param {boolean} isFirstSpan - Whether this is the first span in the trace
11
+ * @returns {object} The formatted span
12
+ */
13
+ function formatSpan (span, isFirstSpan) {
14
+ span = normalizeSpan(truncateSpan(span, false))
15
+
16
+ if (span.span_events) {
17
+ span.meta.events = JSON.stringify(span.span_events)
18
+ delete span.span_events
19
+ }
20
+
21
+ if (isFirstSpan) {
22
+ span.meta['_dd.compute_stats'] = '1'
23
+ }
24
+
25
+ if (span.parent_id?.toString(10) === '0') {
26
+ span.metrics._trace_root = 1
27
+ }
28
+
29
+ if (span.metrics[TOP_LEVEL_KEY]) {
30
+ span.metrics._top_level = 1
31
+ }
32
+
33
+ return span
34
+ }
35
+
36
+ /**
37
+ * Converts a span to JSON-serializable format.
38
+ * IDs are converted to lowercase hex strings. Start time is converted from
39
+ * nanoseconds to seconds for the intake format.
40
+ * @param {object} span - The formatted span
41
+ * @returns {object} JSON-serializable span object
42
+ */
43
+ function spanToJSON (span) {
44
+ const result = {
45
+ trace_id: span.trace_id.toString(16).toLowerCase(),
46
+ span_id: span.span_id.toString(16).toLowerCase(),
47
+ parent_id: span.parent_id.toString(16).toLowerCase(),
48
+ name: span.name,
49
+ resource: span.resource,
50
+ service: span.service,
51
+ error: span.error,
52
+ start: Math.floor(span.start / 1e9),
53
+ duration: span.duration,
54
+ meta: span.meta,
55
+ metrics: span.metrics,
56
+ }
57
+
58
+ if (span.type) {
59
+ result.type = span.type
60
+ }
61
+
62
+ if (span.meta_struct) {
63
+ result.meta_struct = span.meta_struct
64
+ }
65
+
66
+ if (span.links && span.links.length > 0) {
67
+ result.links = span.links
68
+ }
69
+
70
+ return result
71
+ }
72
+
73
+ /**
74
+ * JSON encoder for agentless span intake.
75
+ * Encodes a single trace as JSON with the payload format: {"spans": [...]}
76
+ *
77
+ * This encoder handles one trace at a time since each trace must be sent as a
78
+ * separate request to the intake. -- bengl
79
+ */
80
+ class AgentlessJSONEncoder {
81
+ constructor () {
82
+ this._reset()
83
+ }
84
+
85
+ /**
86
+ * Returns the number of spans encoded.
87
+ * @returns {number}
88
+ */
89
+ count () {
90
+ return this._spanCount
91
+ }
92
+
93
+ /**
94
+ * Encodes a trace (array of spans) into the buffer.
95
+ * @param {object[]} trace - Array of spans to encode
96
+ */
97
+ encode (trace) {
98
+ for (const span of trace) {
99
+ try {
100
+ const formattedSpan = formatSpan(span, this._spanCount === 0)
101
+ const jsonSpan = spanToJSON(formattedSpan)
102
+
103
+ this._spans.push(jsonSpan)
104
+ this._spanCount++
105
+ } catch (err) {
106
+ log.error(
107
+ 'Failed to encode span (name: %s, service: %s). Span will be dropped. Error: %s\n%s',
108
+ span?.name || 'unknown',
109
+ span?.service || 'unknown',
110
+ err.message,
111
+ err.stack
112
+ )
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Creates the JSON payload for the encoded trace.
119
+ * @returns {Buffer} JSON payload as a buffer, or empty buffer if no spans
120
+ */
121
+ makePayload () {
122
+ if (this._spans.length === 0) {
123
+ this._reset()
124
+ return Buffer.alloc(0)
125
+ }
126
+
127
+ try {
128
+ const payload = JSON.stringify({ spans: this._spans })
129
+ this._reset()
130
+ return Buffer.from(payload, 'utf8')
131
+ } catch (err) {
132
+ log.error(
133
+ 'Failed to encode trace as JSON (%d spans). Trace will be dropped. Error: %s',
134
+ this._spans.length,
135
+ err.message
136
+ )
137
+ this._reset()
138
+ return Buffer.alloc(0)
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Resets the encoder state.
144
+ */
145
+ reset () {
146
+ this._reset()
147
+ }
148
+
149
+ _reset () {
150
+ this._spans = []
151
+ this._spanCount = 0
152
+ }
153
+ }
154
+
155
+ 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,13 @@
1
1
  'use strict'
2
2
 
3
- const { inspect } = require('util')
4
-
3
+ const { inspect } = require('node:util')
5
4
  const request = require('../common/request')
6
- const { startupLog } = require('../../startup-log')
5
+ const { logIntegrations, logAgentError } = require('../../startup-log')
7
6
  const runtimeMetrics = require('../../runtime_metrics')
8
7
  const log = require('../../log')
9
8
  const tracerVersion = require('../../../../../package.json').version
10
9
  const BaseWriter = require('../common/writer')
10
+ const propagationHash = require('../../propagation-hash')
11
11
 
12
12
  const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
13
13
 
@@ -29,10 +29,7 @@ class AgentWriter extends BaseWriter {
29
29
  runtimeMetrics.increment(`${METRIC_PREFIX}.requests`, true)
30
30
 
31
31
  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
-
32
+ makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, (err, res, status, headers) => {
36
33
  if (status) {
37
34
  runtimeMetrics.increment(`${METRIC_PREFIX}.responses`, true)
38
35
  runtimeMetrics.increment(`${METRIC_PREFIX}.responses.by.status`, `status:${status}`, true)
@@ -53,6 +50,16 @@ class AgentWriter extends BaseWriter {
53
50
 
54
51
  log.debug('Response from the agent: %s', res)
55
52
 
53
+ // Capture container tags hash from agent response headers
54
+ // The hash is sent by the agent only when Datadog-Container-ID is present in the request
55
+ // (Datadog-Container-ID is automatically injected by docker.inject() in exporters/common/request.js)
56
+ if (headers) {
57
+ const containerTagsHash = headers['Datadog-Container-Tags-Hash']
58
+ if (containerTagsHash) {
59
+ propagationHash.updateContainerTagsHash(containerTagsHash)
60
+ }
61
+ }
62
+
56
63
  try {
57
64
  this._prioritySampler.update(JSON.parse(res).rate_by_service)
58
65
  } catch (e) {
@@ -91,7 +98,13 @@ function makeRequest (version, data, count, url, headers, lookup, cb) {
91
98
 
92
99
  log.debug('Request to the agent: %j', options)
93
100
 
94
- request(data, options, cb)
101
+ request(data, options, (err, res, status, headers) => {
102
+ logIntegrations()
103
+ if (status !== 404 && status !== 200 && err) {
104
+ logAgentError({ status, message: err.message ?? inspect(err) })
105
+ }
106
+ cb(err, res, status, headers)
107
+ })
95
108
  }
96
109
 
97
110
  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