dd-trace 5.100.0 → 5.102.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 (189) hide show
  1. package/index.d.ts +14 -0
  2. package/package.json +11 -9
  3. package/packages/datadog-instrumentations/src/aerospike.js +2 -2
  4. package/packages/datadog-instrumentations/src/ai.js +8 -8
  5. package/packages/datadog-instrumentations/src/amqplib.js +6 -7
  6. package/packages/datadog-instrumentations/src/anthropic.js +10 -10
  7. package/packages/datadog-instrumentations/src/apollo-server-core.js +3 -3
  8. package/packages/datadog-instrumentations/src/apollo-server.js +5 -5
  9. package/packages/datadog-instrumentations/src/avsc.js +6 -6
  10. package/packages/datadog-instrumentations/src/aws-sdk.js +151 -67
  11. package/packages/datadog-instrumentations/src/azure-durable-functions.js +8 -8
  12. package/packages/datadog-instrumentations/src/bluebird.js +2 -2
  13. package/packages/datadog-instrumentations/src/body-parser.js +2 -2
  14. package/packages/datadog-instrumentations/src/cassandra-driver.js +7 -7
  15. package/packages/datadog-instrumentations/src/child_process.js +12 -12
  16. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +9 -9
  17. package/packages/datadog-instrumentations/src/connect.js +7 -7
  18. package/packages/datadog-instrumentations/src/cookie-parser.js +4 -4
  19. package/packages/datadog-instrumentations/src/cookie.js +2 -2
  20. package/packages/datadog-instrumentations/src/couchbase.js +16 -30
  21. package/packages/datadog-instrumentations/src/crypto.js +4 -4
  22. package/packages/datadog-instrumentations/src/cucumber.js +77 -16
  23. package/packages/datadog-instrumentations/src/cypress.js +5 -3
  24. package/packages/datadog-instrumentations/src/dns.js +0 -3
  25. package/packages/datadog-instrumentations/src/elasticsearch.js +8 -11
  26. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +6 -6
  27. package/packages/datadog-instrumentations/src/express-session.js +4 -4
  28. package/packages/datadog-instrumentations/src/express.js +10 -11
  29. package/packages/datadog-instrumentations/src/fastify.js +2 -2
  30. package/packages/datadog-instrumentations/src/fs.js +14 -14
  31. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +5 -7
  32. package/packages/datadog-instrumentations/src/google-genai.js +4 -4
  33. package/packages/datadog-instrumentations/src/grpc/server.js +2 -2
  34. package/packages/datadog-instrumentations/src/hapi.js +2 -2
  35. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +8 -8
  36. package/packages/datadog-instrumentations/src/helpers/promise.js +2 -2
  37. package/packages/datadog-instrumentations/src/hono.js +2 -2
  38. package/packages/datadog-instrumentations/src/http/client.js +26 -9
  39. package/packages/datadog-instrumentations/src/http/server.js +9 -9
  40. package/packages/datadog-instrumentations/src/jest.js +93 -63
  41. package/packages/datadog-instrumentations/src/kafkajs.js +9 -9
  42. package/packages/datadog-instrumentations/src/knex.js +17 -17
  43. package/packages/datadog-instrumentations/src/koa.js +12 -12
  44. package/packages/datadog-instrumentations/src/ldapjs.js +5 -5
  45. package/packages/datadog-instrumentations/src/light-my-request.js +2 -2
  46. package/packages/datadog-instrumentations/src/limitd-client.js +4 -4
  47. package/packages/datadog-instrumentations/src/lodash.js +4 -4
  48. package/packages/datadog-instrumentations/src/mariadb.js +13 -13
  49. package/packages/datadog-instrumentations/src/memcached.js +2 -2
  50. package/packages/datadog-instrumentations/src/microgateway-core.js +2 -2
  51. package/packages/datadog-instrumentations/src/mocha/common.js +7 -4
  52. package/packages/datadog-instrumentations/src/mocha/main.js +37 -14
  53. package/packages/datadog-instrumentations/src/mocha/utils.js +133 -16
  54. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -7
  55. package/packages/datadog-instrumentations/src/mongodb-core.js +9 -22
  56. package/packages/datadog-instrumentations/src/mongodb.js +5 -5
  57. package/packages/datadog-instrumentations/src/mongoose.js +21 -21
  58. package/packages/datadog-instrumentations/src/mquery.js +5 -5
  59. package/packages/datadog-instrumentations/src/multer.js +4 -4
  60. package/packages/datadog-instrumentations/src/mysql.js +16 -16
  61. package/packages/datadog-instrumentations/src/mysql2.js +4 -4
  62. package/packages/datadog-instrumentations/src/net.js +14 -8
  63. package/packages/datadog-instrumentations/src/nyc.js +5 -5
  64. package/packages/datadog-instrumentations/src/openai.js +19 -19
  65. package/packages/datadog-instrumentations/src/oracledb.js +6 -6
  66. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
  67. package/packages/datadog-instrumentations/src/passport-utils.js +5 -5
  68. package/packages/datadog-instrumentations/src/pg.js +15 -15
  69. package/packages/datadog-instrumentations/src/pino.js +6 -10
  70. package/packages/datadog-instrumentations/src/playwright.js +20 -15
  71. package/packages/datadog-instrumentations/src/protobufjs.js +16 -16
  72. package/packages/datadog-instrumentations/src/redis.js +1 -2
  73. package/packages/datadog-instrumentations/src/restify.js +2 -2
  74. package/packages/datadog-instrumentations/src/router.js +12 -12
  75. package/packages/datadog-instrumentations/src/stripe.js +12 -12
  76. package/packages/datadog-instrumentations/src/vitest.js +107 -26
  77. package/packages/datadog-instrumentations/src/winston.js +4 -4
  78. package/packages/datadog-instrumentations/src/ws.js +7 -7
  79. package/packages/datadog-plugin-aws-sdk/src/base.js +52 -4
  80. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +19 -12
  81. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +45 -35
  82. package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +33 -22
  83. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +12 -13
  84. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +73 -54
  85. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +19 -17
  86. package/packages/datadog-plugin-aws-sdk/src/util.js +22 -0
  87. package/packages/datadog-plugin-bullmq/src/consumer.js +2 -2
  88. package/packages/datadog-plugin-bullmq/src/producer.js +14 -20
  89. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -6
  90. package/packages/datadog-plugin-cucumber/src/index.js +4 -0
  91. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +18 -4
  92. package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
  93. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +1 -5
  94. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +3 -1
  95. package/packages/datadog-plugin-http/src/client.js +1 -5
  96. package/packages/datadog-plugin-jest/src/util.js +1 -2
  97. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
  99. package/packages/datadog-plugin-mocha/src/index.js +4 -0
  100. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -1
  101. package/packages/datadog-plugin-openai/src/tracing.js +12 -23
  102. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  103. package/packages/datadog-plugin-vitest/src/index.js +8 -1
  104. package/packages/datadog-shimmer/src/shimmer.js +7 -1
  105. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +1 -1
  106. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +81 -81
  107. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +2 -2
  110. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -2
  111. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -0
  112. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -3
  113. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +83 -48
  114. package/packages/dd-trace/src/appsec/index.js +21 -24
  115. package/packages/dd-trace/src/appsec/reporter.js +7 -2
  116. package/packages/dd-trace/src/appsec/rule_manager.js +4 -2
  117. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +31 -16
  118. package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
  119. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
  120. package/packages/dd-trace/src/config/config-types.d.ts +0 -2
  121. package/packages/dd-trace/src/config/git_properties.js +2 -2
  122. package/packages/dd-trace/src/config/index.js +1 -55
  123. package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
  124. package/packages/dd-trace/src/datastreams/encoding.js +39 -28
  125. package/packages/dd-trace/src/datastreams/index.js +2 -1
  126. package/packages/dd-trace/src/datastreams/pathway.js +29 -26
  127. package/packages/dd-trace/src/datastreams/processor.js +18 -17
  128. package/packages/dd-trace/src/datastreams/size.js +6 -2
  129. package/packages/dd-trace/src/debugger/config.js +5 -2
  130. package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
  131. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
  132. package/packages/dd-trace/src/debugger/devtools_client/snapshot-pruner.js +1 -0
  133. package/packages/dd-trace/src/dogstatsd.js +10 -7
  134. package/packages/dd-trace/src/encode/0.4.js +759 -234
  135. package/packages/dd-trace/src/encode/0.5.js +15 -9
  136. package/packages/dd-trace/src/encode/agentless-json.js +2 -2
  137. package/packages/dd-trace/src/encode/tags-processors.js +2 -27
  138. package/packages/dd-trace/src/exporters/common/request.js +22 -11
  139. package/packages/dd-trace/src/exporters/common/retry.js +104 -0
  140. package/packages/dd-trace/src/git_metadata.js +66 -0
  141. package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
  142. package/packages/dd-trace/src/id.js +15 -26
  143. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -2
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
  146. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
  147. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +33 -13
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
  149. package/packages/dd-trace/src/llmobs/sdk.js +29 -27
  150. package/packages/dd-trace/src/llmobs/span_processor.js +52 -6
  151. package/packages/dd-trace/src/llmobs/tagger.js +42 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
  153. package/packages/dd-trace/src/llmobs/util.js +81 -5
  154. package/packages/dd-trace/src/msgpack/chunk.js +6 -3
  155. package/packages/dd-trace/src/openfeature/noop.js +40 -36
  156. package/packages/dd-trace/src/openfeature/writers/exposures.js +33 -52
  157. package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
  158. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
  159. package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -2
  160. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +1 -2
  161. package/packages/dd-trace/src/opentelemetry/span-helpers.js +188 -50
  162. package/packages/dd-trace/src/opentelemetry/span.js +42 -80
  163. package/packages/dd-trace/src/opentelemetry/tracer.js +0 -22
  164. package/packages/dd-trace/src/opentracing/propagation/text_map.js +65 -27
  165. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +2 -11
  166. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +58 -22
  167. package/packages/dd-trace/src/opentracing/span.js +56 -48
  168. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  169. package/packages/dd-trace/src/plugins/util/ci.js +1 -1
  170. package/packages/dd-trace/src/plugins/util/git-cache.js +3 -5
  171. package/packages/dd-trace/src/plugins/util/test.js +19 -7
  172. package/packages/dd-trace/src/plugins/util/url.js +1 -3
  173. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  174. package/packages/dd-trace/src/plugins/util/web.js +5 -7
  175. package/packages/dd-trace/src/priority_sampler.js +6 -4
  176. package/packages/dd-trace/src/profiling/config.js +5 -4
  177. package/packages/dd-trace/src/profiling/profilers/events.js +3 -23
  178. package/packages/dd-trace/src/profiling/profilers/wall.js +4 -5
  179. package/packages/dd-trace/src/remote_config/index.js +5 -3
  180. package/packages/dd-trace/src/runtime_metrics/index.js +2 -2
  181. package/packages/dd-trace/src/scope.js +3 -10
  182. package/packages/dd-trace/src/serverless.js +1 -4
  183. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +7 -1
  184. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +4 -0
  185. package/packages/dd-trace/src/span_format.js +52 -5
  186. package/packages/dd-trace/src/span_processor.js +0 -4
  187. package/packages/dd-trace/src/spanleak.js +0 -1
  188. package/packages/dd-trace/src/tracer.js +7 -7
  189. package/packages/dd-trace/src/util.js +17 -0
