dd-trace 6.9.0 → 6.10.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 (59) hide show
  1. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  2. package/index.d.ts +42 -0
  3. package/package.json +4 -3
  4. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  5. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
  6. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  10. package/packages/datadog-instrumentations/src/jest.js +1 -1
  11. package/packages/datadog-instrumentations/src/mocha/main.js +1 -1
  12. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -8
  13. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  14. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  15. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  16. package/packages/datadog-instrumentations/src/pg.js +116 -92
  17. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  18. package/packages/datadog-instrumentations/src/vitest-main.js +169 -17
  19. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  21. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  22. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  23. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
  24. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  25. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +1 -1
  26. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  27. package/packages/datadog-plugin-jest/src/index.js +5 -4
  28. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  29. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  30. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  31. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  32. package/packages/datadog-plugin-mocha/src/index.js +550 -39
  33. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  34. package/packages/datadog-plugin-next/src/index.js +1 -19
  35. package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
  36. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  37. package/packages/datadog-plugin-pg/src/index.js +1 -0
  38. package/packages/datadog-plugin-vitest/src/index.js +8 -20
  39. package/packages/datadog-plugin-ws/src/util.js +2 -1
  40. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  41. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  42. package/packages/dd-trace/src/carrier.js +356 -0
  43. package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
  44. package/packages/dd-trace/src/config/supported-configurations.json +10 -0
  45. package/packages/dd-trace/src/datastreams/context.js +6 -1
  46. package/packages/dd-trace/src/datastreams/manager.js +5 -1
  47. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  48. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  49. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  50. package/packages/dd-trace/src/llmobs/index.js +34 -2
  51. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  52. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  53. package/packages/dd-trace/src/llmobs/util.js +78 -0
  54. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  55. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  56. package/packages/dd-trace/src/plugin_manager.js +2 -0
  57. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +1 -62
  58. package/packages/dd-trace/src/plugins/util/test.js +84 -5
  59. package/packages/dd-trace/src/plugins/util/web.js +53 -2
