dd-trace 5.87.0 → 5.88.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 (77) hide show
  1. package/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/index.d.ts +225 -4
  5. package/package.json +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +54 -90
  7. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
  15. package/packages/datadog-instrumentations/src/jest.js +76 -12
  16. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  17. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  18. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  19. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  20. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  21. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  22. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -0
  24. package/packages/datadog-plugin-cypress/src/support.js +48 -8
  25. package/packages/datadog-plugin-jest/src/index.js +12 -2
  26. package/packages/datadog-plugin-jest/src/util.js +2 -1
  27. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  28. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  29. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  30. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  31. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  32. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  33. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  34. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  35. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  36. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  37. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  38. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  39. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  40. package/packages/dd-trace/src/azure_metadata.js +0 -2
  41. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  42. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
  43. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  44. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  45. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  46. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  47. package/packages/dd-trace/src/config/defaults.js +148 -197
  48. package/packages/dd-trace/src/config/helper.js +43 -1
  49. package/packages/dd-trace/src/config/index.js +36 -14
  50. package/packages/dd-trace/src/config/supported-configurations.json +4115 -512
  51. package/packages/dd-trace/src/constants.js +0 -2
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  53. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  54. package/packages/dd-trace/src/datastreams/processor.js +14 -1
  55. package/packages/dd-trace/src/encode/agentless-json.js +141 -0
  56. package/packages/dd-trace/src/exporter.js +2 -0
  57. package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
  58. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  59. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  60. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  61. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  62. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  63. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  64. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  65. package/packages/dd-trace/src/opentracing/span.js +6 -4
  66. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  67. package/packages/dd-trace/src/plugins/database.js +15 -2
  68. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  69. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  70. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  71. package/packages/dd-trace/src/proxy.js +4 -0
  72. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  73. package/packages/dd-trace/src/startup-log.js +1 -1
  74. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  75. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  76. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  77. package/packages/dd-trace/src/scope/noop/scope.js +0 -21
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { trace, ROOT_CONTEXT, propagation } = require('@opentelemetry/api')
4
4
  const { storage } = require('../../../datadog-core')
5
+ const { getAllBaggageItems, setBaggageItem, removeAllBaggageItems } = require('../baggage')
5
6
 
6
7
  const tracer = require('../../')
7
8
  const SpanContext = require('./span_context')
@@ -19,31 +20,26 @@ class ContextManager {
19
20
 
20
21
  const storedSpan = store ? trace.getSpan(store) : null
21
22
 
23
+ // Convert DD baggage to OTel format
24
+ const baggages = getAllBaggageItems()
25
+ const hasBaggage = Object.keys(baggages).length > 0
26
+ let otelBaggages
27
+ if (hasBaggage) {
28
+ const entries = {}
29
+ for (const [key, value] of Object.entries(baggages)) {
30
+ entries[key] = { value }
31
+ }
32
+ otelBaggages = propagation.createBaggage(entries)
33
+ }
34
+
22
35
  // If stored span wraps the active DD span, prefer the stored context
23
36
  if (storedSpan && storedSpan._ddSpan === activeSpan) {
24
- const baggages = JSON.parse(activeSpan.getAllBaggageItems())
25
- if (Object.keys(baggages).length > 0) {
26
- const entries = {}
27
- for (const [key, value] of Object.entries(baggages)) {
28
- entries[key] = { value }
29
- }
30
- const otelBaggages = propagation.createBaggage(entries)
31
- return propagation.setBaggage(store, otelBaggages)
32
- }
37
+ if (otelBaggages) return propagation.setBaggage(store, otelBaggages)
33
38
  return store
34
39
  }
35
40
 
36
41
  if (!activeSpan) {
37
- const storedBaggageItems = storedSpan?._spanContext?._ddContext?._baggageItems
38
- if (storedBaggageItems) {
39
- const baggages = storedBaggageItems
40
- const entries = {}
41
- for (const [key, value] of Object.entries(baggages)) {
42
- entries[key] = { value }
43
- }
44
- const otelBaggages = propagation.createBaggage(entries)
45
- return propagation.setBaggage(baseContext, otelBaggages)
46
- }
42
+ if (otelBaggages) return propagation.setBaggage(baseContext, otelBaggages)
47
43
  return baseContext
48
44
  }
49
45
 
@@ -53,18 +49,6 @@ class ContextManager {
53
49
  ddContext._otelSpanContext = new SpanContext(ddContext)
54
50
  }
55
51
 
56
- // Convert DD baggage to OTel format
57
- const baggages = JSON.parse(activeSpan.getAllBaggageItems())
58
- const hasBaggage = Object.keys(baggages).length > 0
59
- let otelBaggages
60
- if (hasBaggage) {
61
- const entries = {}
62
- for (const [key, value] of Object.entries(baggages)) {
63
- entries[key] = { value }
64
- }
65
- otelBaggages = propagation.createBaggage(entries)
66
- }
67
-
68
52
  if (store && trace.getSpanContext(store) === ddContext._otelSpanContext) {
69
53
  return otelBaggages ? propagation.setBaggage(store, otelBaggages) : store
70
54
  }
@@ -86,22 +70,11 @@ class ContextManager {
86
70
  if (baggages) {
87
71
  baggageItems = baggages.getAllEntries()
88
72
  }
89
- if (span && span._ddSpan) {
90
- // does otel always override datadog?
91
- span._ddSpan.removeAllBaggageItems()
92
- for (const baggage of baggageItems) {
93
- span._ddSpan.setBaggageItem(baggage[0], baggage[1].value)
94
- }
95
- return ddScope.activate(span._ddSpan, run)
96
- }
97
- // span instanceof NonRecordingSpan
98
- const ddContext = span?._spanContext?._ddContext
99
- if (ddContext && ddContext._baggageItems) {
100
- ddContext._baggageItems = {}
101
- for (const baggage of baggageItems) {
102
- ddContext._baggageItems[baggage[0]] = baggage[1].value
103
- }
73
+ removeAllBaggageItems()
74
+ for (const baggage of baggageItems) {
75
+ setBaggageItem(baggage[0], baggage[1].value)
104
76
  }
77
+ if (span && span._ddSpan) return ddScope.activate(span._ddSpan, run)
105
78
  return run()
106
79
  }
107
80
 
@@ -20,7 +20,7 @@ class OtlpHttpExporterBase {
20
20
  * Creates a new OtlpHttpExporterBase instance.
21
21
  *
22
22
  * @param {string} url - OTLP endpoint URL
23
- * @param {string} headers - Additional HTTP headers as comma-separated key=value string
23
+ * @param {string|undefined} headers - Additional HTTP headers as comma-separated key=value string
24
24
  * @param {number} timeout - Request timeout in milliseconds
25
25
  * @param {string} protocol - OTLP protocol (http/protobuf or http/json)
26
26
  * @param {string} defaultPath - Default path to use if URL has no path
@@ -117,11 +117,10 @@ class OtlpHttpExporterBase {
117
117
 
118
118
  /**
119
119
  * Parses additional HTTP headers from a comma-separated string.
120
- * @param {string} headersString - Comma-separated key=value pairs
120
+ * @param {string} [headersString=''] - Comma-separated key=value pairs
121
121
  * @returns {Record<string, string>} Parsed headers object
122
- * @private
123
122
  */
124
- #parseAdditionalHeaders (headersString) {
123
+ #parseAdditionalHeaders (headersString = '') {
125
124
  const headers = {}
126
125
  let key = ''
127
126
  let value = ''
@@ -670,10 +670,8 @@ class TextMapPropagator {
670
670
  if (!this._hasPropagationStyle('extract', 'baggage')) return
671
671
  if (!carrier?.baggage) return
672
672
  const baggages = carrier.baggage.split(',')
673
- const tagAllKeys = this._config.baggageTagKeys === '*'
674
- const keysToSpanTag = tagAllKeys
675
- ? undefined
676
- : new Set(this._config.baggageTagKeys.split(','))
673
+ const baggageTagKeys = new Set(this._config.baggageTagKeys)
674
+ const tagAllKeys = baggageTagKeys.has('*')
677
675
  for (const keyValue of baggages) {
678
676
  if (!keyValue) continue
679
677
 
@@ -707,7 +705,7 @@ class TextMapPropagator {
707
705
  return
708
706
  }
709
707
 
710
- if (spanContext && (tagAllKeys || keysToSpanTag?.has(key))) {
708
+ if (spanContext && (tagAllKeys || baggageTagKeys.has(key))) {
711
709
  spanContext._trace.tags['baggage.' + key] = value
712
710
  }
713
711
  setBaggageItem(key, value)
@@ -3,7 +3,6 @@
3
3
  // TODO (new internal tracer): use DC events for lifecycle metrics and test them
4
4
  const { performance } = require('perf_hooks')
5
5
  const now = performance.now.bind(performance)
6
- const dateNow = Date.now
7
6
  const util = require('util')
8
7
  const { channel } = require('dc-polyfill')
9
8
  const id = require('../id')
@@ -13,12 +12,15 @@ const log = require('../log')
13
12
  const { storage } = require('../../../datadog-core')
14
13
  const telemetryMetrics = require('../telemetry/metrics')
15
14
  const { getValueFromEnvSources } = require('../config/helper')
15
+ const { isTrue } = require('../util')
16
16
  const SpanContext = require('./span_context')
17
17
 
18
+ const dateNow = Date.now
19
+
18
20
  const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
19
21
 
20
- const DD_TRACE_EXPERIMENTAL_STATE_TRACKING = getValueFromEnvSources('DD_TRACE_EXPERIMENTAL_STATE_TRACKING')
21
- const DD_TRACE_EXPERIMENTAL_SPAN_COUNTS = getValueFromEnvSources('DD_TRACE_EXPERIMENTAL_SPAN_COUNTS')
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'))
22
24
 
23
25
  const unfinishedRegistry = createRegistry('unfinished')
24
26
  const finishedRegistry = createRegistry('finished')
@@ -251,7 +253,7 @@ class DatadogSpan {
251
253
  return
252
254
  }
253
255
 
254
- if (DD_TRACE_EXPERIMENTAL_STATE_TRACKING === 'true' && !this._spanContext._tags['service.name']) {
256
+ if (DD_TRACE_EXPERIMENTAL_STATE_TRACKING && !this._spanContext._tags['service.name']) {
255
257
  log.error('Finishing invalid span: %s', this)
256
258
  }
257
259
 
@@ -63,6 +63,8 @@ const {
63
63
  getPullRequestDiff,
64
64
  getModifiedFilesFromDiff,
65
65
  getPullRequestBaseBranch,
66
+ getSessionRequestErrorTags,
67
+ DD_CI_LIBRARY_CONFIGURATION_ERROR,
66
68
  TEST_IS_TEST_FRAMEWORK_WORKER,
67
69
  TEST_IS_NEW,
68
70
  TEST_IS_RUM_ACTIVE,
@@ -120,6 +122,7 @@ module.exports = class CiPlugin extends Plugin {
120
122
  this.fileLineToProbeId = new Map()
121
123
  this.rootDir = process.cwd() // fallback in case :session:start events are not emitted
122
124
  this._testSuiteSpansByTestSuite = new Map()
125
+ this._pendingRequestErrorTags = []
123
126
 
124
127
  this.addSub(`ci:${this.constructor.id}:library-configuration`, (ctx) => {
125
128
  const { onDone, isParallel, frameworkVersion } = ctx
@@ -131,10 +134,15 @@ module.exports = class CiPlugin extends Plugin {
131
134
  this.tracer._exporter.getLibraryConfiguration(this.testConfiguration, (err, libraryConfig) => {
132
135
  if (err) {
133
136
  log.error('Library configuration could not be fetched. %s', err.message)
137
+ this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR, err)
134
138
  } else {
135
139
  this.libraryConfig = libraryConfig
136
140
  }
137
141
 
142
+ const requestErrorTags = this.testSessionSpan
143
+ ? getSessionRequestErrorTags(this.testSessionSpan)
144
+ : Object.fromEntries(this._pendingRequestErrorTags.map(({ tag, value }) => [tag, value]))
145
+
138
146
  const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, isParallel, frameworkVersion)
139
147
  const metadataTags = {
140
148
  test: {
@@ -142,7 +150,7 @@ module.exports = class CiPlugin extends Plugin {
142
150
  },
143
151
  }
144
152
  this.tracer._exporter.addMetadataTags(metadataTags)
145
- onDone({ err, libraryConfig })
153
+ onDone({ err, libraryConfig, requestErrorTags })
146
154
  })
147
155
  })
148
156
 
@@ -200,6 +208,10 @@ module.exports = class CiPlugin extends Plugin {
200
208
  },
201
209
  integrationName: this.constructor.id,
202
210
  })
211
+ for (const { tag, value } of this._pendingRequestErrorTags) {
212
+ this.testSessionSpan.setTag(tag, value)
213
+ }
214
+ this._pendingRequestErrorTags = []
203
215
  // TODO: add telemetry tag when we can add `is_agentless_log_submission_enabled` for agentless log submission
204
216
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'session')
205
217
 
@@ -209,6 +221,7 @@ module.exports = class CiPlugin extends Plugin {
209
221
  [COMPONENT]: this.constructor.id,
210
222
  ...this.testEnvironmentMetadata,
211
223
  ...testModuleSpanMetadata,
224
+ ...getSessionRequestErrorTags(this.testSessionSpan),
212
225
  },
213
226
  integrationName: this.constructor.id,
214
227
  })
@@ -230,7 +243,10 @@ module.exports = class CiPlugin extends Plugin {
230
243
  this.addSub(`ci:${this.constructor.id}:itr:skipped-suites`, ({ skippedSuites, frameworkVersion }) => {
231
244
  const testCommand = this.testSessionSpan.context()._tags[TEST_COMMAND]
232
245
  for (const testSuite of skippedSuites) {
233
- const testSuiteMetadata = getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, this.constructor.id)
246
+ const testSuiteMetadata = {
247
+ ...getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, this.constructor.id),
248
+ ...getSessionRequestErrorTags(this.testSessionSpan),
249
+ }
234
250
  if (this.itrCorrelationId) {
235
251
  testSuiteMetadata[ITR_CORRELATION_ID] = this.itrCorrelationId
236
252
  }
@@ -261,8 +277,10 @@ module.exports = class CiPlugin extends Plugin {
261
277
  this.tracer._exporter.getKnownTests(this.testConfiguration, (err, knownTests) => {
262
278
  if (err) {
263
279
  log.error('Known tests could not be fetched. %s', err.message)
264
- this.libraryConfig.isEarlyFlakeDetectionEnabled = false
265
- this.libraryConfig.isKnownTestsEnabled = false
280
+ if (this.libraryConfig) {
281
+ this.libraryConfig.isEarlyFlakeDetectionEnabled = false
282
+ this.libraryConfig.isKnownTestsEnabled = false
283
+ }
266
284
  }
267
285
  onDone({ err, knownTests })
268
286
  })
@@ -279,7 +297,9 @@ module.exports = class CiPlugin extends Plugin {
279
297
  this.tracer._exporter.getTestManagementTests(this.testConfiguration, (err, testManagementTests) => {
280
298
  if (err) {
281
299
  log.error('Test management tests could not be fetched. %s', err.message)
282
- this.libraryConfig.isTestManagementEnabled = false
300
+ if (this.libraryConfig) {
301
+ this.libraryConfig.isTestManagementEnabled = false
302
+ }
283
303
  }
284
304
  onDone({ err, testManagementTests })
285
305
  })
@@ -346,8 +366,15 @@ module.exports = class CiPlugin extends Plugin {
346
366
  span.meta = {
347
367
  ...span.meta,
348
368
  ...testSuiteTags,
369
+ ...getSessionRequestErrorTags(this.testSessionSpan),
349
370
  }
350
371
  }
372
+
373
+ // Jest and Vitest worker test spans are serialized in the worker and may not include
374
+ // request error tags; add them from the session span in the main process.
375
+ if ((span.name === 'jest.test' || span.name === 'vitest.test') && this.testSessionSpan) {
376
+ Object.assign(span.meta, getSessionRequestErrorTags(this.testSessionSpan))
377
+ }
351
378
  }
352
379
  this.tracer._exporter.export(trace)
353
380
  }
@@ -418,6 +445,30 @@ module.exports = class CiPlugin extends Plugin {
418
445
  }
419
446
  }
420
447
 
448
+ /**
449
+ * Adds a hidden _dd tag to the test session span when a test-optimization request fails.
450
+ * If the session span does not exist yet (e.g. library-configuration failed before session:start),
451
+ * the tag is queued and applied when the span is created.
452
+ * @param {string} tag - Tag name (e.g. DD_CI_LIBRARY_CONFIGURATION_ERROR)
453
+ * @param {Error} err - Request error
454
+ */
455
+ _addRequestErrorTag (tag, err) {
456
+ const value = 'true'
457
+ if (this.testSessionSpan) {
458
+ this.testSessionSpan.setTag(tag, value)
459
+ } else {
460
+ this._pendingRequestErrorTags.push({ tag, value })
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Returns request error tags from the test session span for propagation to module, suite and test spans.
466
+ * @returns {Record<string, string>}
467
+ */
468
+ getSessionRequestErrorTags () {
469
+ return getSessionRequestErrorTags(this.testSessionSpan)
470
+ }
471
+
421
472
  configure (config, shouldGetEnvironmentData = true) {
422
473
  super.configure(config)
423
474
 
@@ -529,6 +580,7 @@ module.exports = class CiPlugin extends Plugin {
529
580
  [TEST_SESSION_ID]: testSuiteSpan.context().toTraceId(),
530
581
  [TEST_COMMAND]: testSuiteSpan.context()._tags[TEST_COMMAND],
531
582
  [TEST_MODULE]: this.constructor.id,
583
+ ...getSessionRequestErrorTags(this.testSessionSpan),
532
584
  }
533
585
  if (testSuiteSpan.context()._parentId) {
534
586
  suiteTags[TEST_MODULE_ID] = testSuiteSpan.context()._parentId.toString(10)
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { PEER_SERVICE_KEY, PEER_SERVICE_SOURCE_KEY } = require('../constants')
4
+ const propagationHash = require('../propagation-hash')
4
5
  const StoragePlugin = require('./storage')
5
6
 
6
7
  class DatabasePlugin extends StoragePlugin {
@@ -59,13 +60,25 @@ class DatabasePlugin extends StoragePlugin {
59
60
  const dbmService = this.#getDbmServiceName(serviceName, peerData)
60
61
  const servicePropagation = this.#createDBMPropagationCommentService(dbmService, span, peerData)
61
62
 
63
+ let dbmComment = servicePropagation
64
+
65
+ // Add propagation hash if both process tags and SQL base hash injection are enabled
66
+ if (propagationHash.isEnabled() && this.config['dbm.injectSqlBaseHash']) {
67
+ const hashBase64 = propagationHash.getHashBase64()
68
+ if (hashBase64) {
69
+ dbmComment += `,ddsh='${hashBase64}'`
70
+ // Add hash to span meta as a tag
71
+ span.setTag('_dd.dbm.propagation_hash', hashBase64)
72
+ }
73
+ }
74
+
62
75
  if (disableFullMode || mode === 'service') {
63
- return servicePropagation
76
+ return dbmComment
64
77
  } else if (mode === 'full') {
65
78
  span.setTag('_dd.dbm_trace_injected', 'true')
66
79
  span._processor.sample(span)
67
80
  const traceparent = span._spanContext.toTraceparent()
68
- return `${servicePropagation},traceparent='${traceparent}'`
81
+ return `${dbmComment},traceparent='${traceparent}'`
69
82
  }
70
83
  }
71
84
 
@@ -145,6 +145,10 @@ const DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE = '_dd.library_capabilities.tes
145
145
  const DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE = '_dd.library_capabilities.test_management.disable'
146
146
  const DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX = '_dd.library_capabilities.test_management.attempt_to_fix'
147
147
  const DD_CAPABILITIES_FAILED_TEST_REPLAY = '_dd.library_capabilities.failed_test_replay'
148
+
149
+ // Library configuration request error tag
150
+ const DD_CI_LIBRARY_CONFIGURATION_ERROR = '_dd.ci.library_configuration_error'
151
+
148
152
  const UNSUPPORTED_TIA_FRAMEWORKS = new Set(['playwright', 'vitest'])
149
153
  const UNSUPPORTED_TIA_FRAMEWORKS_PARALLEL_MODE = new Set(['cucumber', 'mocha'])
150
154
  const MINIMUM_FRAMEWORK_VERSION_FOR_EFD = {
@@ -202,6 +206,22 @@ const TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED = 'test.test_management.attempt_to_f
202
206
  const POSSIBLE_BASE_BRANCHES = ['main', 'master', 'preprod', 'prod', 'dev', 'development', 'trunk']
203
207
  const BASE_LIKE_BRANCH_FILTER = /^(main|master|preprod|prod|dev|development|trunk|release\/.*|hotfix\/.*)$/
204
208
 
209
+ /**
210
+ * Returns request error tags from a test session span for propagation to child events.
211
+ * @param {{ context: () => { _tags?: Record<string, string> } } | undefined} sessionSpan
212
+ * @returns {Record<string, string>}
213
+ */
214
+ function getSessionRequestErrorTags (sessionSpan) {
215
+ const tags = sessionSpan?.context()._tags
216
+ if (!tags || typeof tags !== 'object') return {}
217
+ if (tags[DD_CI_LIBRARY_CONFIGURATION_ERROR] === 'true') {
218
+ return {
219
+ [DD_CI_LIBRARY_CONFIGURATION_ERROR]: 'true',
220
+ }
221
+ }
222
+ return {}
223
+ }
224
+
205
225
  module.exports = {
206
226
  TEST_CODE_OWNERS,
207
227
  TEST_SESSION_NAME,
@@ -278,6 +298,7 @@ module.exports = {
278
298
  removeInvalidMetadata,
279
299
  parseAnnotations,
280
300
  getIsFaultyEarlyFlakeDetection,
301
+ getEfdRetryCount,
281
302
  TEST_BROWSER_DRIVER,
282
303
  TEST_BROWSER_DRIVER_VERSION,
283
304
  TEST_BROWSER_NAME,
@@ -308,6 +329,8 @@ module.exports = {
308
329
  TEST_MANAGEMENT_ENABLED,
309
330
  TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED,
310
331
  getLibraryCapabilitiesTags,
332
+ getSessionRequestErrorTags,
333
+ DD_CI_LIBRARY_CONFIGURATION_ERROR,
311
334
  checkShaDiscrepancies,
312
335
  getPullRequestDiff,
313
336
  getPullRequestBaseBranch,
@@ -846,6 +869,31 @@ function parseAnnotations (annotations) {
846
869
  }, {})
847
870
  }
848
871
 
872
+ /**
873
+ * Given a test's first-execution duration (ms) and the slow_test_retries map
874
+ * from the backend, return how many EFD retries to run.
875
+ *
876
+ * Returns 0 when the test is too slow to retry (≥ 5 min).
877
+ *
878
+ * @param {number} durationMs
879
+ * @param {Record<string, number>} slowTestRetries e.g. { '5s': 10, '10s': 5, '30s': 3, '5m': 2 }
880
+ * @returns {number}
881
+ */
882
+ function getEfdRetryCount (durationMs, slowTestRetries) {
883
+ const thresholds = [
884
+ { limitMs: 5 * 1000, key: '5s' },
885
+ { limitMs: 10 * 1000, key: '10s' },
886
+ { limitMs: 30 * 1000, key: '30s' },
887
+ { limitMs: 5 * 60 * 1000, key: '5m' },
888
+ ]
889
+ for (const { limitMs, key } of thresholds) {
890
+ if (durationMs < limitMs) {
891
+ return slowTestRetries[key] ?? 0
892
+ }
893
+ }
894
+ return 0 // ≥ 5 min — abort
895
+ }
896
+
849
897
  function getIsFaultyEarlyFlakeDetection (projectSuites, testsBySuiteName, faultyThresholdPercentage) {
850
898
  let newSuites = 0
851
899
  for (const suite of projectSuites) {
@@ -17,6 +17,7 @@ function exporterFromURL (url) {
17
17
  if (url.protocol === 'file:') {
18
18
  return new FileExporter({ pprofPrefix: fileURLToPath(url) })
19
19
  }
20
+ // TODO: Why is DD_INJECTION_ENABLED a comma separated list?
20
21
  const injectionEnabled = (getValueFromEnvSources('DD_INJECTION_ENABLED') ?? '').split(',')
21
22
  const libraryInjected = injectionEnabled.length > 0
22
23
  const profilingEnabled = (getValueFromEnvSources('DD_PROFILING_ENABLED') ?? '').toLowerCase()
@@ -0,0 +1,145 @@
1
+ 'use strict'
2
+
3
+ const { fnv64 } = require('../datastreams/fnv')
4
+ const log = require('../log')
5
+
6
+ /**
7
+ * PropagationHashManager is a singleton that manages the propagation hash computation.
8
+ * The propagation hash is an FNV-1a 64-bit hash combining:
9
+ * - Process tags (entrypoint info, package.json name, etc.)
10
+ * - Container tags hash (received from the Datadog agent)
11
+ *
12
+ * This hash is used to correlate traces with database operations (DBM) and
13
+ * data stream pathways (DSM) for enhanced observability.
14
+ */
15
+ class PropagationHashManager {
16
+ _containerTagsHash = null
17
+ _cachedHash = null
18
+ _cachedHashString = null
19
+ _cachedHashBase64 = null
20
+ _config = null
21
+
22
+ /**
23
+ * Configure the propagation hash manager with tracer config
24
+ * @param {object} config - Tracer configuration
25
+ */
26
+ configure (config) {
27
+ this._config = config
28
+ }
29
+
30
+ /**
31
+ * Check if process tags propagation is enabled
32
+ * @returns {boolean}
33
+ */
34
+ isEnabled () {
35
+ return this._config?.propagateProcessTags?.enabled === true
36
+ }
37
+
38
+ /**
39
+ * Update the container tags hash received from the agent
40
+ * @param {string} hash - Container tags hash from agent response
41
+ */
42
+ updateContainerTagsHash (hash) {
43
+ if (hash !== this._containerTagsHash) {
44
+ log.debug('Updating container tags hash: %s', hash)
45
+ this._containerTagsHash = hash
46
+ this._invalidateCache()
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get the propagation hash as a BigInt
52
+ * @returns {bigint | null} The propagation hash or null if disabled/unavailable
53
+ */
54
+ getHash () {
55
+ if (!this.isEnabled()) {
56
+ return null
57
+ }
58
+ if (this._cachedHash) {
59
+ return this._cachedHash
60
+ }
61
+ this._computeHash()
62
+ return this._cachedHash
63
+ }
64
+
65
+ /**
66
+ * Get the propagation hash as a hexadecimal string
67
+ * @returns {string|null} The propagation hash in hex format or null if disabled/unavailable
68
+ */
69
+ getHashString () {
70
+ const hash = this.getHash()
71
+ if (!hash) {
72
+ return null
73
+ }
74
+ if (!this._cachedHashString) {
75
+ this._cachedHashString = hash.toString(16)
76
+ }
77
+ return this._cachedHashString
78
+ }
79
+
80
+ /**
81
+ * Get the propagation hash as a base64 string
82
+ * @returns {string|null} The propagation hash in base64 format or null if disabled/unavailable
83
+ */
84
+ getHashBase64 () {
85
+ const hash = this.getHash()
86
+ if (!hash) {
87
+ return null
88
+ }
89
+ if (!this._cachedHashBase64) {
90
+ // Convert BigInt to 8-byte buffer (64-bit hash)
91
+ const buffer = Buffer.allocUnsafe(8)
92
+ // Write as big-endian 64-bit unsigned integer
93
+ buffer.writeBigUInt64BE(hash, 0)
94
+ this._cachedHashBase64 = buffer.toString('base64')
95
+ }
96
+ return this._cachedHashBase64
97
+ }
98
+
99
+ /**
100
+ * Compute the propagation hash using FNV-1a algorithm
101
+ * @private
102
+ */
103
+ _computeHash () {
104
+ try {
105
+ const processTags = require('../process-tags')
106
+
107
+ // Combine process tags and container tags hash
108
+ // Process tags are already serialized as a comma-separated string
109
+ const input = processTags.serialized + (this._containerTagsHash || '')
110
+
111
+ if (!input) {
112
+ // If both are empty, don't compute a hash
113
+ this._cachedHash = null
114
+ this._cachedHashString = null
115
+ this._cachedHashBase64 = null
116
+ return
117
+ }
118
+
119
+ // Compute FNV-1a 64-bit hash
120
+ this._cachedHash = fnv64(input)
121
+ this._cachedHashString = null // Will be computed on demand
122
+ this._cachedHashBase64 = null // Will be computed on demand
123
+
124
+ log.debug('Computed propagation hash from input (length=%s): "%s"', input.length, this._cachedHash.toString(16))
125
+ } catch (e) {
126
+ log.error('Error computing propagation hash', e)
127
+ this._cachedHash = null
128
+ this._cachedHashString = null
129
+ this._cachedHashBase64 = null
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Invalidate the cached hash
135
+ * @private
136
+ */
137
+ _invalidateCache () {
138
+ this._cachedHash = null
139
+ this._cachedHashString = null
140
+ this._cachedHashBase64 = null
141
+ }
142
+ }
143
+
144
+ // Export singleton instance
145
+ module.exports = new PropagationHashManager()
@@ -102,6 +102,10 @@ class Tracer extends NoopProxy {
102
102
  try {
103
103
  const config = getConfig(options) // TODO: support dynamic code config
104
104
 
105
+ // Configure propagation hash manager for process tags + container tags
106
+ const propagationHash = require('./propagation-hash')
107
+ propagationHash.configure(config)
108
+
105
109
  if (config.crashtracking.enabled) {
106
110
  require('./crashtracking').start(config)
107
111
  }
@@ -238,7 +238,7 @@ function captureHeapSpace () {
238
238
  const stats = v8.getHeapSpaceStatistics()
239
239
 
240
240
  for (let i = 0, l = stats.length; i < l; i++) {
241
- const tags = [`space:${stats[i].space_name}`]
241
+ const tags = [`heap_space:${stats[i].space_name}`]
242
242
 
243
243
  client.gauge('runtime.node.heap.size.by.space', stats[i].space_size, tags)
244
244
  client.gauge('runtime.node.heap.used_size.by.space', stats[i].space_used_size, tags)
@@ -74,7 +74,7 @@ function tracerInfo () {
74
74
  runtime_metrics_enabled: !!config.runtimeMetrics,
75
75
  profiling_enabled: config.profiling?.enabled === 'true' || config.profiling?.enabled === 'auto',
76
76
  integrations_loaded: Object.keys(pluginManager._pluginsByName),
77
- appsec_enabled: !!config.appsec.enabled,
77
+ appsec_enabled: config.appsec.enabled,
78
78
  data_streams_enabled: !!config.dsmEnabled,
79
79
  }
80
80