dd-trace 5.106.0 → 5.108.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 (105) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +9 -11
  3. package/packages/datadog-core/src/storage.js +47 -48
  4. package/packages/datadog-esbuild/index.js +6 -1
  5. package/packages/datadog-instrumentations/src/ai.js +12 -3
  6. package/packages/datadog-instrumentations/src/body-parser.js +5 -2
  7. package/packages/datadog-instrumentations/src/connect.js +3 -2
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
  9. package/packages/datadog-instrumentations/src/cucumber.js +7 -0
  10. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
  11. package/packages/datadog-instrumentations/src/express-session.js +12 -11
  12. package/packages/datadog-instrumentations/src/express.js +24 -20
  13. package/packages/datadog-instrumentations/src/fastify.js +18 -6
  14. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
  15. package/packages/datadog-instrumentations/src/http/client.js +9 -12
  16. package/packages/datadog-instrumentations/src/http/server.js +30 -16
  17. package/packages/datadog-instrumentations/src/http2/client.js +15 -12
  18. package/packages/datadog-instrumentations/src/http2/server.js +15 -8
  19. package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
  20. package/packages/datadog-instrumentations/src/jest.js +143 -73
  21. package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
  23. package/packages/datadog-instrumentations/src/multer.js +3 -2
  24. package/packages/datadog-instrumentations/src/mysql2.js +34 -0
  25. package/packages/datadog-instrumentations/src/net.js +8 -6
  26. package/packages/datadog-instrumentations/src/openai.js +19 -7
  27. package/packages/datadog-instrumentations/src/pg.js +19 -0
  28. package/packages/datadog-instrumentations/src/router.js +12 -10
  29. package/packages/datadog-instrumentations/src/vitest.js +29 -4
  30. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
  31. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
  32. package/packages/datadog-plugin-cucumber/src/index.js +2 -0
  33. package/packages/datadog-plugin-cypress/src/support.js +31 -1
  34. package/packages/datadog-plugin-http/src/client.js +0 -3
  35. package/packages/datadog-plugin-http/src/server.js +11 -1
  36. package/packages/datadog-plugin-mocha/src/index.js +2 -0
  37. package/packages/datadog-plugin-pg/src/index.js +10 -0
  38. package/packages/dd-trace/src/aiguard/index.js +34 -15
  39. package/packages/dd-trace/src/aiguard/sdk.js +34 -3
  40. package/packages/dd-trace/src/aiguard/tags.js +6 -0
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +3 -2
  42. package/packages/dd-trace/src/appsec/iast/index.js +3 -2
  43. package/packages/dd-trace/src/appsec/rasp/ssrf.js +2 -1
  44. package/packages/dd-trace/src/appsec/reporter.js +1 -1
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
  46. package/packages/dd-trace/src/config/defaults.js +14 -0
  47. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -1
  48. package/packages/dd-trace/src/config/helper.js +1 -0
  49. package/packages/dd-trace/src/config/index.js +5 -9
  50. package/packages/dd-trace/src/config/parsers.js +8 -0
  51. package/packages/dd-trace/src/config/supported-configurations.json +20 -6
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  53. package/packages/dd-trace/src/datastreams/writer.js +1 -2
  54. package/packages/dd-trace/src/debugger/config.js +1 -1
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
  56. package/packages/dd-trace/src/debugger/index.js +1 -2
  57. package/packages/dd-trace/src/dogstatsd.js +2 -3
  58. package/packages/dd-trace/src/encode/0.4.js +49 -41
  59. package/packages/dd-trace/src/encode/agentless-json.js +5 -1
  60. package/packages/dd-trace/src/encode/tags-processors.js +14 -0
  61. package/packages/dd-trace/src/exporters/agent/index.js +1 -2
  62. package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
  63. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
  64. package/packages/dd-trace/src/exporters/common/request.js +26 -0
  65. package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
  66. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
  67. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
  68. package/packages/dd-trace/src/llmobs/sdk.js +4 -1
  69. package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
  70. package/packages/dd-trace/src/llmobs/tagger.js +5 -3
  71. package/packages/dd-trace/src/llmobs/util.js +54 -0
  72. package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
  73. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  74. package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
  75. package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
  76. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
  77. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
  78. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
  79. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +3 -2
  80. package/packages/dd-trace/src/opentracing/span.js +23 -18
  81. package/packages/dd-trace/src/opentracing/tracer.js +16 -12
  82. package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
  83. package/packages/dd-trace/src/priority_sampler.js +6 -5
  84. package/packages/dd-trace/src/profiling/config.js +3 -2
  85. package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
  86. package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
  87. package/packages/dd-trace/src/proxy.js +13 -10
  88. package/packages/dd-trace/src/remote_config/index.js +1 -2
  89. package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
  90. package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
  91. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
  92. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
  93. package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
  94. package/packages/dd-trace/src/span_format.js +33 -25
  95. package/packages/dd-trace/src/span_stats.js +1 -1
  96. package/packages/dd-trace/src/startup-log.js +1 -2
  97. package/packages/dd-trace/src/telemetry/send-data.js +1 -1
  98. package/packages/dd-trace/src/tracer.js +1 -1
  99. package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
  100. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  101. package/vendor/dist/protobufjs/index.js +1 -1
  102. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  103. package/vendor/dist/shell-quote/index.js +1 -1
  104. package/packages/dd-trace/src/agent/url.js +0 -28
  105. package/scripts/preinstall.js +0 -34
@@ -50,7 +50,7 @@ class Instrument {
50
50
  * Creates a measurement object for recording metric values.
51
51
  * @param {string} type - Metric type from METRIC_TYPES
52
52
  * @param {number} value - Numeric value to record
53
- * @param {Attributes} attributes - Key-value pairs for metric dimensions
53
+ * @param {Attributes} [attributes] - Key-value pairs for metric dimensions
54
54
  * @returns {Measurement} Measurement object with metadata and timestamp
55
55
  */
56
56
  createMeasurement (type, value, attributes) {
@@ -73,9 +73,9 @@ class Instrument {
73
73
  * @class Counter
74
74
  */
75
75
  class Counter extends Instrument {
76
- add (value, attributes = {}) {
76
+ add (value, attributes) {
77
77
  if (value < 0) return
78
- this.reader?.record(this.createMeasurement(METRIC_TYPES.COUNTER, value, attributes))
78
+ this.reader.record(this.createMeasurement(METRIC_TYPES.COUNTER, value, attributes))
79
79
  }
80
80
  }
81
81
 
@@ -85,8 +85,8 @@ class Counter extends Instrument {
85
85
  * @class UpDownCounter
86
86
  */
87
87
  class UpDownCounter extends Instrument {
88
- add (value, attributes = {}) {
89
- this.reader?.record(this.createMeasurement(METRIC_TYPES.UPDOWNCOUNTER, value, attributes))
88
+ add (value, attributes) {
89
+ this.reader.record(this.createMeasurement(METRIC_TYPES.UPDOWNCOUNTER, value, attributes))
90
90
  }
91
91
  }
92
92
 
@@ -96,9 +96,9 @@ class UpDownCounter extends Instrument {
96
96
  * @class Histogram
97
97
  */
98
98
  class Histogram extends Instrument {
99
- record (value, attributes = {}) {
99
+ record (value, attributes) {
100
100
  if (value < 0) return
101
- this.reader?.record(this.createMeasurement(METRIC_TYPES.HISTOGRAM, value, attributes))
101
+ this.reader.record(this.createMeasurement(METRIC_TYPES.HISTOGRAM, value, attributes))
102
102
  }
103
103
  }
104
104
 
@@ -108,8 +108,8 @@ class Histogram extends Instrument {
108
108
  * @class Gauge
109
109
  */
110
110
  class Gauge extends Instrument {
111
- record (value, attributes = {}) {
112
- this.reader?.record(this.createMeasurement(METRIC_TYPES.GAUGE, value, attributes))
111
+ record (value, attributes) {
112
+ this.reader.record(this.createMeasurement(METRIC_TYPES.GAUGE, value, attributes))
113
113
  }
114
114
  }
115
115
 
@@ -136,7 +136,7 @@ class ObservableInstrument extends Instrument {
136
136
  addCallback (callback) {
137
137
  if (typeof callback !== 'function') return
138
138
  this.#callbacks.push(callback)
139
- this.reader?.observableInstruments.add(this)
139
+ this.reader.observableInstruments.add(this)
140
140
  }
141
141
 
142
142
  /**
@@ -150,7 +150,7 @@ class ObservableInstrument extends Instrument {
150
150
  this.#callbacks.splice(index, 1)
151
151
  if (this.#callbacks.length === 0) {
152
152
  // Remove instrument from collection when no callbacks remain
153
- this.reader?.observableInstruments.delete(this)
153
+ this.reader.observableInstruments.delete(this)
154
154
  }
155
155
  }
156
156
  }
@@ -163,8 +163,8 @@ class ObservableInstrument extends Instrument {
163
163
  collect () {
164
164
  const observations = []
165
165
  const observableResult = {
166
- observe: (value, attributes = {}) => {
167
- observations.push(this.createMeasurement(this.#type, value, attributes))
166
+ observe: (value, attributes) => {
167
+ observations.push(this.createObservation(value, attributes))
168
168
  },
169
169
  }
170
170
 
@@ -179,6 +179,18 @@ class ObservableInstrument extends Instrument {
179
179
 
180
180
  return observations
181
181
  }
182
+
183
+ /**
184
+ * Builds a measurement for this instrument's metric type. Keeps the type
185
+ * encapsulated so callers (e.g. batch observable callbacks) don't read it.
186
+ *
187
+ * @param {number} value
188
+ * @param {Attributes} attributes
189
+ * @returns {Measurement}
190
+ */
191
+ createObservation (value, attributes) {
192
+ return this.createMeasurement(this.#type, value, attributes)
193
+ }
182
194
  }
183
195
 
184
196
  /**
@@ -219,6 +231,7 @@ module.exports = {
219
231
  UpDownCounter,
220
232
  Histogram,
221
233
  Gauge,
234
+ ObservableInstrument,
222
235
  ObservableGauge,
223
236
  ObservableCounter,
224
237
  ObservableUpDownCounter,
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { VERSION: packageVersion } = require('../../../../../version')
4
- const log = require('../../log')
5
4
  const {
6
5
  Counter, UpDownCounter, Histogram, Gauge, ObservableGauge, ObservableCounter, ObservableUpDownCounter,
7
6
  } = require('./instruments')
@@ -148,23 +147,21 @@ class Meter {
148
147
  }
149
148
 
150
149
  /**
151
- * Adds a batch observable callback (not implemented).
150
+ * Registers a batch observable callback for the given observables.
152
151
  *
153
- * @param {Function} callback - Batch observable callback
154
- * @param {Array} observables - Array of observable instruments
152
+ * @param {Function} callback
153
+ * @param {Array} observables
155
154
  */
156
155
  addBatchObservableCallback (callback, observables) {
157
- log.warn('addBatchObservableCallback is not implemented')
156
+ this.meterProvider.reader.addBatchObservableCallback(callback, observables)
158
157
  }
159
158
 
160
159
  /**
161
- * Removes a batch observable callback (not implemented).
162
- *
163
- * @param {Function} callback - Batch observable callback
164
- * @param {Array} observables - Array of observable instruments
160
+ * @param {Function} callback
161
+ * @param {Array} observables
165
162
  */
166
163
  removeBatchObservableCallback (callback, observables) {
167
- log.warn('removeBatchObservableCallback is not implemented')
164
+ this.meterProvider.reader.removeBatchObservableCallback(callback, observables)
168
165
  }
169
166
  }
170
167
 
@@ -5,6 +5,7 @@ const { stableStringify } = require('../otlp/otlp_transformer_base')
5
5
  const {
6
6
  METRIC_TYPES, TEMPORALITY, DEFAULT_HISTOGRAM_BUCKETS, DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE,
7
7
  } = require('./constants')
8
+ const { ObservableInstrument } = require('./instruments')
8
9
 
9
10
  /**
10
11
  * @typedef {import('@opentelemetry/api').Attributes} Attributes
@@ -102,6 +103,7 @@ class PeriodicMetricReader {
102
103
  #isShutdown = false
103
104
  #exportInterval
104
105
  #aggregator
106
+ #batchCallbacks = []
105
107
 
106
108
  /**
107
109
  * Creates a new PeriodicMetricReader instance.
@@ -132,6 +134,66 @@ class PeriodicMetricReader {
132
134
  this.#measurements.push(measurement)
133
135
  }
134
136
 
137
+ /**
138
+ * Registers a batch observable callback. Mirrors
139
+ * `@opentelemetry/sdk-metrics` `ObservableRegistry.addBatchCallback`.
140
+ *
141
+ * @param {Function} callback
142
+ * @param {Array} observables
143
+ */
144
+ addBatchObservableCallback (callback, observables) {
145
+ if (typeof callback !== 'function') return
146
+ const instruments = new Set(observables?.filter(isObservableInstrument))
147
+ if (instruments.size === 0) return
148
+ if (this.#findBatchCallback(callback, instruments) !== -1) return
149
+ this.#batchCallbacks.push({ callback, instruments })
150
+ }
151
+
152
+ /**
153
+ * @param {Function} callback
154
+ * @param {Array} observables
155
+ */
156
+ removeBatchObservableCallback (callback, observables) {
157
+ const instruments = new Set(observables?.filter(isObservableInstrument))
158
+ const idx = this.#findBatchCallback(callback, instruments)
159
+ if (idx !== -1) this.#batchCallbacks.splice(idx, 1)
160
+ }
161
+
162
+ /**
163
+ * @param {Function} callback
164
+ * @param {Set} instruments
165
+ * @returns {number} index in #batchCallbacks, or -1
166
+ */
167
+ #findBatchCallback (callback, instruments) {
168
+ return this.#batchCallbacks.findIndex(record =>
169
+ record.callback === callback && setEquals(record.instruments, instruments))
170
+ }
171
+
172
+ /**
173
+ * Invokes batch observable callbacks and returns the produced measurements.
174
+ *
175
+ * @returns {Measurement[]}
176
+ */
177
+ #collectBatchObservables () {
178
+ if (this.#batchCallbacks.length === 0) return []
179
+ const out = []
180
+ for (const { callback, instruments } of this.#batchCallbacks) {
181
+ const result = {
182
+ observe: (instrument, value, attributes) => {
183
+ if (instruments.has(instrument)) {
184
+ out.push(instrument.createObservation(value, attributes))
185
+ }
186
+ },
187
+ }
188
+ try {
189
+ callback(result)
190
+ } catch (e) {
191
+ log.error('Error running batch observable callback', e)
192
+ }
193
+ }
194
+ return out
195
+ }
196
+
135
197
  /**
136
198
  * Forces an immediate collection and export of all metrics.
137
199
  * @returns {void}
@@ -210,6 +272,17 @@ class PeriodicMetricReader {
210
272
  }
211
273
  }
212
274
 
275
+ const batchMeasurements = this.#collectBatchObservables()
276
+ if (batchMeasurements.length > 0) {
277
+ const remainingCapacity = DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE - allMeasurements.length
278
+ if (batchMeasurements.length <= remainingCapacity) {
279
+ allMeasurements.push(...batchMeasurements)
280
+ } else {
281
+ allMeasurements.push(...batchMeasurements.slice(0, remainingCapacity))
282
+ this.#droppedCount += batchMeasurements.length - remainingCapacity
283
+ }
284
+ }
285
+
213
286
  if (this.#droppedCount > 0) {
214
287
  log.warn('Metric queue exceeded limit (max: %d). Dropping %d measurements.',
215
288
  DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE, this.#droppedCount)
@@ -555,4 +628,23 @@ class MetricAggregator {
555
628
  }
556
629
  }
557
630
 
631
+ /**
632
+ * @param {object} x
633
+ * @returns {boolean}
634
+ */
635
+ function isObservableInstrument (x) {
636
+ return x instanceof ObservableInstrument
637
+ }
638
+
639
+ /**
640
+ * @param {Set} a
641
+ * @param {Set} b
642
+ * @returns {boolean}
643
+ */
644
+ function setEquals (a, b) {
645
+ if (a.size !== b.size) return false
646
+ for (const x of a) if (!b.has(x)) return false
647
+ return true
648
+ }
649
+
558
650
  module.exports = PeriodicMetricReader
@@ -4,6 +4,7 @@ const OtlpTransformerBase = require('../otlp/otlp_transformer_base')
4
4
  const { getProtobufTypes } = require('../otlp/protobuf_loader')
5
5
  const { VERSION } = require('../../../../../version')
6
6
  const id = require('../../id')
7
+ const { eventTimeNano } = require('../../encode/tags-processors')
7
8
 
8
9
  const { protoSpanKind } = getProtobufTypes()
9
10
  const SPAN_KIND_UNSPECIFIED = protoSpanKind.values.SPAN_KIND_UNSPECIFIED
@@ -33,7 +34,7 @@ const TRACE_ID_128 = '_dd.p.tid'
33
34
  *
34
35
  * @typedef {object} DDSpanEvent
35
36
  * @property {string} name - Event name
36
- * @property {number} time_unix_nano - Event time in nanoseconds since epoch
37
+ * @property {number} startTime - Event start time in milliseconds (sub-ms precision)
37
38
  * @property {Record<string, string | number | boolean>} [attributes] - Event attributes
38
39
  *
39
40
  * @typedef {object} DDFormattedSpan
@@ -274,7 +275,7 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
274
275
  */
275
276
  #transformEvent (event) {
276
277
  return {
277
- timeUnixNano: event.time_unix_nano,
278
+ timeUnixNano: eventTimeNano(event),
278
279
  name: event.name || '',
279
280
  attributes: this.transformAttributes(event.attributes ?? {}),
280
281
  droppedAttributesCount: 0,
@@ -38,20 +38,26 @@ const tagsUpdateCh = channel('dd-trace:span:tags:update')
38
38
  // Module-scope so we don't allocate a fresh recursive closure on every
39
39
  // `addLink` / `addEvent`.
40
40
  /**
41
- * @param {Record<string, string>} out
41
+ * @param {Record<string, string> | undefined} out Accumulator, created lazily
42
+ * on the first surviving entry so an all-dropped set stays `undefined`.
42
43
  * @param {string} key
43
44
  * @param {unknown} value
45
+ * @returns {Record<string, string> | undefined}
44
46
  */
45
47
  function addArrayOrScalarAttribute (out, key, value) {
46
48
  if (Array.isArray(value)) {
47
49
  for (let i = 0; i < value.length; i++) {
48
- addArrayOrScalarAttribute(out, `${key}.${i}`, value[i])
50
+ out = addArrayOrScalarAttribute(out, `${key}.${i}`, value[i])
49
51
  }
50
- } else if (ALLOWED.has(typeof value)) {
52
+ return out
53
+ }
54
+ if (ALLOWED.has(typeof value)) {
55
+ out ??= {}
51
56
  out[key] = typeof value === 'string' ? value : String(value)
52
- } else {
53
- log.warn('Dropping span link attribute. It is not of an allowed type')
57
+ return out
54
58
  }
59
+ log.warn('Dropping span link attribute. It is not of an allowed type')
60
+ return out
55
61
  }
56
62
 
57
63
  function getIntegrationCounter (event, integration) {
@@ -79,12 +85,6 @@ class DatadogSpan {
79
85
 
80
86
  const operationName = fields.operationName
81
87
  const parent = fields.parent || null
82
- // Stay on `Object.assign({}, src)` for backportability: V8 12+ (Node 22 /
83
- // 24) inlines `{ ...src }` and beats `Object.assign` here, but on V8 10.2
84
- // / 11.3 (Node 18 / 20) the spread takes a generic runtime path and slows
85
- // `spans-finish-*` by ~140%. Revisit once those LTS lines drop.
86
- // eslint-disable-next-line prefer-object-spread
87
- const tags = Object.assign({}, fields.tags)
88
88
  const hostname = fields.hostname
89
89
 
90
90
  this.#parentTracer = tracer
@@ -106,7 +106,7 @@ class DatadogSpan {
106
106
 
107
107
  this._spanContext = this._createContext(parent, fields)
108
108
  this._spanContext._name = operationName
109
- Object.assign(this._spanContext.getTags(), tags)
109
+ if (fields.tags) Object.assign(this._spanContext.getTags(), fields.tags)
110
110
  this._spanContext._hostname = hostname
111
111
 
112
112
  this._spanContext._trace.started.push(this)
@@ -348,21 +348,24 @@ class DatadogSpan {
348
348
 
349
349
  /**
350
350
  * @param {Record<string, unknown>} [attributes]
351
+ * @returns {Record<string, string> | undefined} `undefined` when nothing
352
+ * survives, so `extractSpanLinks` omits the slot without an emptiness probe.
351
353
  */
352
354
  _sanitizeAttributes (attributes = {}) {
353
- /** @type {Record<string, string>} */
354
- const out = {}
355
+ let out
355
356
  for (const key of Object.keys(attributes)) {
356
- addArrayOrScalarAttribute(out, key, attributes[key])
357
+ out = addArrayOrScalarAttribute(out, key, attributes[key])
357
358
  }
358
359
  return out
359
360
  }
360
361
 
361
362
  /**
362
363
  * @param {Record<string, unknown>} [attributes]
364
+ * @returns {Record<string, unknown> | undefined} `undefined` when nothing
365
+ * survives, so the encoders skip the slot without an emptiness probe.
363
366
  */
364
367
  _sanitizeEventAttributes (attributes = {}) {
365
- const sanitizedAttributes = {}
368
+ let sanitizedAttributes
366
369
 
367
370
  for (const key of Object.keys(attributes)) {
368
371
  const value = attributes[key]
@@ -375,8 +378,10 @@ class DatadogSpan {
375
378
  log.warn('Dropping span event attribute. It is not of an allowed type')
376
379
  }
377
380
  }
381
+ sanitizedAttributes ??= {}
378
382
  sanitizedAttributes[key] = newArray
379
383
  } else if (ALLOWED.has(typeof value)) {
384
+ sanitizedAttributes ??= {}
380
385
  sanitizedAttributes[key] = value
381
386
  } else {
382
387
  log.warn('Dropping span event attribute. It is not of an allowed type')
@@ -389,7 +394,7 @@ class DatadogSpan {
389
394
  let spanContext
390
395
  let startTime
391
396
 
392
- let baggage = {}
397
+ let baggage
393
398
  const propagationBehavior = this.#parentTracer._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT
394
399
  if (parent && parent._isRemote && propagationBehavior !== 'continue') {
395
400
  baggage = parent._baggageItems
@@ -431,7 +436,7 @@ class DatadogSpan {
431
436
  }
432
437
 
433
438
  if (propagationBehavior === 'restart') {
434
- spanContext._baggageItems = baggage
439
+ spanContext._baggageItems = baggage ?? {}
435
440
  }
436
441
  }
437
442
 
@@ -23,6 +23,8 @@ class DatadogTracer {
23
23
  constructor (config, prioritySampler) {
24
24
  this._config = config
25
25
  this._service = config.service
26
+ // Lowercased once for span_format's per-span base-service comparison.
27
+ this.serviceLower = typeof config.service === 'string' ? config.service.toLowerCase() : ''
26
28
  this._version = config.version
27
29
  this._env = config.env
28
30
  this._logInjection = config.logInjection
@@ -64,21 +66,9 @@ class DatadogTracer {
64
66
  ? getContext(options.childOf)
65
67
  : getParent(options.references)
66
68
 
67
- // as per spec, allow the setting of service name through options
68
- const tags = {
69
- 'service.name': options?.tags?.service ? String(options.tags.service) : this._service,
70
- }
71
-
72
- // As per unified service tagging spec if a span is created with a service name different from the global
73
- // service name it will not inherit the global version value
74
- if (options?.tags?.service && options.tags.service !== this._service) {
75
- options.tags.version = undefined
76
- }
77
-
78
69
  const span = new Span(this, this._processor, this._prioritySampler, {
79
70
  operationName: options.operationName || name,
80
71
  parent,
81
- tags,
82
72
  startTime: options.startTime,
83
73
  hostname: this._hostname,
84
74
  traceId128BitGenerationEnabled: this._traceId128BitGenerationEnabled,
@@ -86,6 +76,20 @@ class DatadogTracer {
86
76
  links: options.links,
87
77
  }, this._debug)
88
78
 
79
+ // As per unified service tagging spec if a span is created with a service name different from the global
80
+ // service name it will not inherit the global version value
81
+ const ctx = span.context()
82
+ if (options.tags?.service) {
83
+ if (options.tags.service !== this._service) options.tags.version = undefined
84
+ // as per spec, allow the setting of service name through options; set it
85
+ // after all tags are merged so config/options values take precedence
86
+ // eslint-disable-next-line eslint-rules/eslint-prefer-set-service-name
87
+ ctx.setTag('service.name', String(options.tags.service))
88
+ } else {
89
+ // eslint-disable-next-line eslint-rules/eslint-prefer-set-service-name
90
+ ctx.setTag('service.name', this._service)
91
+ }
92
+
89
93
  span.addTags(this._config.tags)
90
94
  span.addTags(options.tags)
91
95
 
@@ -143,6 +143,7 @@ module.exports = class CiPlugin extends Plugin {
143
143
  this.fileLineToProbeId = new Map()
144
144
  this.rootDir = process.cwd() // fallback in case :session:start events are not emitted
145
145
  this._testSuiteSpansByTestSuite = new Map()
146
+ this._pendingWorkerTracesByTestSuite = new Map()
146
147
  this._pendingRequestErrorTags = []
147
148
 
148
149
  this.addSub(`ci:${this.constructor.id}:library-configuration`, (ctx) => {
@@ -352,52 +353,8 @@ module.exports = class CiPlugin extends Plugin {
352
353
  const formattedTraces = JSON.parse(traces)
353
354
 
354
355
  for (const trace of formattedTraces) {
355
- for (const span of trace) {
356
- span.span_id = id(span.span_id)
357
- span.trace_id = id(span.trace_id)
358
- span.parent_id = id(span.parent_id)
359
-
360
- if (span.name?.startsWith(`${this.constructor.id}.`)) {
361
- span.meta[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
362
- if (span.name === `${this.constructor.id}.test` || span.name === `${this.constructor.id}.test_suite`) {
363
- Object.assign(span.meta, getSessionItrSkippingEnabledTags(this.testSessionSpan))
364
- }
365
- // augment with git information (since it will not be available in the worker)
366
- for (const key in this.testEnvironmentMetadata) {
367
- // CAREFUL: this bypasses the metadata/metrics distinction
368
- // Be careful not to pass numbers in `meta`
369
- if (key.startsWith('git.')) {
370
- span.meta[key] = this.testEnvironmentMetadata[key]
371
- }
372
- }
373
- }
374
-
375
- // Only test hooks run in the cucumber worker, so the test events do not have the
376
- // test session, test module and test suite ids. We have to update them here.
377
- if (span.name === 'cucumber.test' || span.name === 'mocha.test') {
378
- const testSuite = span.meta[TEST_SUITE]
379
- const testSuiteSpan = this._testSuiteSpansByTestSuite.get(testSuite)
380
- if (!testSuiteSpan) {
381
- log.warn('Test suite span not found for test span with test suite %s', testSuite)
382
- continue
383
- }
384
-
385
- const testSuiteTags = getTestSuiteLevelVisibilityTags(testSuiteSpan, this.constructor.id)
386
- span.meta = {
387
- ...span.meta,
388
- ...testSuiteTags,
389
- ...getSessionRequestErrorTags(this.testSessionSpan),
390
- }
391
- }
392
-
393
- // Jest and Vitest worker test spans are serialized in the worker and may not include
394
- // request error tags; add them from the session span in the main process.
395
- if ((span.name === 'jest.test' || span.name === 'vitest.test' || span.name === 'vitest.test_suite') &&
396
- this.testSessionSpan) {
397
- Object.assign(span.meta, getSessionRequestErrorTags(this.testSessionSpan))
398
- }
399
- }
400
- this.tracer._exporter.export(trace)
356
+ this._prepareWorkerTrace(trace)
357
+ this._exportWorkerTraceOrBuffer(trace)
401
358
  }
402
359
  })
403
360
 
@@ -502,6 +459,134 @@ module.exports = class CiPlugin extends Plugin {
502
459
  return getSessionItrSkippingEnabledTags(this.testSessionSpan)
503
460
  }
504
461
 
462
+ /**
463
+ * Normalizes worker trace identifiers and adds process-level metadata before exporting or buffering.
464
+ *
465
+ * @param {object[]} trace - Worker trace spans decoded from the worker payload.
466
+ */
467
+ _prepareWorkerTrace (trace) {
468
+ for (const span of trace) {
469
+ span.span_id = id(span.span_id)
470
+ span.trace_id = id(span.trace_id)
471
+ span.parent_id = id(span.parent_id)
472
+
473
+ if (span.name?.startsWith(`${this.constructor.id}.`)) {
474
+ span.meta[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
475
+ if (span.name === `${this.constructor.id}.test` || span.name === `${this.constructor.id}.test_suite`) {
476
+ Object.assign(span.meta, getSessionItrSkippingEnabledTags(this.testSessionSpan))
477
+ }
478
+ // augment with git information (since it will not be available in the worker)
479
+ for (const key in this.testEnvironmentMetadata) {
480
+ // CAREFUL: this bypasses the metadata/metrics distinction
481
+ // Be careful not to pass numbers in `meta`
482
+ if (key.startsWith('git.')) {
483
+ span.meta[key] = this.testEnvironmentMetadata[key]
484
+ }
485
+ }
486
+ }
487
+
488
+ // Jest and Vitest worker test spans are serialized in the worker and may not include
489
+ // request error tags; add them from the session span in the main process.
490
+ if ((span.name === 'jest.test' || span.name === 'vitest.test' || span.name === 'vitest.test_suite') &&
491
+ this.testSessionSpan) {
492
+ Object.assign(span.meta, getSessionRequestErrorTags(this.testSessionSpan))
493
+ }
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Adds suite-level CI Visibility tags to worker test spans when their suite span is available.
499
+ *
500
+ * @param {object[]} trace - Worker trace spans.
501
+ * @returns {string|undefined} Missing test suite name, if the trace cannot be exported yet.
502
+ */
503
+ _addSuiteTagsToWorkerTrace (trace) {
504
+ for (const span of trace) {
505
+ // Only test hooks run in Cucumber and Mocha workers, so the test events do not have the
506
+ // test session, test module and test suite ids. We have to update them in the main process.
507
+ if (span.name !== 'cucumber.test' && span.name !== 'mocha.test') continue
508
+
509
+ const testSuite = span.meta[TEST_SUITE]
510
+ const testSuiteSpan = this._testSuiteSpansByTestSuite.get(testSuite)
511
+ if (!testSuiteSpan) return testSuite
512
+
513
+ const testSuiteTags = getTestSuiteLevelVisibilityTags(testSuiteSpan, this.constructor.id)
514
+ span.meta = {
515
+ ...span.meta,
516
+ ...testSuiteTags,
517
+ ...getSessionRequestErrorTags(this.testSessionSpan),
518
+ }
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Stores a worker trace until the matching test suite span exists in the main process.
524
+ *
525
+ * @param {string} testSuite - Test suite path used as the pending trace key.
526
+ * @param {object[]} trace - Worker trace spans.
527
+ */
528
+ _bufferWorkerTrace (testSuite, trace) {
529
+ let pendingTraces = this._pendingWorkerTracesByTestSuite.get(testSuite)
530
+ if (!pendingTraces) {
531
+ pendingTraces = []
532
+ this._pendingWorkerTracesByTestSuite.set(testSuite, pendingTraces)
533
+ }
534
+ pendingTraces.push(trace)
535
+ }
536
+
537
+ /**
538
+ * Exports a worker trace immediately, or buffers it if suite-level tags cannot be added yet.
539
+ *
540
+ * @param {object[]} trace - Worker trace spans.
541
+ */
542
+ _exportWorkerTraceOrBuffer (trace) {
543
+ const missingTestSuite = this._addSuiteTagsToWorkerTrace(trace)
544
+ if (missingTestSuite) {
545
+ this._bufferWorkerTrace(missingTestSuite, trace)
546
+ return
547
+ }
548
+ this.tracer._exporter.export(trace)
549
+ }
550
+
551
+ /**
552
+ * Exports buffered worker traces for a suite after its suite span has been created.
553
+ *
554
+ * @param {string} testSuite - Test suite path that may now have pending worker traces.
555
+ */
556
+ _exportPendingWorkerTracesForTestSuite (testSuite) {
557
+ const pendingTraces = this._pendingWorkerTracesByTestSuite.get(testSuite)
558
+ if (!pendingTraces) return
559
+
560
+ this._pendingWorkerTracesByTestSuite.delete(testSuite)
561
+ for (const trace of pendingTraces) {
562
+ this._exportWorkerTraceOrBuffer(trace)
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Drains all buffered worker traces, falling back to the previous unaugmented export behavior
568
+ * if a matching suite span never appears.
569
+ */
570
+ _exportPendingWorkerTraces () {
571
+ if (!this._pendingWorkerTracesByTestSuite.size) return
572
+
573
+ const pendingTraces = new Set()
574
+ for (const traces of this._pendingWorkerTracesByTestSuite.values()) {
575
+ for (const trace of traces) {
576
+ pendingTraces.add(trace)
577
+ }
578
+ }
579
+ this._pendingWorkerTracesByTestSuite.clear()
580
+
581
+ for (const trace of pendingTraces) {
582
+ const missingTestSuite = this._addSuiteTagsToWorkerTrace(trace)
583
+ if (missingTestSuite) {
584
+ log.warn('Test suite span not found for test span with test suite %s', missingTestSuite)
585
+ }
586
+ this.tracer._exporter.export(trace)
587
+ }
588
+ }
589
+
505
590
  /**
506
591
  * @param {import('../config/config-base')} config - Tracer configuration
507
592
  * @param {boolean} shouldGetEnvironmentData - Whether to get environment data
@@ -333,11 +333,12 @@ class PrioritySampler {
333
333
  if (!trace.tags[DECISION_MAKER_KEY]) {
334
334
  trace.tags[DECISION_MAKER_KEY] = `-${mechanism}`
335
335
  }
336
- } else if (DECISION_MAKER_KEY in trace.tags) {
337
- // Guard the `delete` so the common drop path doesn't pay the V8
338
- // dictionary-mode transition unless a prior keep decision actually
339
- // set the tag.
340
- delete trace.tags[DECISION_MAKER_KEY]
336
+ } else if (trace.tags[DECISION_MAKER_KEY] !== undefined) {
337
+ // Clear by assigning undefined rather than deleting: `delete` drops
338
+ // trace.tags into V8 dictionary (slow) mode for the per-trace extract
339
+ // and propagation scans that follow. Both skip undefined values, so the
340
+ // emitted meta and injected headers are unchanged.
341
+ trace.tags[DECISION_MAKER_KEY] = undefined
341
342
  }
342
343
  }
343
344