@@ -0,0 +1,356 @@
1
+ 'use strict'
2
+
3
+ /** @typedef {Record<string, unknown>} Carrier */
4
+ /** @typedef {string | Buffer} PathwayValue */
5
+
6
+ const FIELD_READ = 1
7
+ const FIELD_WRITE = 2
8
+ const FIELD_HAS = 4
9
+ const FIELD_DELETE = 8
10
+ const FIELD_NAME_LENGTH = 16
11
+
12
+ /** @type {Record<string, unknown>} */
13
+ const carrierOperations = {}
14
+
15
+ /**
16
+ * @typedef {object} CarrierCapability
17
+ * @property {unknown} [read]
18
+ * @property {unknown} [write]
19
+ * @property {unknown} [has]
20
+ * @property {unknown} [delete]
21
+ * @property {unknown} [nameLength]
22
+ */
23
+
24
+ /**
25
+ * @template Value
26
+ * @typedef {object} CarrierField
27
+ * @property {(carrier: Carrier) => Value | undefined} read
28
+ * @property {(carrier: Carrier, value: Value) => void} write
29
+ * @property {(carrier: Carrier) => boolean} has
30
+ * @property {(carrier: Carrier) => void} delete
31
+ * @property {(carrier: Carrier, target: Carrier) => void} copy
32
+ * @property {number} nameLength
33
+ */
34
+
35
+ /**
36
+ * @template {CarrierCapability} Capability
37
+ * @param {string} name
38
+ * @param {number} operations
39
+ * @param {Capability} capability
40
+ * @returns {Capability}
41
+ */
42
+ function defineCapability (name, operations, capability) {
43
+ const suffix = name[0].toUpperCase() + name.slice(1)
44
+ if (operations & FIELD_READ) carrierOperations[`read${suffix}`] = capability.read
45
+ if (operations & FIELD_WRITE) carrierOperations[`write${suffix}`] = capability.write
46
+ if (operations & FIELD_HAS) carrierOperations[`has${suffix}`] = capability.has
47
+ if (operations & FIELD_DELETE) carrierOperations[`delete${suffix}`] = capability.delete
48
+ if (operations & FIELD_NAME_LENGTH) carrierOperations[`${name}NameLength`] = capability.nameLength
49
+ return capability
50
+ }
51
+
52
+ /**
53
+ * @template Value
54
+ * @param {string} fieldName
55
+ * @param {string} headerName
56
+ * @param {(value: unknown) => Value | undefined} resolve
57
+ * @param {number} operations
58
+ * @returns {CarrierField<Value>}
59
+ */
60
+ function defineField (fieldName, headerName, resolve, operations) {
61
+ return defineCapability(fieldName, operations, {
62
+ /**
63
+ * @param {Carrier} carrier
64
+ * @returns {Value | undefined}
65
+ */
66
+ read (carrier) {
67
+ return resolve(carrier[headerName])
68
+ },
69
+
70
+ /**
71
+ * @param {Carrier} carrier
72
+ * @param {Value} value
73
+ * @returns {void}
74
+ */
75
+ write (carrier, value) {
76
+ carrier[headerName] = value
77
+ },
78
+
79
+ /**
80
+ * @param {Carrier} carrier
81
+ * @returns {boolean}
82
+ */
83
+ has (carrier) {
84
+ return headerName in carrier
85
+ },
86
+
87
+ /**
88
+ * @param {Carrier} carrier
89
+ * @returns {void}
90
+ */
91
+ delete (carrier) {
92
+ delete carrier[headerName]
93
+ },
94
+
95
+ /**
96
+ * @param {Carrier} carrier
97
+ * @param {Carrier} target
98
+ * @returns {void}
99
+ */
100
+ copy (carrier, target) {
101
+ if (Object.hasOwn(carrier, headerName)) target[headerName] = carrier[headerName]
102
+ },
103
+
104
+ nameLength: headerName.length,
105
+ })
106
+ }
107
+
108
+ /**
109
+ * Skipping non-strings keeps a Symbol or a throwing `toString` from aborting
110
+ * extraction, which discards the whole context rather than one field.
111
+ *
112
+ * @param {unknown} value
113
+ * @returns {string | undefined}
114
+ */
115
+ function readListText (value) {
116
+ if (typeof value === 'string') return value
117
+ if (!Array.isArray(value)) return
118
+
119
+ let joined = ''
120
+ let first = true
121
+
122
+ for (const item of value) {
123
+ if (typeof item !== 'string') continue
124
+ if (first) {
125
+ joined = item
126
+ first = false
127
+ } else {
128
+ joined += `,${item}`
129
+ }
130
+ }
131
+
132
+ return joined
133
+ }
134
+
135
+ /**
136
+ * @param {unknown} value
137
+ * @returns {string | undefined}
138
+ */
139
+ function readLastText (value) {
140
+ if (typeof value === 'string') return value
141
+ if (!Array.isArray(value)) return
142
+
143
+ let result
144
+ for (let i = value.length - 1; i >= 0; i--) {
145
+ if (typeof value[i] === 'string') {
146
+ result = value[i]
147
+ break
148
+ }
149
+ }
150
+ return result
151
+ }
152
+
153
+ /**
154
+ * @param {unknown} value
155
+ * @returns {string | undefined}
156
+ */
157
+ function readUniqueText (value) {
158
+ let result
159
+ if (typeof value === 'string') {
160
+ result = value
161
+ } else if (Array.isArray(value) && value.length === 1 && typeof value[0] === 'string') {
162
+ result = value[0]
163
+ }
164
+ return result
165
+ }
166
+
167
+ /**
168
+ * @param {unknown} value
169
+ * @returns {PathwayValue | undefined}
170
+ */
171
+ function readLastPathway (value) {
172
+ if (typeof value === 'string' || Buffer.isBuffer(value)) return value
173
+ if (!Array.isArray(value)) return
174
+
175
+ let result
176
+ for (let i = value.length - 1; i >= 0; i--) {
177
+ const item = value[i]
178
+ if (typeof item === 'string' || Buffer.isBuffer(item)) {
179
+ result = item
180
+ break
181
+ }
182
+ }
183
+ return result
184
+ }
185
+
186
+ const datadogTraceId = defineField(
187
+ 'datadogTraceId',
188
+ 'x-datadog-trace-id',
189
+ readLastText,
190
+ FIELD_READ | FIELD_WRITE
191
+ )
192
+ const datadogParentId = defineField(
193
+ 'datadogParentId',
194
+ 'x-datadog-parent-id',
195
+ readLastText,
196
+ FIELD_READ | FIELD_WRITE | FIELD_DELETE
197
+ )
198
+ const datadogOrigin = defineField(
199
+ 'datadogOrigin',
200
+ 'x-datadog-origin',
201
+ readLastText,
202
+ FIELD_READ | FIELD_WRITE
203
+ )
204
+ const datadogSamplingPriority = defineField(
205
+ 'datadogSamplingPriority',
206
+ 'x-datadog-sampling-priority',
207
+ readLastText,
208
+ FIELD_READ | FIELD_WRITE
209
+ )
210
+ defineField('datadogTags', 'x-datadog-tags', readListText, FIELD_READ | FIELD_WRITE)
211
+ defineField('baggage', 'baggage', readListText, FIELD_READ | FIELD_WRITE)
212
+ const b3TraceId = defineField('b3TraceId', 'x-b3-traceid', readLastText, FIELD_READ | FIELD_WRITE)
213
+ const b3SpanId = defineField('b3SpanId', 'x-b3-spanid', readLastText, FIELD_READ | FIELD_WRITE)
214
+ const b3ParentId = defineField('b3ParentId', 'x-b3-parentspanid', readLastText, FIELD_WRITE)
215
+ const b3Sampled = defineField('b3Sampled', 'x-b3-sampled', readLastText, FIELD_READ | FIELD_WRITE)
216
+ const b3Flags = defineField('b3Flags', 'x-b3-flags', readLastText, FIELD_READ | FIELD_WRITE)
217
+ const b3 = defineField('b3', 'b3', readLastText, FIELD_READ | FIELD_WRITE)
218
+ defineField('sqsd', 'x-aws-sqsd-attr-_datadog', readLastText, FIELD_READ)
219
+ const traceparent = defineField('traceparent', 'traceparent', readUniqueText, FIELD_READ | FIELD_WRITE)
220
+ const tracestate = defineField('tracestate', 'tracestate', readListText, FIELD_READ | FIELD_WRITE)
221
+ const dsmBase64 = defineField(
222
+ 'dsmBase64',
223
+ 'dd-pathway-ctx-base64',
224
+ readLastPathway,
225
+ FIELD_READ | FIELD_WRITE | FIELD_HAS | FIELD_NAME_LENGTH
226
+ )
227
+ const dsmBinary = defineField('dsmBinary', 'dd-pathway-ctx', readLastPathway, FIELD_READ | FIELD_HAS)
228
+
229
+ const legacyBaggagePrefix = 'ot-baggage-'
230
+ // RFC 7230 `token`, compatible with Node.js header-name validation.
231
+ const httpHeaderNameExpr = /^[0-9A-Za-z!#$%&'*+\-.^_`|~]+$/
232
+
233
+ defineCapability('legacyBaggage', FIELD_READ | FIELD_WRITE, {
234
+ /**
235
+ * @param {Record<string, string> | undefined} carrier
236
+ * @param {string} key
237
+ * @param {string} value
238
+ * @returns {Record<string, string> | undefined}
239
+ */
240
+ write (carrier, key, value) {
241
+ const name = legacyBaggagePrefix + key
242
+ if (!httpHeaderNameExpr.test(name)) return
243
+
244
+ carrier ??= {}
245
+ carrier[name] = value
246
+ return carrier
247
+ },
248
+
249
+ /**
250
+ * @param {Carrier} carrier
251
+ * @param {Record<string, string>} target
252
+ * @returns {void}
253
+ */
254
+ read (carrier, target) {
255
+ for (const name of Object.keys(carrier)) {
256
+ if (!name.startsWith(legacyBaggagePrefix)) continue
257
+ const key = name.slice(legacyBaggagePrefix.length)
258
+ if (!key) continue
259
+ const value = readLastText(carrier[name])
260
+ if (value !== undefined) target[key] = value
261
+ }
262
+ },
263
+ })
264
+
265
+ const textMapLogFields = [
266
+ datadogTraceId,
267
+ datadogParentId,
268
+ datadogSamplingPriority,
269
+ datadogOrigin,
270
+ b3TraceId,
271
+ b3SpanId,
272
+ b3ParentId,
273
+ b3Sampled,
274
+ b3Flags,
275
+ b3,
276
+ traceparent,
277
+ tracestate,
278
+ ]
279
+ const dsmLogFields = [dsmBinary, dsmBase64]
280
+
281
+ /**
282
+ * @param {Carrier} carrier
283
+ * @param {Array<Pick<CarrierField<unknown>, 'copy'>>} selectedFields
284
+ * @returns {Carrier}
285
+ */
286
+ function pick (carrier, selectedFields) {
287
+ /** @type {Carrier} */
288
+ const result = {}
289
+ for (const field of selectedFields) field.copy(carrier, result)
290
+ return result
291
+ }
292
+
293
+ /**
294
+ * @param {Carrier} carrier
295
+ * @returns {Carrier}
296
+ */
297
+ function pickTextMap (carrier) {
298
+ return pick(carrier, textMapLogFields)
299
+ }
300
+
301
+ /**
302
+ * @param {Carrier} carrier
303
+ * @returns {Carrier}
304
+ */
305
+ function pickDsm (carrier) {
306
+ return pick(carrier, dsmLogFields)
307
+ }
308
+
309
+ /**
310
+ * @typedef {object} CarrierOperations
311
+ * @property {typeof datadogParentId.delete} deleteDatadogParentId
312
+ * @property {typeof dsmBase64.nameLength} dsmBase64NameLength
313
+ * @property {typeof dsmBase64.has} hasDsmBase64
314
+ * @property {typeof dsmBinary.has} hasDsmBinary
315
+ * @property {typeof pickDsm} pickDsm
316
+ * @property {typeof pickTextMap} pickTextMap
317
+ * @property {(carrier: Carrier) => string | undefined} readBaggage
318
+ * @property {typeof b3.read} readB3
319
+ * @property {typeof b3Flags.read} readB3Flags
320
+ * @property {typeof b3Sampled.read} readB3Sampled
321
+ * @property {typeof b3SpanId.read} readB3SpanId
322
+ * @property {typeof b3TraceId.read} readB3TraceId
323
+ * @property {typeof datadogOrigin.read} readDatadogOrigin
324
+ * @property {typeof datadogParentId.read} readDatadogParentId
325
+ * @property {typeof datadogSamplingPriority.read} readDatadogSamplingPriority
326
+ * @property {(carrier: Carrier) => string | undefined} readDatadogTags
327
+ * @property {typeof datadogTraceId.read} readDatadogTraceId
328
+ * @property {typeof dsmBase64.read} readDsmBase64
329
+ * @property {typeof dsmBinary.read} readDsmBinary
330
+ * @property {(carrier: Carrier, target: Record<string, string>) => void} readLegacyBaggage
331
+ * @property {(carrier: Carrier) => string | undefined} readSqsd
332
+ * @property {typeof traceparent.read} readTraceparent
333
+ * @property {typeof tracestate.read} readTracestate
334
+ * @property {(carrier: Carrier, value: string) => void} writeBaggage
335
+ * @property {typeof b3.write} writeB3
336
+ * @property {typeof b3Flags.write} writeB3Flags
337
+ * @property {typeof b3ParentId.write} writeB3ParentId
338
+ * @property {typeof b3Sampled.write} writeB3Sampled
339
+ * @property {typeof b3SpanId.write} writeB3SpanId
340
+ * @property {typeof b3TraceId.write} writeB3TraceId
341
+ * @property {typeof datadogOrigin.write} writeDatadogOrigin
342
+ * @property {typeof datadogParentId.write} writeDatadogParentId
343
+ * @property {typeof datadogSamplingPriority.write} writeDatadogSamplingPriority
344
+ * @property {(carrier: Carrier, value: string) => void} writeDatadogTags
345
+ * @property {typeof datadogTraceId.write} writeDatadogTraceId
346
+ * @property {typeof dsmBase64.write} writeDsmBase64
347
+ * @property {(carrier: Record<string, string> | undefined, key: string, value: string) =>
348
+ * Record<string, string> | undefined} writeLegacyBaggage
349
+ * @property {typeof traceparent.write} writeTraceparent
350
+ * @property {typeof tracestate.write} writeTracestate
351
+ */
352
+
353
+ carrierOperations.pickDsm = pickDsm
354
+ carrierOperations.pickTextMap = pickTextMap
355
+
356
+ module.exports = /** @type {CarrierOperations} */ (carrierOperations)
@@ -272,6 +272,7 @@ export interface GeneratedConfig {
272
272
  DD_TRACE_HAPI_HAPI_ENABLED: boolean;
273
273
  DD_TRACE_HONO_ENABLED: boolean;
274
274
  DD_TRACE_HTTP_ENABLED: boolean;
275
+ DD_TRACE_HTTP_SERVER_ERROR_STATUSES: string;
275
276
  DD_TRACE_HTTP2_ENABLED: boolean;
276
277
  DD_TRACE_HTTPS_ENABLED: boolean;
277
278
  DD_TRACE_IOREDIS_ENABLED: boolean;
@@ -727,6 +728,7 @@ export interface GeneratedEnvVarConfig {
727
728
  DD_HEAP_SNAPSHOT_COUNT: number;
728
729
  DD_HEAP_SNAPSHOT_DESTINATION: string;
729
730
  DD_HEAP_SNAPSHOT_INTERVAL: number;
731
+ DD_HTTP_SERVER_ERROR_STATUSES: string;
730
732
  DD_IAST_DB_ROWS_TO_TAINT: number;
731
733
  DD_IAST_DEDUPLICATION_ENABLED: boolean;
732
734
  DD_IAST_ENABLED: boolean;
@@ -966,6 +968,7 @@ export interface GeneratedEnvVarConfig {
966
968
  DD_TRACE_HEADER_TAGS: string[];
967
969
  DD_TRACE_HONO_ENABLED: boolean;
968
970
  DD_TRACE_HTTP_ENABLED: boolean;
971
+ DD_TRACE_HTTP_SERVER_ERROR_STATUSES: string;
969
972
  DD_TRACE_HTTP2_ENABLED: boolean;
970
973
  DD_TRACE_HTTPS_ENABLED: boolean;
971
974
  DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED: boolean;
@@ -2999,6 +2999,16 @@
2999
2999
  "default": "true"
3000
3000
  }
3001
3001
  ],
3002
+ "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": [
3003
+ {
3004
+ "implementation": "A",
3005
+ "type": "string",
3006
+ "default": "500-599",
3007
+ "aliases": [
3008
+ "DD_HTTP_SERVER_ERROR_STATUSES"
3009
+ ]
3010
+ }
3011
+ ],
3002
3012
  "DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED": [
3003
3013
  {
3004
3014
  "implementation": "A",
@@ -13,7 +13,12 @@ function getDataStreamsContext () {
13
13
  function setDataStreamsContext (dataStreamsContext) {
14
14
  log.debug('Setting new DSM Context: %j.', dataStreamsContext)
15
15
 
16
- if (dataStreamsContext) legacyStorage.enterWith({ ...legacyStorage.getStore(), dataStreamsContext })
16
+ // `undefined` clears: a message that carries no context must not inherit the previous message's.
17
+ // The identity check keeps the common no-context case from allocating a store per message.
18
+ const store = legacyStorage.getStore()
19
+ if (store?.dataStreamsContext !== dataStreamsContext) {
20
+ legacyStorage.enterWith({ ...store, dataStreamsContext })
21
+ }
17
22
  }
18
23
 
19
24
  module.exports = {
@@ -16,9 +16,13 @@ class DataStreamsManager {
16
16
  return ctx
17
17
  }
18
18
 
19
+ /**
20
+ * @param {Record<string, string>|undefined} carrier Pass every message's carrier, including the
21
+ * ones without a context: that is what starts a new pathway instead of extending the previous
22
+ * message's.
23
+ */
19
24
  decodeDataStreamsContext (carrier) {
20
25
  const ctx = DsmPathwayCodec.decode(carrier)
21
- // we erase the previous context everytime we decode a new one
22
26
  DataStreamsContext.setDataStreamsContext(ctx)
23
27
  return ctx
24
28
  }
@@ -5,23 +5,25 @@
5
5
  // this inconsistency is ok because hashes do not need to be consistent across services
6
6
  const crypto = require('crypto')
7
7
  const { LRUCache } = require('../../../../vendor/dist/lru-cache')
8
+ const {
9
+ hasDsmBase64,
10
+ hasDsmBinary,
11
+ pickDsm,
12
+ readDsmBase64,
13
+ readDsmBinary,
14
+ writeDsmBase64,
15
+ } = require('../carrier')
8
16
  const log = require('../log')
9
- const pick = require('../../../datadog-core/src/utils/src/pick')
10
17
  const { encodeVarintInto, decodeVarint } = require('./encoding')
11
18
 
12
19
  const cache = new LRUCache({ max: 500 })
13
20
 
14
- const CONTEXT_PROPAGATION_KEY = 'dd-pathway-ctx'
15
- const CONTEXT_PROPAGATION_KEY_BASE64 = 'dd-pathway-ctx-base64'
16
-
17
21
  const PATHWAY_CONTEXT_BYTES = 20
18
22
 
19
23
  // Reused across `encodePathwayContext` calls; the buffer is fully rewritten before each
20
24
  // `Buffer.from(...)` copy-out so callers never observe mutation between checkpoints.
21
25
  const pathwayScratch = Buffer.allocUnsafe(PATHWAY_CONTEXT_BYTES)
22
26
 
23
- const logKeys = [CONTEXT_PROPAGATION_KEY, CONTEXT_PROPAGATION_KEY_BASE64]
24
-
25
27
  function shaHash (checkpointString) {
26
28
  // Copy out of the 32-byte digest so the LRU cache doesn't retain it.
27
29
  return Buffer.from(crypto.createHash('sha256').update(checkpointString).digest().subarray(0, 8))
@@ -114,7 +116,7 @@ function decodePathwayContext (pathwayContext) {
114
116
  }
115
117
 
116
118
  /**
117
- * @param {string} pathwayContext
119
+ * @param {string | Buffer} pathwayContext
118
120
  * @returns {ReturnType<typeof decodePathwayContext>|undefined}
119
121
  */
120
122
  function decodePathwayContextBase64 (pathwayContext) {
@@ -142,10 +144,10 @@ const DsmPathwayCodec = {
142
144
  encode (dataStreamsContext, carrier) {
143
145
  if (!dataStreamsContext || !dataStreamsContext.hash) return
144
146
  carrier ??= {}
145
- carrier[CONTEXT_PROPAGATION_KEY_BASE64] = encodePathwayContextBase64(dataStreamsContext)
147
+ writeDsmBase64(carrier, encodePathwayContextBase64(dataStreamsContext))
146
148
 
147
149
  // eslint-disable-next-line eslint-rules/eslint-log-printf-style
148
- log.debug(() => `Injected into DSM carrier: ${JSON.stringify(pick(carrier, logKeys))}.`)
150
+ log.debug(() => `Injected into DSM carrier: ${JSON.stringify(pickDsm(carrier))}.`)
149
151
 
150
152
  return carrier
151
153
  },
@@ -155,25 +157,25 @@ const DsmPathwayCodec = {
155
157
  * @returns {ReturnType<typeof decodePathwayContext>|undefined}
156
158
  */
157
159
  decode (carrier) {
158
- // eslint-disable-next-line eslint-rules/eslint-log-printf-style
159
- log.debug(() => `Attempting extract from DSM carrier: ${JSON.stringify(pick(carrier, logKeys))}.`)
160
-
161
160
  if (carrier == null) return
162
161
 
162
+ // eslint-disable-next-line eslint-rules/eslint-log-printf-style
163
+ log.debug(() => `Attempting extract from DSM carrier: ${JSON.stringify(pickDsm(carrier))}.`)
164
+
163
165
  let ctx
164
- if (CONTEXT_PROPAGATION_KEY_BASE64 in carrier) {
166
+ if (hasDsmBase64(carrier)) {
165
167
  // decode v2 encoding of base64
166
- ctx = decodePathwayContextBase64(carrier[CONTEXT_PROPAGATION_KEY_BASE64])
167
- } else if (CONTEXT_PROPAGATION_KEY in carrier) {
168
- try {
168
+ const encoded = readDsmBase64(carrier)
169
+ if (encoded !== undefined) ctx = decodePathwayContextBase64(encoded)
170
+ } else if (hasDsmBinary(carrier)) {
171
+ const encoded = readDsmBinary(carrier)
172
+ if (Buffer.isBuffer(encoded)) {
169
173
  // decode v1 encoding
170
- ctx = decodePathwayContext(carrier[CONTEXT_PROPAGATION_KEY])
171
- } catch {
172
- // pass
174
+ ctx = decodePathwayContext(encoded)
173
175
  }
174
176
  // cover case where base64 context was received under wrong key
175
- if (!ctx && CONTEXT_PROPAGATION_KEY in carrier) {
176
- ctx = decodePathwayContextBase64(carrier[CONTEXT_PROPAGATION_KEY])
177
+ if (!ctx && encoded !== undefined) {
178
+ ctx = decodePathwayContextBase64(encoded)
177
179
  }
178
180
  }
179
181
 
@@ -182,7 +184,6 @@ const DsmPathwayCodec = {
182
184
  }
183
185
 
184
186
  module.exports = {
185
- CONTEXT_PROPAGATION_KEY_BASE64,
186
187
  computePathwayHash: computeHash,
187
188
  encodePathwayContext,
188
189
  decodePathwayContext,
@@ -5,10 +5,11 @@ const pkg = require('../../../../package.json')
5
5
 
6
6
  const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js')
7
7
  const { PATHWAY_HASH, DSM_TRANSACTION_ID, DSM_TRANSACTION_CHECKPOINT } = require('../../../../ext/tags')
8
+ const { dsmBase64NameLength } = require('../carrier')
8
9
  const log = require('../log')
9
10
  const processTags = require('../process-tags')
10
11
  const propagationHash = require('../propagation-hash')
11
- const { CONTEXT_PROPAGATION_KEY_BASE64, computePathwayHash } = require('./pathway')
12
+ const { computePathwayHash } = require('./pathway')
12
13
  const { DataStreamsWriter } = require('./writer')
13
14
  const { getAmqpMessageSize, getHeadersSize, getMessageSize, getSizeOrZero } = require('./size')
14
15
  const { SchemaBuilder } = require('./schemas/schema_builder')
@@ -21,7 +22,7 @@ const ENTRY_PARENT_HASH = Buffer.from('0000000000000000', 'hex')
21
22
  // bytes, encoded as 28 base64 chars; together with the header key and
22
23
  // JSON framing (matching the prior `JSON.stringify({key: value})` byte
23
24
  // count minus 1), this is a fixed value.
24
- const PATHWAY_HEADER_BYTES = CONTEXT_PROPAGATION_KEY_BASE64.length + 28 + 6
25
+ const PATHWAY_HEADER_BYTES = dsmBase64NameLength + 28 + 6
25
26
 
26
27
  class StatsPoint {
27
28
  constructor (hash, parentHash, edgeTags) {
@@ -16,6 +16,11 @@ module.exports = {
16
16
  PROPAGATED_ML_APP_KEY: '_dd.p.llmobs_ml_app',
17
17
  PROPAGATED_SESSION_ID_KEY: '_dd.p.llmobs_sid',
18
18
  PARENT_ID_KEY: '_ml_obs.llmobs_parent_id',
19
+
20
+ PARENT_AGENT_NAME: '_ml_obs.parent_agent_name',
21
+ PARENT_AGENT_SPAN_ID: '_ml_obs.parent_agent_span_id',
22
+ PROPAGATED_PARENT_AGENT_ID_KEY: '_dd.p.llmobs_pagent_span_id',
23
+ PROPAGATED_PARENT_AGENT_NAME_KEY: '_dd.p.llmobs_pagent_name',
19
24
  PROPAGATED_SAMPLE_RATE_KEY: '_dd.p.llmobs_sr',
20
25
  PROPAGATED_SAMPLING_DECISION_KEY: '_dd.p.llmobs_sd',
21
26
  SAMPLE_RATE: '_ml_obs.sample_rate',
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
+ const { readDatadogTags, writeDatadogTags } = require('../carrier')
5
6
  const log = require('../log')
6
7
  const { DD_MAJOR } = require('../../../../version')
7
8
  const startupLogs = require('../startup-log')
@@ -12,6 +13,8 @@ const {
12
13
  PROPAGATED_ML_APP_KEY,
13
14
  PROPAGATED_PARENT_ID_KEY,
14
15
  PROPAGATED_SESSION_ID_KEY,
16
+ PROPAGATED_PARENT_AGENT_ID_KEY,
17
+ PROPAGATED_PARENT_AGENT_NAME_KEY,
15
18
  SAMPLE_RATE,
16
19
  SAMPLING_DECISION,
17
20
  PROPAGATED_SAMPLE_RATE_KEY,
@@ -20,6 +23,7 @@ const {
20
23
  PROPAGATED_TRACE_ID_KEY,
21
24
  } = require('./constants/tags')
22
25
  const { storage } = require('./storage')
26
+ const { agentNameWireSafe, appendOptionalPropagatedTag, resolveAgentAttribution, stripTagsetEntry } = require('./util')
23
27
  const telemetry = require('./telemetry')
24
28
  const LLMObsSpanProcessor = require('./span_processor')
25
29
  const LLMObsEvalMetricsWriter = require('./writers/evaluations')
@@ -147,9 +151,17 @@ function handleLLMObsInjection ({ carrier }) {
147
151
 
148
152
  if (!parentId && !mlApp && samplingDecision == null && !sessionId && !propagatedTraceId) return
149
153
 
154
+ // Propagate the nearest agent so spans in the downstream process attribute correctly. When the
155
+ // active span sits under a distributed agent, `resolveAgentAttribution` inherits the propagated
156
+ // id/name already on the parent's registry entry, so the chain survives multiple hops. Resolved
157
+ // after the bail-out above so we don't allocate when there is nothing to inject.
158
+ const { name: parentAgentName, spanId: parentAgentSpanId } = resolveAgentAttribution(
159
+ mlObsSpanTags, parent
160
+ )
161
+
150
162
  // `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*`
151
163
  // tags, so it may be undefined here — coalesce before appending.
152
- const existing = carrier['x-datadog-tags']
164
+ const existing = readDatadogTags(carrier)
153
165
  let tags = existing || ''
154
166
  if (parentId) tags += `${tags ? ',' : ''}${PROPAGATED_PARENT_ID_KEY}=${parentId}`
155
167
  if (mlApp) tags += `${tags ? ',' : ''}${PROPAGATED_ML_APP_KEY}=${mlApp}`
@@ -157,7 +169,27 @@ function handleLLMObsInjection ({ carrier }) {
157
169
  if (sampleRate != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLE_RATE_KEY}=${sampleRate}`
158
170
  if (samplingDecision != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLING_DECISION_KEY}=${samplingDecision}`
159
171
  if (propagatedTraceId != null) tags += `${tags ? ',' : ''}${PROPAGATED_TRACE_ID_KEY}=${propagatedTraceId}`
160
- if (tags !== existing) carrier['x-datadog-tags'] = tags
172
+ // When a local agent attribution is resolved, strip any stale upstream pagent entries that
173
+ // `_injectTags` may have already written into the carrier (it propagates all `_dd.p.*` from
174
+ // `_trace.tags`). This ensures the downstream sees a consistent id-only or id+name pair
175
+ // rather than a mix from different hops. The id is always digit-safe; an unsafe name is wiped.
176
+ if (parentAgentSpanId) {
177
+ tags = stripTagsetEntry(tags, PROPAGATED_PARENT_AGENT_ID_KEY)
178
+ tags = stripTagsetEntry(tags, PROPAGATED_PARENT_AGENT_NAME_KEY)
179
+ }
180
+ const maxLength = globalTracerConfig.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH
181
+ const tagsWithId = appendOptionalPropagatedTag(
182
+ tags, PROPAGATED_PARENT_AGENT_ID_KEY, parentAgentSpanId, null, maxLength
183
+ )
184
+ // Only append the name when the id fit: a name without an id is unresolvable by the backend.
185
+ if (tagsWithId === tags) {
186
+ tags = tagsWithId
187
+ } else {
188
+ tags = appendOptionalPropagatedTag(
189
+ tagsWithId, PROPAGATED_PARENT_AGENT_NAME_KEY, parentAgentName, agentNameWireSafe, maxLength
190
+ )
191
+ }
192
+ if (tags !== existing) writeDatadogTags(carrier, tags)
161
193
  }
162
194
 
163
195
  function handleFlush () {
@@ -27,6 +27,8 @@ const {
27
27
  ML_APP,
28
28
  TAGS,
29
29
  PARENT_ID_KEY,
30
+ PARENT_AGENT_NAME,
31
+ PARENT_AGENT_SPAN_ID,
30
32
  SESSION_ID,
31
33
  NAME,
32
34
  INPUT_PROMPT,
@@ -157,6 +159,20 @@ class LLMObsSpanProcessor {
157
159
  this.#addObject(mlObsTags[TOOL_DEFINITIONS], meta.tool_definitions)
158
160
  }
159
161
 
162
+ // Surface the agent attribution resolved at registration, but only on spans that actually
163
+ // have an agent ancestor. The id is always present in that case; the name may be missing when
164
+ // it arrived id-only over distributed propagation (older/unsafe upstream). Emit the name as
165
+ // explicit `null` then, matching the cross-language wire shape (dd-trace-py sends null, not an
166
+ // absent key) so the shared backend and system-tests see the same payload.
167
+ const parentAgentName = mlObsTags[PARENT_AGENT_NAME]
168
+ const parentAgentSpanId = mlObsTags[PARENT_AGENT_SPAN_ID]
169
+ if (parentAgentName != null || parentAgentSpanId != null) {
170
+ meta.agent_attribution = {
171
+ pagent_name: parentAgentName ?? null,
172
+ pagent_span_id: parentAgentSpanId,
173
+ }
174
+ }
175
+
160
176
  const llmObsSpan = new LLMObservabilitySpan(spanKind)
161
177
 
162
178
  if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {