dd-trace 5.99.1 → 5.101.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 (101) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/index.d.ts +14 -0
  3. package/package.json +8 -8
  4. package/packages/datadog-instrumentations/src/cucumber.js +69 -5
  5. package/packages/datadog-instrumentations/src/cypress.js +5 -3
  6. package/packages/datadog-instrumentations/src/express.js +3 -2
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  8. package/packages/datadog-instrumentations/src/hono.js +15 -4
  9. package/packages/datadog-instrumentations/src/http/client.js +20 -3
  10. package/packages/datadog-instrumentations/src/jest.js +146 -90
  11. package/packages/datadog-instrumentations/src/mocha/common.js +4 -1
  12. package/packages/datadog-instrumentations/src/mocha/main.js +43 -26
  13. package/packages/datadog-instrumentations/src/mocha/utils.js +114 -96
  14. package/packages/datadog-instrumentations/src/mocha/worker.js +7 -4
  15. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
  16. package/packages/datadog-instrumentations/src/path-to-regexp.js +44 -0
  17. package/packages/datadog-instrumentations/src/playwright.js +108 -18
  18. package/packages/datadog-instrumentations/src/router.js +53 -33
  19. package/packages/datadog-instrumentations/src/vitest.js +76 -30
  20. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -1
  21. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
  22. package/packages/datadog-plugin-bullmq/src/consumer.js +5 -4
  23. package/packages/datadog-plugin-bullmq/src/producer.js +37 -29
  24. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +49 -9
  25. package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
  26. package/packages/datadog-plugin-cypress/src/support.js +22 -21
  27. package/packages/datadog-plugin-grpc/src/client.js +1 -1
  28. package/packages/datadog-plugin-grpc/src/server.js +1 -1
  29. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
  30. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
  31. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -3
  32. package/packages/datadog-plugin-playwright/src/index.js +6 -0
  33. package/packages/datadog-plugin-router/src/index.js +13 -0
  34. package/packages/dd-trace/index.js +4 -3
  35. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  36. package/packages/dd-trace/src/appsec/reporter.js +4 -1
  37. package/packages/dd-trace/src/baggage.js +10 -0
  38. package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
  39. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
  40. package/packages/dd-trace/src/config/config-types.d.ts +0 -2
  41. package/packages/dd-trace/src/config/generated-config-types.d.ts +17 -41
  42. package/packages/dd-trace/src/config/index.js +7 -60
  43. package/packages/dd-trace/src/config/normalize-service.js +31 -0
  44. package/packages/dd-trace/src/config/supported-configurations.json +15 -32
  45. package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
  46. package/packages/dd-trace/src/datastreams/encoding.js +39 -28
  47. package/packages/dd-trace/src/datastreams/pathway.js +29 -26
  48. package/packages/dd-trace/src/datastreams/processor.js +17 -15
  49. package/packages/dd-trace/src/datastreams/size.js +6 -2
  50. package/packages/dd-trace/src/debugger/config.js +6 -3
  51. package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
  52. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
  53. package/packages/dd-trace/src/dogstatsd.js +10 -7
  54. package/packages/dd-trace/src/encode/0.4.js +3 -3
  55. package/packages/dd-trace/src/encode/0.5.js +2 -2
  56. package/packages/dd-trace/src/encode/agentless-json.js +2 -2
  57. package/packages/dd-trace/src/encode/tags-processors.js +2 -27
  58. package/packages/dd-trace/src/exporters/common/request.js +22 -11
  59. package/packages/dd-trace/src/exporters/common/retry.js +104 -0
  60. package/packages/dd-trace/src/git_metadata.js +66 -0
  61. package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
  62. package/packages/dd-trace/src/heap_snapshots.js +4 -4
  63. package/packages/dd-trace/src/id.js +15 -26
  64. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  65. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
  66. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
  67. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +30 -13
  68. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
  69. package/packages/dd-trace/src/llmobs/sdk.js +5 -1
  70. package/packages/dd-trace/src/llmobs/span_processor.js +28 -2
  71. package/packages/dd-trace/src/llmobs/tagger.js +42 -0
  72. package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
  73. package/packages/dd-trace/src/llmobs/util.js +80 -5
  74. package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +2 -2
  75. package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
  76. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
  77. package/packages/dd-trace/src/opentelemetry/context_manager.js +22 -10
  78. package/packages/dd-trace/src/opentelemetry/span-helpers.js +308 -0
  79. package/packages/dd-trace/src/opentelemetry/span.js +42 -108
  80. package/packages/dd-trace/src/opentelemetry/tracer.js +11 -36
  81. package/packages/dd-trace/src/opentracing/propagation/text_map.js +95 -36
  82. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +98 -32
  83. package/packages/dd-trace/src/opentracing/span.js +58 -49
  84. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  85. package/packages/dd-trace/src/plugins/util/ci.js +119 -32
  86. package/packages/dd-trace/src/plugins/util/test.js +293 -27
  87. package/packages/dd-trace/src/priority_sampler.js +6 -4
  88. package/packages/dd-trace/src/profiling/config.js +5 -4
  89. package/packages/dd-trace/src/profiling/ssi-heuristics.js +2 -2
  90. package/packages/dd-trace/src/propagation-hash/index.js +1 -1
  91. package/packages/dd-trace/src/proxy.js +3 -3
  92. package/packages/dd-trace/src/remote_config/index.js +5 -3
  93. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  94. package/packages/dd-trace/src/span_format.js +52 -5
  95. package/packages/dd-trace/src/span_processor.js +1 -5
  96. package/packages/dd-trace/src/spanleak.js +0 -1
  97. package/packages/dd-trace/src/telemetry/telemetry.js +7 -5
  98. package/packages/dd-trace/src/tracer_metadata.js +1 -1
  99. package/packages/dd-trace/src/util.js +17 -0
  100. package/vendor/dist/path-to-regexp/LICENSE +0 -21
  101. package/vendor/dist/path-to-regexp/index.js +0 -1
@@ -9,31 +9,13 @@ const { timeInputToHrTime } = require('../../../../vendor/dist/@opentelemetry/co
9
9
 
10
10
  const tracer = require('../../')
11
11
  const DatadogSpan = require('../opentracing/span')
12
- const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK, IGNORE_OTEL_ERROR } = require('../constants')
13
12
  const { SERVICE_NAME, RESOURCE_NAME, SPAN_KIND } = require('../../../../ext/tags')
14
13
  const kinds = require('../../../../ext/kinds')
15
14
 
16
15
  const id = require('../id')
16
+ const BridgeSpanBase = require('./bridge-span-base')
17
17
  const SpanContext = require('./span_context')
18
-
19
- // The one built into OTel rounds so we lose sub-millisecond precision.
20
- function hrTimeToMilliseconds (time) {
21
- return time[0] * 1e3 + time[1] / 1e6
22
- }
23
-
24
- function isTimeInput (startTime) {
25
- if (typeof startTime === 'number') {
26
- return true
27
- }
28
- if (startTime instanceof Date) {
29
- return true
30
- }
31
- if (Array.isArray(startTime) && startTime.length === 2 &&
32
- typeof startTime[0] === 'number' && typeof startTime[1] === 'number') {
33
- return true
34
- }
35
- return false
36
- }
18
+ const { setOtelOperationName } = require('./span-helpers')
37
19
 