@@ -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)
@@ -174,14 +197,14 @@ class TextMapPropagator {
174
197
 
175
198
  const baggageItems = getAllBaggageItems()
176
199
  if (!baggageItems) return
177
- for (const [key, value] of Object.entries(baggageItems)) {
200
+ for (const key of Object.keys(baggageItems)) {
178
201
  const baggageKey = key.trim()
179
202
  if (!baggageTokenExpr.test(baggageKey)) continue
180
203
 
181
204
  // Do not trim values. If callers include leading/trailing whitespace, it must be percent-encoded.
182
205
  // W3C list-member allows optional properties after ';'.
183
206
  // https://www.w3.org/TR/baggage/#header-content
184
- const item = `${baggageKey}=${encodeURIComponent(value)},`
207
+ const item = `${baggageKey}=${encodeURIComponent(baggageItems[key])},`
185
208
  itemCounter += 1
186
209
  byteCounter += item.length
187
210
 
@@ -218,14 +241,15 @@ class TextMapPropagator {
218
241
 
219
242
  const tags = []
220
243
 
221
- for (const key in trace.tags) {
222
- if (!trace.tags[key] || !key.startsWith('_dd.p.')) continue
223
- 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)) {
224
248
  log.error('Trace tags from span are invalid, skipping injection.')
225
249
  return
226
250
  }
227
251
 
228
- tags.push(`${key}=${trace.tags[key]}`)
252
+ tags.push(`${key}=${value}`)
229
253
  }
230
254
 
231
255
  const header = tags.join(',')
@@ -275,7 +299,7 @@ class TextMapPropagator {
275
299
  const {
276
300
  _sampling: { priority, mechanism },
277
301
  _tracestate: ts = new TraceState(),
278
- _trace: { origin, tags },
302
+ _trace: { origin, tags: traceTags },
279
303
  } = spanContext
280
304
 
281
305
  carrier[traceparentKey] = spanContext.toTraceparent()
@@ -297,21 +321,22 @@ class TextMapPropagator {
297
321
  if (typeof origin === 'string') {
298
322
  const originValue = origin
299
323
  .replaceAll(tracestateOriginFilter, '_')
300
- .replaceAll(/[\x3D]/g, '~')
324
+ .replaceAll('=', '~')
301
325
 
302
326
  state.set('o', originValue)
303
327
  }
304
328
 
305
- for (const key in tags) {
306
- 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
307
332
 
308
333
  const tagKey = 't.' + key.slice(6)
309
334
  .replaceAll(tracestateTagKeyFilter, '_')
310
335
 
311
- const tagValue = tags[key]
336
+ const tagValue = tagValueRaw
312
337
  .toString()
313
338
  .replaceAll(tracestateTagValueFilter, '_')
314
- .replaceAll(/[\x3D]/g, '~')
339
+ .replaceAll('=', '~')
315
340
 
316
341
  state.set(tagKey, tagValue)
317
342
  }
@@ -410,9 +435,11 @@ class TextMapPropagator {
410
435
  }
411
436
 
412
437
  if (this._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT === 'ignore') {
413
- context._links = []
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 = []
414
441
  } else {
415
- if (this._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT === 'restart') {
442
+ if (this._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT === 'restart' && context) {
416
443
  context._links = []
417
444
  context._links.push({
418
445
  context,
@@ -503,14 +530,19 @@ class TextMapPropagator {
503
530
 
504
531
  _extractTraceparentContext (carrier) {
505
532
  const headerValue = carrier[traceparentKey]
506
- if (!headerValue) {
533
+ if (typeof headerValue !== 'string') {
507
534
  return null
508
535
  }
509
536
  const matches = headerValue.trim().match(traceparentExpr)
510
- if (matches?.length) {
511
- const [version, traceId, spanId, flags, tail] = matches.slice(1)
537
+ if (matches !== null) {
538
+ const [, version, traceId, spanId, flags, tail] = matches
512
539
  const traceparent = { version }
513
- 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)
514
546
  if (invalidSegment.test(traceId)) return null
515
547
  if (invalidSegment.test(spanId)) return null
516
548
 
@@ -524,7 +556,7 @@ class TextMapPropagator {
524
556
  traceId: id(traceId, 16),
525
557
  spanId: id(spanId, 16),
526
558
  isRemote: true,
527
- sampling: { priority: Number.parseInt(flags, 10) & 1 ? 1 : 0 },
559
+ sampling: { priority: Number.parseInt(flags, 16) & 1 ? 1 : 0 },
528
560
  traceparent,
529
561
  tracestate,
530
562
  })
@@ -550,7 +582,7 @@ class TextMapPropagator {
550
582
  break
551
583
  }
552
584
  case 'o':
553
- spanContext._trace.origin = value
585
+ spanContext._trace.origin = value.replaceAll('~', '=')
554
586
  break
555
587
  case 't.dm': {
556
588
  const mechanism = Math.abs(Number.parseInt(value, 10))
@@ -563,7 +595,7 @@ class TextMapPropagator {
563
595
  default: {
564
596
  if (!key.startsWith('t.')) continue
565
597
  const subKey = key.slice(2) // e.g. t.tid -> tid
566
- const transformedValue = value.replaceAll(/[\x7E]/gm, '=')
598
+ const transformedValue = value.replaceAll('~', '=')
567
599
 
568
600
  // If subkey is tid then do nothing because trace header tid should always be preserved
569
601
  if (subKey === 'tid') {
@@ -681,7 +713,7 @@ class TextMapPropagator {
681
713
  if (!header) return
682
714
 
683
715
  const baggages = header.split(',')
684
- const baggageTagKeys = new Set(this._config.baggageTagKeys)
716
+ const baggageTagKeys = this.#getBaggageTagKeysSet()
685
717
  const tagAllKeys = baggageTagKeys.has('*')
686
718
  /** @type {Record<string, string> | undefined} */
687
719
  let items
@@ -721,11 +753,17 @@ class TextMapPropagator {
721
753
  tracerMetrics.count('context_header_style.malformed', ['header_style:baggage']).inc()
722
754
  return
723
755
  }
724
- try {
725
- value = decodeURIComponent(value)
726
- } catch {
727
- const bytes = value.replaceAll(percentByte, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
728
- 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
+ }
729
767
  }
730
768
  items ??= {}
731
769
  items[key] = value
@@ -16,7 +16,7 @@ class DSMTextMapPropagator {
16
16
  inject (ctx, carrier) {
17
17
  if (!this.config.dsmEnabled) return
18
18
 
19
- this._injectDatadogDSMContext(ctx, carrier)
19
+ DsmPathwayCodec.encode(ctx, carrier)
20
20
 
21
21
  // eslint-disable-next-line eslint-rules/eslint-log-printf-style
22
22
  log.debug(() => `Inject into carrier (DSM): ${JSON.stringify(pick(carrier, logKeys))}.`)
@@ -25,7 +25,7 @@ class DSMTextMapPropagator {
25
25
  extract (carrier) {
26
26
  if (!this.config.dsmEnabled) return
27
27
 
28
- const dsmContext = this._extractDatadogDSMContext(carrier)
28
+ const dsmContext = DsmPathwayCodec.decode(carrier)
29
29
 
30
30
  if (!dsmContext) return dsmContext
31
31
 
@@ -33,15 +33,6 @@ class DSMTextMapPropagator {
33
33
  log.debug(() => `Extract from carrier (DSM): ${JSON.stringify(pick(carrier, logKeys))}.`)
34
34
  return dsmContext
35
35
  }
36
-
37
- _injectDatadogDSMContext (ctx, carrier) {
38
- DsmPathwayCodec.encode(ctx, carrier)
39
- }
40
-
41
- _extractDatadogDSMContext (carrier) {
42
- const ctx = DsmPathwayCodec.decode(carrier)
43
- return ctx
44
- }
45
36
  }
46
37
 
47
38
  module.exports = DSMTextMapPropagator
@@ -15,8 +15,7 @@ const WHITESPACE = /[ \t]/
15
15
  * @returns {[string, string][]} Entries in reverse of wire order.
16
16
  */
17
17
  function parseEntries (value, fieldSeparator, pairSeparator, rejectValueTabs) {
18
- const segments = value.split(fieldSeparator)
19
- segments.length = Math.min(segments.length, MAX_LIST_MEMBERS)
18
+ const segments = value.split(fieldSeparator, MAX_LIST_MEMBERS)
20
19
 
21
20
  // TODO: We should extract dd no matter at what position and move it to the front of the list.
22
21
  // Extract up 31 additional entries.
@@ -57,28 +56,45 @@ function toString (map, pairSeparator, fieldSeparator) {
57
56
  return result
58
57
  }
59
58
 
60
- class TraceStateData extends Map {
61
- constructor (...args) {
62
- super(...args)
63
- this.changed = false
59
+ class TraceStateData {
60
+ #map
61
+ changed = false
62
+
63
+ constructor (entries) {
64
+ this.#map = entries ? new Map(entries) : new Map()
64
65
  }
65
66
 
66
- set (...args) {
67
- if (this.has(args[0]) && this.get(args[0]) === args[1]) {
68
- return
69
- }
67
+ set (key, value) {
68
+ if (this.#map.get(key) === value && (value !== undefined || this.#map.has(key))) return this
70
69
  this.changed = true
71
- return super.set(...args)
70
+ this.#map.set(key, value)
71
+ return this
72
72
  }
73
73
 
74
- delete (...args) {
74
+ get (key) {
75
+ return this.#map.get(key)
76
+ }
77
+
78
+ delete (key) {
75
79
  this.changed = true
76
- return super.delete(...args)
80
+ return this.#map.delete(key)
77
81
  }
78
82
 
79
- clear (...args) {
83
+ clear () {
80
84
  this.changed = true
81
- return super.clear(...args)
85
+ this.#map.clear()
86
+ }
87
+
88
+ entries () {
89
+ return this.#map.entries()
90
+ }
91
+
92
+ [Symbol.iterator] () {
93
+ return this.#map[Symbol.iterator]()
94
+ }
95
+
96
+ get size () {
97
+ return this.#map.size
82
98
  }
83
99
 
84
100
  static fromString (value) {
@@ -94,25 +110,45 @@ class TraceStateData extends Map {
94
110
  * Pairs are stored in reverse of the serialized format to rely on set ordering
95
111
  * new entries at the end to express update movement.
96
112
  */
97
- class TraceState extends Map {
113
+ class TraceState {
114
+ #map
115
+
116
+ constructor (entries) {
117
+ this.#map = entries ? new Map(entries) : new Map()
118
+ }
119
+
98
120
  // Delete entries on update to ensure they're moved to the end of the list
99
121
  set (key, value) {
100
- if (this.has(key)) {
101
- this.delete(key)
102
- }
122
+ if (this.#map.has(key)) this.#map.delete(key)
123
+ this.#map.set(key, value)
124
+ return this
125
+ }
126
+
127
+ get (key) {
128
+ return this.#map.get(key)
129
+ }
130
+
131
+ delete (key) {
132
+ return this.#map.delete(key)
133
+ }
134
+
135
+ [Symbol.iterator] () {
136
+ return this.#map[Symbol.iterator]()
137
+ }
103
138
 
104
- return super.set(key, value)
139
+ get size () {
140
+ return this.#map.size
105
141
  }
106
142
 
107
143
  forVendor (vendor, handle) {
108
- const data = super.get(vendor)
144
+ const data = this.#map.get(vendor)
109
145
  const state = TraceStateData.fromString(data)
110
146
  const result = handle(state)
111
147
 
112
148
  if (state.changed) {
113
149
  const value = state.toString()
114
150
  if (value) {
115
- this.set(vendor, state.toString())
151
+ this.set(vendor, value)
116
152
  } else {
117
153
  this.delete(vendor)
118
154
  }
@@ -11,21 +11,16 @@ const runtimeMetrics = require('../runtime_metrics')
11
11
  const log = require('../log')
12
12
  const { storage } = require('../../../datadog-core')
13
13
  const telemetryMetrics = require('../telemetry/metrics')
14
- const { getValueFromEnvSources } = require('../config/helper')
15
- const { isTrue } = require('../util')
16
14
  const SpanContext = require('./span_context')
17
15
 
18
16
  const dateNow = Date.now
19
17
 
20
18
  const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
21
19
 
22
- const DD_TRACE_EXPERIMENTAL_STATE_TRACKING = isTrue(getValueFromEnvSources('DD_TRACE_EXPERIMENTAL_STATE_TRACKING'))
23
- const DD_TRACE_EXPERIMENTAL_SPAN_COUNTS = isTrue(getValueFromEnvSources('DD_TRACE_EXPERIMENTAL_SPAN_COUNTS'))
24
-
25
20
  const unfinishedRegistry = createRegistry('unfinished')
26
21
  const finishedRegistry = createRegistry('finished')
27
22
 
28
- const OTEL_ENABLED = !!getValueFromEnvSources('DD_TRACE_OTEL_ENABLED')
23
+ let OTEL_ENABLED = false
29
24
  const ALLOWED = new Set(['string', 'number', 'boolean'])
30
25
 
31
26
  const integrationCounters = {
@@ -37,6 +32,25 @@ const startCh = channel('dd-trace:span:start')
37
32
  const finishCh = channel('dd-trace:span:finish')
38
33
  const tagsUpdateCh = channel('dd-trace:span:tags:update')
39
34
 
35
+ // Module-scope so we don't allocate a fresh recursive closure on every
36
+ // `addLink` / `addEvent`.
37
+ /**
38
+ * @param {Record<string, string>} out
39
+ * @param {string} key
40
+ * @param {unknown} value
41
+ */
42
+ function addArrayOrScalarAttribute (out, key, value) {
43
+ if (Array.isArray(value)) {
44
+ for (let i = 0; i < value.length; i++) {
45
+ addArrayOrScalarAttribute(out, `${key}.${i}`, value[i])
46
+ }
47
+ } else if (ALLOWED.has(typeof value)) {
48
+ out[key] = typeof value === 'string' ? value : String(value)
49
+ } else {
50
+ log.warn('Dropping span link attribute. It is not of an allowed type')
51
+ }
52
+ }
53
+
40
54
  function getIntegrationCounter (event, integration) {
41
55
  const counters = integrationCounters[event]
42
56
 
@@ -55,15 +69,22 @@ function getIntegrationCounter (event, integration) {
55
69
  }
56
70
 
57
71
  class DatadogSpan {
72
+ #parentTracer
73
+
58
74
  constructor (tracer, processor, prioritySampler, fields, debug) {
75
+ OTEL_ENABLED = tracer._config.DD_TRACE_OTEL_ENABLED
76
+
59
77
  const operationName = fields.operationName
60
78
  const parent = fields.parent || null
61
- // TODO(BridgeAR): Investigate why this is causing a performance regression
79
+ // Stay on `Object.assign({}, src)` for backportability: V8 12+ (Node 22 /
80
+ // 24) inlines `{ ...src }` and beats `Object.assign` here, but on V8 10.2
81
+ // / 11.3 (Node 18 / 20) the spread takes a generic runtime path and slows
82
+ // `spans-finish-*` by ~140%. Revisit once those LTS lines drop.
62
83
  // eslint-disable-next-line prefer-object-spread
63
84
  const tags = Object.assign({}, fields.tags)
64
85
  const hostname = fields.hostname
65
86
 
66
- this._parentTracer = tracer
87
+ this.#parentTracer = tracer
67
88
  this._debug = debug
68
89
  this._processor = processor
69
90
  this._prioritySampler = prioritySampler
@@ -94,7 +115,7 @@ class DatadogSpan {
94
115
  attributes: this._sanitizeAttributes(link.attributes),
95
116
  })) ?? []
96
117
 
97
- if (DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
118
+ if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
98
119
  runtimeMetrics.increment('runtime.node.spans.unfinished')
99
120
  runtimeMetrics.increment('runtime.node.spans.unfinished.by.name', `span_name:${operationName}`)
100
121
 
@@ -104,16 +125,8 @@ class DatadogSpan {
104
125
  unfinishedRegistry.register(this, operationName, this)
105
126
  }
106
127
 
107
- // Nullish operator is used here because both `tracer` and `tracer._config`
108
- // can be null and there are tests passing invalid values to the `Span`
109
- // constructor which still succeed today. Part of the problem is that `Span`
110
- // stores only the tracer and not the config, so anything that needs the
111
- // config has to read it from the tracer stored on the span, including
112
- // even `Span` itself in this case.
113
- //
114
- // TODO: Refactor Tracer/Span + tests to avoid having to do nullish checks.
115
- if (tracer?._config?.DD_TRACE_SPAN_LEAK_DEBUG > 0) {
116
- require('../spanleak').addSpan(this, operationName)
128
+ if (tracer._config.DD_TRACE_SPAN_LEAK_DEBUG > 0) {
129
+ require('../spanleak').addSpan(this)
117
130
  }
118
131
 
119
132
  if (startCh.hasSubscribers) {
@@ -124,7 +137,7 @@ class DatadogSpan {
124
137
  [util.inspect.custom] () {
125
138
  return {
126
139
  ...this,
127
- _parentTracer: `[${this._parentTracer.constructor.name}]`,
140
+ parentTracer: `[${this.#parentTracer.constructor.name}]`,
128
141
  _prioritySampler: `[${this._prioritySampler.constructor.name}]`,
129
142
  _processor: `[${this._processor.constructor.name}]`,
130
143
  }
@@ -156,7 +169,7 @@ class DatadogSpan {
156
169
  }
157
170
 
158
171
  tracer () {
159
- return this._parentTracer
172
+ return this.#parentTracer
160
173
  }
161
174
 
162
175
  setOperationName (name) {
@@ -254,14 +267,14 @@ class DatadogSpan {
254
267
  return
255
268
  }
256
269
 
257
- if (DD_TRACE_EXPERIMENTAL_STATE_TRACKING && !this._spanContext._tags['service.name']) {
270
+ if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_STATE_TRACKING && !this._spanContext._tags['service.name']) {
258
271
  log.error('Finishing invalid span: %s', this)
259
272
  }
260
273
 
261
274
  getIntegrationCounter('spans_finished', this._integrationName).inc()
262
275
  this._spanContext._tags['_dd.integration'] = this._integrationName
263
276
 
264
- if (DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
277
+ if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
265
278
  runtimeMetrics.decrement('runtime.node.spans.unfinished')
266
279
  runtimeMetrics.decrement('runtime.node.spans.unfinished.by.name', `span_name:${this._name}`)
267
280
  runtimeMetrics.increment('runtime.node.spans.finished')
@@ -274,7 +287,11 @@ class DatadogSpan {
274
287
  finishedRegistry.register(this, this._name)
275
288
  }
276
289
 
277
- finishTime = Number.parseFloat(finishTime) || this._getTime()
290
+ // Dominant call site is `span.finish()` with no argument; skip the
291
+ // `Number.parseFloat` round-trip for the undefined case.
292
+ finishTime = finishTime === undefined
293
+ ? this._getTime()
294
+ : (Number.parseFloat(finishTime) || this._getTime())
278
295
 
279
296
  this._duration = finishTime - this._startTime
280
297
  this._spanContext._trace.finished.push(this)
@@ -283,38 +300,29 @@ class DatadogSpan {
283
300
  this._processor.process(this)
284
301
  }
285
302
 
303
+ /**
304
+ * @param {Record<string, unknown>} [attributes]
305
+ */
286
306
  _sanitizeAttributes (attributes = {}) {
287
- const sanitizedAttributes = {}
288
-
289
- const addArrayOrScalarAttributes = (key, maybeArray) => {
290
- if (Array.isArray(maybeArray)) {
291
- for (const subkey in maybeArray) {
292
- addArrayOrScalarAttributes(`${key}.${subkey}`, maybeArray[subkey])
293
- }
294
- } else {
295
- const maybeScalar = maybeArray
296
- if (ALLOWED.has(typeof maybeScalar)) {
297
- // Wrap the value as a string if it's not already a string
298
- sanitizedAttributes[key] = typeof maybeScalar === 'string' ? maybeScalar : String(maybeScalar)
299
- } else {
300
- log.warn('Dropping span link attribute. It is not of an allowed type')
301
- }
302
- }
307
+ /** @type {Record<string, string>} */
308
+ const out = {}
309
+ for (const key of Object.keys(attributes)) {
310
+ addArrayOrScalarAttribute(out, key, attributes[key])
303
311
  }
304
-
305
- for (const [key, value] of Object.entries(attributes)) {
306
- addArrayOrScalarAttributes(key, value)
307
- }
308
- return sanitizedAttributes
312
+ return out
309
313
  }
310
314
 
315
+ /**
316
+ * @param {Record<string, unknown>} [attributes]
317
+ */
311
318
  _sanitizeEventAttributes (attributes = {}) {
312
319
  const sanitizedAttributes = {}
313
320
 
314
- for (const [key, value] of Object.entries(attributes)) {
321
+ for (const key of Object.keys(attributes)) {
322
+ const value = attributes[key]
315
323
  if (Array.isArray(value)) {
316
324
  const newArray = []
317
- for (const subvalue of Object.values(value)) {
325
+ for (const subvalue of value) {
318
326
  if (ALLOWED.has(typeof subvalue)) {
319
327
  newArray.push(subvalue)
320
328
  } else {
@@ -336,7 +344,7 @@ class DatadogSpan {
336
344
  let startTime
337
345
 
338
346
  let baggage = {}
339
- const propagationBehavior = this._parentTracer?._config?.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT
347
+ const propagationBehavior = this.#parentTracer._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT
340
348
  if (parent && parent._isRemote && propagationBehavior !== 'continue') {
341
349
  baggage = parent._baggageItems
342
350
  parent = null
@@ -30,6 +30,7 @@ class DatadogSpanContext {
30
30
  tags: {},
31
31
  }
32
32
  this._otelSpanContext = undefined
33
+ this._otelActiveSpan = undefined
33
34
  }
34
35
 
35
36
  [util.inspect.custom] () {
@@ -650,7 +650,7 @@ module.exports = {
650
650
  [GIT_TAG]: BITBUCKET_TAG,
651
651
  [GIT_REPOSITORY_URL]: BITBUCKET_GIT_SSH_ORIGIN || BITBUCKET_GIT_HTTP_ORIGIN,
652
652
  [CI_WORKSPACE_PATH]: BITBUCKET_CLONE_DIR,
653
- [CI_PIPELINE_ID]: BITBUCKET_PIPELINE_UUID && BITBUCKET_PIPELINE_UUID.replaceAll(/{|}/gm, ''),
653
+ [CI_PIPELINE_ID]: BITBUCKET_PIPELINE_UUID && BITBUCKET_PIPELINE_UUID.replaceAll(/[{}]/gm, ''),
654
654
  [GIT_PULL_REQUEST_BASE_BRANCH]: BITBUCKET_PR_DESTINATION_BRANCH,
655
655
  [PR_NUMBER]: BITBUCKET_PR_ID,
656
656
  }
@@ -53,14 +53,12 @@ function getCache (cacheKey) {
53
53
  try {
54
54
  const cacheFilePath = getCacheFilePath(cacheKey)
55
55
  if (!fs.existsSync(cacheFilePath)) {
56
- return null
56
+ return
57
57
  }
58
58
 
59
- const content = fs.readFileSync(cacheFilePath, 'utf8')
60
- return content
59
+ return fs.readFileSync(cacheFilePath, 'utf8')
61
60
  } catch (err) {
62
61
  log.error('Failed to read git cache', err)
63
- return null
64
62
  }
65
63
  }
66
64
 
@@ -87,7 +85,7 @@ function cachedExec (cmd, flags, options) {
87
85
  }
88
86
  const cacheKey = getCacheKey(cmd, flags)
89
87
  const cachedResult = getCache(cacheKey)
90
- if (cachedResult !== null) {
88
+ if (cachedResult !== undefined) {
91
89
  if (cachedResult.startsWith('__GIT_COMMAND_FAILED__')) {
92
90
  let error
93
91
  try {
@@ -318,6 +318,7 @@ module.exports = {
318
318
  parseAnnotations,
319
319
  getIsFaultyEarlyFlakeDetection,
320
320
  getEfdRetryCount,
321
+ getMaxEfdRetryCount,
321
322
  TEST_BROWSER_DRIVER,
322
323
  TEST_BROWSER_DRIVER_VERSION,
323
324
  TEST_BROWSER_NAME,
@@ -922,9 +923,7 @@ function getCoveredFilenamesFromCoverage (coverage) {
922
923
  .filter(filename => {
923
924
  const fileCoverage = coverageMap.fileCoverageFor(filename)
924
925
  const lineCoverage = fileCoverage.getLineCoverage()
925
- const isAnyLineExecuted = Object.entries(lineCoverage).some(([, numExecutions]) => !!numExecutions)
926
-
927
- return isAnyLineExecuted
926
+ return Object.entries(lineCoverage).some(([, numExecutions]) => !!numExecutions)
928
927
  })
929
928
  }
930
929
 
@@ -1045,6 +1044,22 @@ function getEfdRetryCount (durationMs, slowTestRetries) {
1045
1044
  return 0 // ≥ 5 min — abort
1046
1045
  }
1047
1046
 
1047
+ /**
1048
+ * Returns the maximum retry count configured by the backend for EFD.
1049
+ *
1050
+ * @param {Record<string, number>} slowTestRetries e.g. { '5s': 10, '10s': 5, '30s': 3, '5m': 2 }
1051
+ * @returns {number}
1052
+ */
1053
+ function getMaxEfdRetryCount (slowTestRetries) {
1054
+ let maxRetries = 0
1055
+ for (const retryCount of Object.values(slowTestRetries || {})) {
1056
+ if (retryCount > maxRetries) {
1057
+ maxRetries = retryCount
1058
+ }
1059
+ }
1060
+ return maxRetries
1061
+ }
1062
+
1048
1063
  function getIsFaultyEarlyFlakeDetection (projectSuites, testsBySuiteName, faultyThresholdPercentage) {
1049
1064
  let newSuites = 0
1050
1065
  for (const suite of projectSuites) {
@@ -1276,10 +1291,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) {
1276
1291
  let bestScore = Infinity
1277
1292
  for (const branch of Object.keys(metrics)) {
1278
1293
  const score = metrics[branch].ahead
1279
- if (score < bestScore) {
1280
- bestScore = score
1281
- bestBranch = branch
1282
- } else if (score === bestScore && isDefaultBranch(branch)) {
1294
+ if (score < bestScore || score === bestScore && isDefaultBranch(branch)) {
1283
1295
  bestScore = score
1284
1296
  bestBranch = branch
1285
1297
  }
@@ -108,11 +108,9 @@ function calculateHttpEndpoint (url) {
108
108
  return element
109
109
  })
110
110
 
111
- const endpoint = normalizedElements.length > 0
111
+ return normalizedElements.length > 0
112
112
  ? '/' + normalizedElements.join('/')
113
113
  : '/'
114
-
115
- return endpoint
116
114
  }
117
115
 
118
116
  function filterSensitiveInfoFromRepository (repositoryUrl) {