38
20
  const spanKindNames = {
39
21
  [api.SpanKind.INTERNAL]: kinds.INTERNAL,
@@ -43,6 +25,15 @@ const spanKindNames = {
43
25
  [api.SpanKind.CONSUMER]: kinds.CONSUMER,
44
26
  }
45
27
 
28
+ /**
29
+ * The OTel-shipped `hrTimeToMilliseconds` rounds, dropping sub-millisecond precision we want.
30
+ *
31
+ * @param {[number, number]} hrTime
32
+ */
33
+ function hrTimeToMilliseconds (hrTime) {
34
+ return hrTime[0] * 1e3 + hrTime[1] / 1e6
35
+ }
36
+
46
37
  /**
47
38
  * Several of these attributes are not yet supported by the Node.js OTel API.
48
39
  * We check for old equivalents where we can, but not all had equivalents.
@@ -122,7 +113,21 @@ function spanNameMapper (spanName, kind, attributes) {
122
113
  return spanKindNames[kind]
123
114
  }
124
115
 
125
- class Span {
116
+ /**
117
+ * OTel-bridge span backed by a `DatadogSpan`. `Tracer` constructs these on the OTel API
118
+ * surface; the underlying DD span carries the lifecycle.
119
+ */
120
+ class Span extends BridgeSpanBase {
121
+ /**
122
+ * @param {import('./tracer')} parentTracer
123
+ * @param {import('@opentelemetry/api').Context} context
124
+ * @param {string | undefined} spanName
125
+ * @param {import('./span_context')} spanContext
126
+ * @param {import('@opentelemetry/api').SpanKind} kind
127
+ * @param {Array<import('@opentelemetry/api').Link>} [links]
128
+ * @param {import('@opentelemetry/api').TimeInput} [timeInput]
129
+ * @param {import('@opentelemetry/api').Attributes} [attributes]
130
+ */
126
131
  constructor (
127
132
  parentTracer,
128
133
  context,
@@ -138,7 +143,7 @@ class Span {
138
143
  const hrStartTime = timeInputToHrTime(timeInput || (performance.now() + timeOrigin))
139
144
  const startTime = hrTimeToMilliseconds(hrStartTime)
140
145
 
141
- this._ddSpan = new DatadogSpan(_tracer, _tracer._processor, _tracer._prioritySampler, {
146
+ const ddSpan = new DatadogSpan(_tracer, _tracer._processor, _tracer._prioritySampler, {
142
147
  operationName: spanNameMapper(spanName, kind, attributes),
143
148
  context: spanContext._ddContext,
144
149
  startTime,
@@ -152,6 +157,8 @@ class Span {
152
157
  links,
153
158
  }, _tracer._debug)
154
159
 
160
+ super(ddSpan)
161
+
155
162
  if (attributes) {
156
163
  this.setAttributes(attributes)
157
164
  }
@@ -159,8 +166,6 @@ class Span {
159
166
  this._parentTracer = parentTracer
160
167
  this._context = context
161
168
 
162
- this._hasStatus = false
163
-
164
169
  // NOTE: Need to grab the value before setting it on the span because the
165
170
  // math for computing opentracing timestamps is apparently lossy...
166
171
  this.startTime = hrStartTime
@@ -194,43 +199,13 @@ class Span {
194
199
  return new SpanContext(this._ddSpan.context())
195
200
  }
196
201
 
197
- setAttribute (key, value) {
198
- if (key === 'http.response.status_code') {
199
- this._ddSpan.setTag('http.status_code', value.toString())
200
- }
201
-
202
- this._ddSpan.setTag(key, value)
203
- return this
204
- }
205
-
206
- setAttributes (attributes) {
207
- if ('http.response.status_code' in attributes) {
208
- attributes['http.status_code'] = attributes['http.response.status_code'].toString()
209
- }
210
-
211
- this._ddSpan.addTags(attributes)
212
- return this
213
- }
214
-
215
- addLink (link, attrs) {
216
- // TODO: Remove this once we remove addLink(context, attrs) in v6.0.0
217
- if (link instanceof SpanContext) {
218
- link = { context: link, attributes: attrs ?? {} }
219
- }
220
-
221
- const { context, attributes } = link
222
- // Extract dd context
223
- const ddSpanContext = context._ddContext
224
- this._ddSpan.addLink({ context: ddSpanContext, attributes })
225
- return this
226
- }
227
-
228
- addLinks (links) {
229
- for (const link of links) this.addLink(link)
230
- return this
231
- }
232
-
202
+ /**
203
+ * @param {string} ptrKind
204
+ * @param {string} ptrDir
205
+ * @param {string} ptrHash
206
+ */
233
207
  addSpanPointer (ptrKind, ptrDir, ptrHash) {
208
+ if (this.ended) return this
234
209
  const zeroContext = new SpanContext({
235
210
  traceId: id('0'),
236
211
  spanId: id('0'),
@@ -244,26 +219,17 @@ class Span {
244
219
  return this.addLink(zeroContext, attributes)
245
220
  }
246
221
 
247
- setStatus ({ code, message }) {
248
- if (!this.ended && !this._hasStatus && code) {
249
- this._hasStatus = true
250
- if (code === 2) {
251
- this._ddSpan.addTags({
252
- [ERROR_MESSAGE]: message,
253
- [IGNORE_OTEL_ERROR]: false,
254
- })
255
- }
256
- }
257
- return this
258
- }
259
-
222
+ /**
223
+ * @param {string} name
224
+ */
260
225
  updateName (name) {
261
- if (!this.ended) {
262
- this._ddSpan.setOperationName(name)
263
- }
226
+ setOtelOperationName(this._ddSpan, name)
264
227
  return this
265
228
  }
266
229
 
230
+ /**
231
+ * @param {import('@opentelemetry/api').TimeInput} [timeInput]
232
+ */
267
233
  end (timeInput) {
268
234
  if (this.ended) {
269
235
  api.diag.error('You can only call end() on a span once.')
@@ -277,41 +243,9 @@ class Span {
277
243
  this._spanProcessor.onEnd(this)
278
244
  }
279
245
 
280
- isRecording () {
281
- return this.ended === false
282
- }
283
-
284
- addEvent (name, attributesOrStartTime, startTime) {
285
- startTime = attributesOrStartTime && isTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime
286
- const hrStartTime = timeInputToHrTime(startTime || (performance.now() + timeOrigin))
287
- startTime = hrTimeToMilliseconds(hrStartTime)
288
-
289
- this._ddSpan.addEvent(name, attributesOrStartTime, startTime)
290
- return this
291
- }
292
-
293
- recordException (exception, timeInput) {
294
- this._ddSpan.addTags({
295
- [ERROR_TYPE]: exception.name,
296
- [ERROR_MESSAGE]: exception.message,
297
- [ERROR_STACK]: exception.stack,
298
- [IGNORE_OTEL_ERROR]: this._ddSpan.context()._tags[IGNORE_OTEL_ERROR] ?? true,
299
- })
300
- const attributes = {}
301
- if (exception.message) attributes['exception.message'] = exception.message
302
- if (exception.type) attributes['exception.type'] = exception.type
303
- if (exception.escaped) attributes['exception.escaped'] = exception.escaped
304
- if (exception.stack) attributes['exception.stacktrace'] = exception.stack
305
- this.addEvent(exception.name, attributes, timeInput)
306
- }
307
-
308
246
  get duration () {
309
247
  return this._ddSpan._duration
310
248
  }
311
-
312
- get ended () {
313
- return this.duration !== undefined
314
- }
315
249
  }
316
250
 
317
251
  module.exports = Span
@@ -3,49 +3,16 @@
3
3
  const api = require('@opentelemetry/api')
4
4
  const { sanitizeAttributes } = require('../../../../vendor/dist/@opentelemetry/core')
5
5
 
6
+ const tracer = require('../../')
7
+
6
8
  const id = require('../id')
7
9
  const log = require('../log')
8
- const DatadogSpanContext = require('../opentracing/span_context')
9
10
  const TextMapPropagator = require('../opentracing/propagation/text_map')
10
11
  const TraceState = require('../opentracing/propagation/tracestate')
11
12
  const SpanContext = require('./span_context')
12
13
  const Span = require('./span')
13
14
  const Sampler = require('./sampler')
14
-
15
- function normalizeLinkContext (context) {
16
- if (!context) return
17
-
18
- // OTel API bridge SpanContext wrapper
19
- if (context._ddContext) return context._ddContext
20
-
21
- // Datadog span context
22
- if (typeof context.toTraceId === 'function' && typeof context.toSpanId === 'function') {
23
- return context
24
- }
25
-
26
- // Standard OTel SpanContext (traceId/spanId)
27
- if (typeof context.traceId !== 'string' || typeof context.spanId !== 'string') {
28
- // Invalid
29
- return
30
- }
31
-
32
- let sampling
33
- if (typeof context.traceFlags === 'number') {
34
- sampling = { priority: context.traceFlags & 1 }
35
- }
36
-
37
- let tracestate
38
- if (context.traceState?.serialize) {
39
- tracestate = TraceState.fromString(context.traceState.serialize())
40
- }
41
-
42
- return new DatadogSpanContext({
43
- traceId: id(context.traceId, 16),
44
- spanId: id(context.spanId, 16),
45
- sampling,
46
- tracestate,
47
- })
48
- }
15
+ const { normalizeLinkContext } = require('./span-helpers')
49
16
 
50
17
  class Tracer {
51
18
  constructor (library, config, tracerProvider) {
@@ -140,6 +107,14 @@ class Tracer {
140
107
  spanContext = new SpanContext()
141
108
  }
142
109
 
110
+ // init() didn't finish setting up real tracing (e.g. DD_TRACE_ENABLED=false,
111
+ // or init() was never called), so the inner tracer is still the noop.
112
+ // DatadogSpan can't construct without a processor + prioritySampler, so fall
113
+ // through to a non-recording span; the SpanContext still propagates.
114
+ if (!tracer._tracingInitialized) {
115
+ return api.trace.wrapSpanContext(spanContext)
116
+ }
117
+
143
118
  const spanKind = options.kind || api.SpanKind.INTERNAL
144
119
  const links = []
145
120
  if (options.links?.length) {
@@ -7,7 +7,7 @@ const DatadogSpanContext = require('../span_context')
7
7
  const log = require('../../log')
8
8
  const tags = require('../../../../../ext/tags')
9
9
  const { getConfiguredEnvName } = require('../../config/helper')
10
- const { setBaggageItem, getAllBaggageItems, removeAllBaggageItems } = require('../../baggage')
10
+ const { setAllBaggageItems, getAllBaggageItems, removeAllBaggageItems } = require('../../baggage')
11
11
  const telemetryMetrics = require('../../telemetry/metrics')
12
12
 
13
13
  const { AUTO_KEEP, AUTO_REJECT, USER_KEEP } = require('../../../../../ext/priority')
@@ -69,6 +69,12 @@ const percentByte = /%([0-9A-Fa-f]{2})/g
69
69
  class TextMapPropagator {
70
70
  #extractB3Context
71
71
 
72
+ /** @type {Set<string> | undefined} Cached `Set` view of `_config.baggageTagKeys`. */
73
+ #baggageTagKeysSet
74
+
75
+ /** @type {string[] | undefined} Source array that `#baggageTagKeysSet` was built from. */
76
+ #baggageTagKeysSetSource
77
+
72
78
  constructor (config) {
73
79
  this._config = config
74
80
 
@@ -78,6 +84,23 @@ class TextMapPropagator {
78
84
  this.#extractB3Context = envName === 'OTEL_PROPAGATORS' ? this._extractB3SingleContext : this._extractB3MultiContext
79
85
  }
80
86
 
87
+ /**
88
+ * Returns a `Set` view of `_config.baggageTagKeys` that is rebuilt only
89
+ * when the source array reference changes. Avoids an `O(n)` `Set` alloc
90
+ * per baggage extract (which is per-request when baggage propagation is
91
+ * enabled).
92
+ *
93
+ * @returns {Set<string>}
94
+ */
95
+ #getBaggageTagKeysSet () {
96
+ const source = this._config.baggageTagKeys
97
+ if (this.#baggageTagKeysSetSource !== source) {
98
+ this.#baggageTagKeysSet = new Set(source)
99
+ this.#baggageTagKeysSetSource = source
100
+ }
101
+ return this.#baggageTagKeysSet
102
+ }
103
+
81
104
  inject (spanContext, carrier) {
82
105
  if (!carrier) return
83
106
  this._injectBaggageItems(spanContext, carrier)
@@ -166,6 +189,7 @@ class TextMapPropagator {
166
189
  }
167
190
  }
168
191
  }
192
+
169
193
  if (this._hasPropagationStyle('inject', 'baggage')) {
170
194
  let baggage = ''
171
195
  let itemCounter = 0
@@ -173,14 +197,14 @@ class TextMapPropagator {
173
197
 
174
198
  const baggageItems = getAllBaggageItems()
175
199
  if (!baggageItems) return
176
- for (const [key, value] of Object.entries(baggageItems)) {
200
+ for (const key of Object.keys(baggageItems)) {
177
201
  const baggageKey = key.trim()
178
202
  if (!baggageTokenExpr.test(baggageKey)) continue
179
203
 
180
204
  // Do not trim values. If callers include leading/trailing whitespace, it must be percent-encoded.
181
205
  // W3C list-member allows optional properties after ';'.
182
206
  // https://www.w3.org/TR/baggage/#header-content
183
- const item = `${baggageKey}=${encodeURIComponent(value)},`
207
+ const item = `${baggageKey}=${encodeURIComponent(baggageItems[key])},`
184
208
  itemCounter += 1
185
209
  byteCounter += item.length
186
210
 
@@ -217,14 +241,15 @@ class TextMapPropagator {
217
241
 
218
242
  const tags = []
219
243
 
220
- for (const key in trace.tags) {
221
- if (!trace.tags[key] || !key.startsWith('_dd.p.')) continue
222
- if (!this._validateTagKey(key) || !this._validateTagValue(trace.tags[key])) {
244
+ for (const key of Object.keys(trace.tags)) {
245
+ const value = trace.tags[key]
246
+ if (!value || !key.startsWith('_dd.p.')) continue
247
+ if (!this._validateTagKey(key) || !this._validateTagValue(value)) {
223
248
  log.error('Trace tags from span are invalid, skipping injection.')
224
249
  return
225
250
  }
226
251
 
227
- tags.push(`${key}=${trace.tags[key]}`)
252
+ tags.push(`${key}=${value}`)
228
253
  }
229
254
 
230
255
  const header = tags.join(',')
@@ -274,7 +299,7 @@ class TextMapPropagator {
274
299
  const {
275
300
  _sampling: { priority, mechanism },
276
301
  _tracestate: ts = new TraceState(),
277
- _trace: { origin, tags },
302
+ _trace: { origin, tags: traceTags },
278
303
  } = spanContext
279
304
 
280
305
  carrier[traceparentKey] = spanContext.toTraceparent()
@@ -296,21 +321,22 @@ class TextMapPropagator {
296
321
  if (typeof origin === 'string') {
297
322
  const originValue = origin
298
323
  .replaceAll(tracestateOriginFilter, '_')
299
- .replaceAll(/[\x3D]/g, '~')
324
+ .replaceAll('=', '~')
300
325
 
301
326
  state.set('o', originValue)
302
327
  }
303
328
 
304
- for (const key in tags) {
305
- if (!tags[key] || !key.startsWith('_dd.p.')) continue
329
+ for (const key of Object.keys(traceTags)) {
330
+ const tagValueRaw = traceTags[key]
331
+ if (!tagValueRaw || !key.startsWith('_dd.p.')) continue
306
332
 
307
333
  const tagKey = 't.' + key.slice(6)
308
334
  .replaceAll(tracestateTagKeyFilter, '_')
309
335
 
310
- const tagValue = tags[key]
336
+ const tagValue = tagValueRaw
311
337
  .toString()
312
338
  .replaceAll(tracestateTagValueFilter, '_')
313
- .replaceAll(/[\x3D]/g, '~')
339
+ .replaceAll('=', '~')
314
340
 
315
341
  state.set(tagKey, tagValue)
316
342
  }
@@ -408,10 +434,12 @@ class TextMapPropagator {
408
434
  }
409
435
  }
410
436
 
411
- if (this._config.tracePropagationBehaviorExtract === 'ignore') {
412
- context._links = []
437
+ if (this._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT === 'ignore') {
438
+ // `context` is null when no extractor matched; the fallback below picks up
439
+ // the SQSD context if present, otherwise the request runs untraced.
440
+ if (context) context._links = []
413
441
  } else {
414
- if (this._config.tracePropagationBehaviorExtract === 'restart') {
442
+ if (this._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT === 'restart' && context) {
415
443
  context._links = []
416
444
  context._links.push({
417
445
  context,
@@ -502,14 +530,19 @@ class TextMapPropagator {
502
530
 
503
531
  _extractTraceparentContext (carrier) {
504
532
  const headerValue = carrier[traceparentKey]
505
- if (!headerValue) {
533
+ if (typeof headerValue !== 'string') {
506
534
  return null
507
535
  }
508
536
  const matches = headerValue.trim().match(traceparentExpr)
509
- if (matches?.length) {
510
- const [version, traceId, spanId, flags, tail] = matches.slice(1)
537
+ if (matches !== null) {
538
+ const [, version, traceId, spanId, flags, tail] = matches
511
539
  const traceparent = { version }
512
- const tracestate = TraceState.fromString(carrier.tracestate)
540
+ // W3C Trace Context §3.3.1.1: multiple tracestate fields MUST be combined per RFC 7230 §3.2.2.
541
+ // `filter` drops non-string members (Symbol, throwing-toString) that would crash `join`.
542
+ const rawTracestate = Array.isArray(carrier.tracestate)
543
+ ? carrier.tracestate.filter(item => typeof item === 'string').join(',')
544
+ : carrier.tracestate
545
+ const tracestate = TraceState.fromString(rawTracestate)
513
546
  if (invalidSegment.test(traceId)) return null
514
547
  if (invalidSegment.test(spanId)) return null
515
548
 
@@ -523,7 +556,7 @@ class TextMapPropagator {
523
556
  traceId: id(traceId, 16),
524
557
  spanId: id(spanId, 16),
525
558
  isRemote: true,
526
- sampling: { priority: Number.parseInt(flags, 10) & 1 ? 1 : 0 },
559
+ sampling: { priority: Number.parseInt(flags, 16) & 1 ? 1 : 0 },
527
560
  traceparent,
528
561
  tracestate,
529
562
  })
@@ -549,7 +582,7 @@ class TextMapPropagator {
549
582
  break
550
583
  }
551
584
  case 'o':
552
- spanContext._trace.origin = value
585
+ spanContext._trace.origin = value.replaceAll('~', '=')
553
586
  break
554
587
  case 't.dm': {
555
588
  const mechanism = Math.abs(Number.parseInt(value, 10))
@@ -562,7 +595,7 @@ class TextMapPropagator {
562
595
  default: {
563
596
  if (!key.startsWith('t.')) continue
564
597
  const subKey = key.slice(2) // e.g. t.tid -> tid
565
- const transformedValue = value.replaceAll(/[\x7E]/gm, '=')
598
+ const transformedValue = value.replaceAll('~', '=')
566
599
 
567
600
  // If subkey is tid then do nothing because trace header tid should always be preserved
568
601
  if (subKey === 'tid') {
@@ -675,11 +708,29 @@ class TextMapPropagator {
675
708
  _extractBaggageItems (carrier, spanContext) {
676
709
  removeAllBaggageItems()
677
710
  if (!this._hasPropagationStyle('extract', 'baggage')) return
678
- if (!carrier?.baggage) return
679
- const baggages = carrier.baggage.split(',')
680
- const baggageTagKeys = new Set(this._config.baggageTagKeys)
711
+ const baggageHeader = carrier?.baggage
712
+ const header = Array.isArray(baggageHeader) ? baggageHeader.join(',') : baggageHeader
713
+ if (!header) return
714
+
715
+ const baggages = header.split(',')
716
+ const baggageTagKeys = this.#getBaggageTagKeysSet()
681
717
  const tagAllKeys = baggageTagKeys.has('*')
718
+ /** @type {Record<string, string> | undefined} */
719
+ let items
720
+ let itemCount = 0
721
+ let byteCount = 0
722
+
682
723
  for (const keyValue of baggages) {
724
+ if (itemCount >= this._config.baggageMaxItems) {
725
+ tracerMetrics.count('context_header.truncated', ['truncation_reason:baggage_item_count_exceeded']).inc()
726
+ break
727
+ }
728
+ // Charge the comma slot before the empty-entry skip so a `,,,,,foo=bar` can't iterate for free.
729
+ byteCount += keyValue.length + 1
730
+ if (byteCount > this._config.baggageMaxBytes) {
731
+ tracerMetrics.count('context_header.truncated', ['truncation_reason:baggage_byte_count_exceeded']).inc()
732
+ break
733
+ }
683
734
  if (!keyValue) continue
684
735
 
685
736
  // Per W3C baggage, list-members can contain optional properties after `;`.
@@ -692,7 +743,6 @@ class TextMapPropagator {
692
743
  const eqIdx = member.indexOf('=')
693
744
  if (eqIdx === -1) {
694
745
  tracerMetrics.count('context_header_style.malformed', ['header_style:baggage']).inc()
695
- removeAllBaggageItems()
696
746
  return
697
747
  }
698
748
 
@@ -701,24 +751,33 @@ class TextMapPropagator {
701
751
 
702
752
  if (!baggageTokenExpr.test(key) || !value) {
703
753
  tracerMetrics.count('context_header_style.malformed', ['header_style:baggage']).inc()
704
- removeAllBaggageItems()
705
754
  return
706
755
  }
707
- try {
708
- value = decodeURIComponent(value)
709
- } catch {
710
- const bytes = value.replaceAll(percentByte, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
711
- value = Buffer.from(bytes, 'binary').toString('utf8')
756
+ // `decodeURIComponent` only does work when the value contains a
757
+ // percent-encoded sequence; everything else passes through unchanged.
758
+ // Skipping the call (and the surrounding `try` frame) shaves an alloc
759
+ // per baggage entry on the dominant ASCII case.
760
+ if (value.includes('%')) {
761
+ try {
762
+ value = decodeURIComponent(value)
763
+ } catch {
764
+ const bytes = value.replaceAll(percentByte, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
765
+ value = Buffer.from(bytes, 'binary').toString('utf8')
766
+ }
712
767
  }
768
+ items ??= {}
769
+ items[key] = value
770
+ itemCount++
713
771
 
714
772
  if (spanContext && (tagAllKeys || baggageTagKeys.has(key))) {
715
773
  spanContext._trace.tags['baggage.' + key] = value
716
774
  }
717
- setBaggageItem(key, value)
718
775
  }
719
776
 
720
- // Successfully extracted baggage
721
- tracerMetrics.count('context_header_style.extracted', ['header_style:baggage']).inc()
777
+ if (items) {
778
+ setAllBaggageItems(items)
779
+ tracerMetrics.count('context_header_style.extracted', ['header_style:baggage']).inc()
780
+ }
722
781
  }
723
782
 
724
783
  _extractSamplingPriority (carrier, spanContext) {