dd-trace 6.2.0 → 6.4.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 (76) hide show
  1. package/README.md +1 -1
  2. package/index.electron.js +3 -0
  3. package/package.json +8 -5
  4. package/packages/datadog-instrumentations/src/ai.js +29 -24
  5. package/packages/datadog-instrumentations/src/cookie.js +7 -1
  6. package/packages/datadog-instrumentations/src/cucumber.js +4 -3
  7. package/packages/datadog-instrumentations/src/express.js +20 -2
  8. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -1
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/modelcontextprotocol-sdk.js +30 -54
  11. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  12. package/packages/datadog-instrumentations/src/http/server.js +27 -0
  13. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  14. package/packages/datadog-instrumentations/src/jest.js +10 -4
  15. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +248 -1
  16. package/packages/datadog-instrumentations/src/playwright.js +40 -3
  17. package/packages/datadog-instrumentations/src/router.js +180 -105
  18. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  19. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  20. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  21. package/packages/datadog-plugin-aws-sdk/src/util.js +0 -22
  22. package/packages/datadog-plugin-azure-functions/src/index.js +2 -1
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +5 -5
  24. package/packages/datadog-plugin-graphql/src/execute.js +90 -14
  25. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  26. package/packages/datadog-plugin-graphql/src/request.js +33 -2
  27. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  28. package/packages/datadog-plugin-graphql/src/validate.js +13 -1
  29. package/packages/datadog-plugin-http2/src/server.js +38 -6
  30. package/packages/datadog-plugin-jest/src/util.js +21 -6
  31. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/tracing.js +144 -26
  32. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/utils.js +30 -0
  33. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  34. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  35. package/packages/datadog-shimmer/src/shimmer.js +9 -2
  36. package/packages/dd-trace/index.electron.js +3 -0
  37. package/packages/dd-trace/index.js +2 -37
  38. package/packages/dd-trace/src/appsec/channels.js +1 -0
  39. package/packages/dd-trace/src/appsec/index.js +6 -3
  40. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  41. package/packages/dd-trace/src/bootstrap.js +39 -0
  42. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +23 -16
  43. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +14 -2
  44. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  45. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  46. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +55 -5
  47. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +8 -1
  48. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  49. package/packages/dd-trace/src/config/config-types.d.ts +8 -0
  50. package/packages/dd-trace/src/config/defaults.js +20 -15
  51. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  52. package/packages/dd-trace/src/config/index.js +13 -3
  53. package/packages/dd-trace/src/config/parsers.js +79 -0
  54. package/packages/dd-trace/src/config/supported-configurations.json +17 -0
  55. package/packages/dd-trace/src/constants.js +7 -0
  56. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  57. package/packages/dd-trace/src/feature-registry.js +22 -0
  58. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  59. package/packages/dd-trace/src/llmobs/index.js +10 -2
  60. package/packages/dd-trace/src/llmobs/tagger.js +19 -1
  61. package/packages/dd-trace/src/noop/proxy.js +5 -4
  62. package/packages/dd-trace/src/openfeature/register.js +46 -0
  63. package/packages/dd-trace/src/opentelemetry/span_processor.js +7 -5
  64. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +32 -16
  65. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  66. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  67. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  68. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  69. package/packages/dd-trace/src/plugins/util/test.js +9 -4
  70. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  71. package/packages/dd-trace/src/proxy.js +21 -9
  72. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +20 -11
  73. package/packages/dd-trace/src/standalone/index.js +0 -22
  74. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  75. package/vendor/dist/protobufjs/index.js +1 -1
  76. package/vendor/dist/protobufjs/minimal/index.js +1 -1
@@ -28,12 +28,16 @@ function parseSkippableSuitesResponse (
28
28
  if (validateRequiredFields) {
29
29
  validateSkippableTestsResponse(parsedResponse)
30
30
  }
31
- const coverage = parsedResponse.meta?.coverage || {}
31
+ const coverage = {}
32
+ for (const [filename, bitmap] of Object.entries(parsedResponse.meta?.coverage || {})) {
33
+ coverage[filename.replaceAll('\\', '/')] = bitmap
34
+ }
32
35
 
33
36
  const skippableItems = parsedResponse
34
37
  .data
35
38
  .filter(({ type }) => type === testLevel)
36
39
  const skippableSuites = []
40
+ let numExcludedByMissingLineCoverage = 0
37
41
  for (const {
38
42
  attributes: {
39
43
  suite,
@@ -42,13 +46,58 @@ function parseSkippableSuitesResponse (
42
46
  },
43
47
  } of skippableItems) {
44
48
  // Only reject candidates without backend line coverage when we need that coverage to backfill reports.
45
- if (isCoverageReportUploadEnabled && isMissingLineCodeCoverage) continue
49
+ if (isCoverageReportUploadEnabled && isMissingLineCodeCoverage) {
50
+ numExcludedByMissingLineCoverage++
51
+ continue
52
+ }
46
53
 
47
54
  skippableSuites.push(testLevel === 'suite' ? suite : { suite, name })
48
55
  }
49
56
  const correlationId = parsedResponse.meta?.correlation_id
50
57
 
51
- return { skippableSuites, correlationId, coverage }
58
+ return {
59
+ skippableSuites,
60
+ correlationId,
61
+ coverage,
62
+ numReceivedSkippableItems: skippableItems.length,
63
+ numExcludedByMissingLineCoverage,
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Logs how missing line coverage affected the skippable candidates without logging their contents.
69
+ *
70
+ * @param {{
71
+ * skippableSuites: Array<string|{suite: string, name: string}>,
72
+ * numReceivedSkippableItems?: number,
73
+ * numExcludedByMissingLineCoverage?: number
74
+ * }} result - Parsed skippable response.
75
+ * @param {'suite'|'test'} testLevel - Test optimization skipping level.
76
+ * @returns {void}
77
+ */
78
+ function logSkippableSuitesResponse (result, testLevel) {
79
+ if (result.numReceivedSkippableItems === undefined) {
80
+ log.debug('Number of received skippable %ss: %d', testLevel, result.skippableSuites.length)
81
+ return
82
+ }
83
+
84
+ log.debug(
85
+ 'Received %d skippable %s candidates; excluded %d because line coverage is missing; %d remain.',
86
+ result.numReceivedSkippableItems,
87
+ testLevel,
88
+ result.numExcludedByMissingLineCoverage,
89
+ result.skippableSuites.length
90
+ )
91
+ if (
92
+ result.numReceivedSkippableItems > 0 &&
93
+ result.numExcludedByMissingLineCoverage === result.numReceivedSkippableItems
94
+ ) {
95
+ log.warn(
96
+ 'All %d skippable %s candidates were excluded: coverage upload is enabled but line coverage is missing.',
97
+ result.numReceivedSkippableItems,
98
+ testLevel
99
+ )
100
+ }
52
101
  }
53
102
 
54
103
  function getSkippableSuites ({
@@ -96,6 +145,7 @@ function getSkippableSuites ({
96
145
  }, cb)
97
146
  }, (err, data) => {
98
147
  if (err) return done(err)
148
+ logSkippableSuitesResponse(data, testLevel)
99
149
  done(null, data.skippableSuites, data.correlationId, data.coverage)
100
150
  })
101
151
  }
@@ -203,6 +253,7 @@ function fetchFromApi ({
203
253
  const result = parseSkippableSuitesResponse(parsedResponse, {
204
254
  testLevel,
205
255
  isCoverageReportUploadEnabled,
256
+ validateRequiredFields: true,
206
257
  })
207
258
  const skippableItems = parsedResponse.data.filter(({ type }) => type === testLevel)
208
259
  incrementCountMetric(
@@ -213,7 +264,6 @@ function fetchFromApi ({
213
264
  skippableItems.length
214
265
  )
215
266
  distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, {}, res.length)
216
- log.debug('Number of received skippable %ss:', testLevel, result.skippableSuites.length)
217
267
 
218
268
  writeToCache(cacheKey, result)
219
269
 
@@ -225,4 +275,4 @@ function fetchFromApi ({
225
275
  })
226
276
  }
227
277
 
228
- module.exports = { getSkippableSuites, parseSkippableSuitesResponse }
278
+ module.exports = { getSkippableSuites, logSkippableSuitesResponse, parseSkippableSuitesResponse }
@@ -49,7 +49,7 @@ function parseLibraryConfigurationResponse (rawJson, config = getConfig(), optio
49
49
  requireGit,
50
50
  isEarlyFlakeDetectionEnabled: isKnownTestsEnabled && (earlyFlakeDetectionConfig?.enabled ?? false),
51
51
  earlyFlakeDetectionNumRetries:
52
- earlyFlakeDetectionConfig?.slow_test_retries?.['5s'] || DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES,
52
+ earlyFlakeDetectionConfig?.slow_test_retries?.['5s'] ?? DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES,
53
53
  earlyFlakeDetectionSlowTestRetries:
54
54
  earlyFlakeDetectionConfig?.slow_test_retries ?? DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES,
55
55
  earlyFlakeDetectionFaultyThreshold:
@@ -74,6 +74,13 @@ function parseLibraryConfigurationResponse (rawJson, config = getConfig(), optio
74
74
  settings.isSuitesSkippingEnabled = true
75
75
  log.debug('Dangerously set test skipping to true')
76
76
  }
77
+ if (
78
+ settings.isCoverageReportUploadEnabled &&
79
+ !config.testOptimization.DD_CIVISIBILITY_CODE_COVERAGE_REPORT_UPLOAD_ENABLED
80
+ ) {
81
+ settings.isCoverageReportUploadEnabled = false
82
+ log.debug('Code coverage report upload was disabled by the environment variable')
83
+ }
77
84
 
78
85
  return settings
79
86
  }
@@ -6,7 +6,10 @@ const path = require('node:path')
6
6
  const log = require('../log')
7
7
  const { getNumFromKnownTests } = require('../plugins/util/test')
8
8
  const { parseKnownTestsResponse } = require('./early-flake-detection/get-known-tests')
9
- const { parseSkippableSuitesResponse } = require('./intelligent-test-runner/get-skippable-suites')
9
+ const {
10
+ logSkippableSuitesResponse,
11
+ parseSkippableSuitesResponse,
12
+ } = require('./intelligent-test-runner/get-skippable-suites')
10
13
  const { parseLibraryConfigurationResponse } = require('./requests/get-library-configuration')
11
14
  const {
12
15
  getNumFromTestManagementTests,
@@ -112,6 +115,7 @@ class TestOptimizationHttpCache {
112
115
  const parsedResponse = JSON.parse(payload)
113
116
  const result = parseSkippableSuitesResponse(parsedResponse, { ...options, validateRequiredFields: true })
114
117
  const testLevel = options.testLevel || 'suite'
118
+ logSkippableSuitesResponse(result, testLevel)
115
119
  const skippableItems = parsedResponse.data.filter(({ type }) => type === testLevel)
116
120
  incrementCountMetric(
117
121
  testLevel === 'test'
@@ -70,3 +70,11 @@ export type ConfigKey = KnownStringKeys<ConfigProperties>
70
70
  export type ConfigPath = ConfigPathFor<ConfigProperties>
71
71
  export type ConfigPathValue<TPath extends ConfigPath> = ConfigPathValueFor<ConfigProperties, TPath>
72
72
  export type ConfigDefaults = Partial<{ [TPath in ConfigPath]: ConfigPathValue<TPath> }>
73
+ export type ConfigurationOption = {
74
+ property?: string
75
+ parser?: (value: unknown, optionName: string, source: string) => unknown
76
+ type: string
77
+ canonicalName?: string
78
+ transformer?: (value: unknown, optionName: string, source: string) => unknown
79
+ telemetryTransformer?: (value: unknown) => unknown
80
+ }
@@ -1,9 +1,16 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert')
3
4
  const util = require('util')
4
5
 
5
6
  const { DD_MAJOR } = require('../../../../version')
6
- const { parsers, transformers, telemetryTransformers, setWarnInvalidValue } = require('./parsers')
7
+ const {
8
+ parsers,
9
+ programmaticTypeCoercions,
10
+ transformers,
11
+ telemetryTransformers,
12
+ setWarnInvalidValue,
13
+ } = require('./parsers')
7
14
  const applyMajorOverrides = require('./major-overrides')
8
15
  const {
9
16
  supportedConfigurations,
@@ -70,6 +77,7 @@ for (const [name, value] of Object.entries(defaults)) {
70
77
  function generateTelemetry (value = null, origin, optionName) {
71
78
  const tableEntry = configurationsTable[optionName]
72
79
  const { type, canonicalName = optionName } = tableEntry ?? { type: typeof value }
80
+ const error = parseErrors.get(`${canonicalName}${origin}`)
73
81
  // Sensitive configurations are excluded from configuration telemetry: their
74
82
  // entry is never added to `configWithOrigin`, the single source for every
75
83
  // telemetry path (app-started and app-client-configuration-change).
@@ -81,7 +89,9 @@ function generateTelemetry (value = null, origin, optionName) {
81
89
  // TODO: Validate that space separated tags are parsed by the backend. Optimizations would be possible with that.
82
90
  // TODO: How to handle telemetry reporting for aliases?
83
91
  if (value !== null) {
84
- if (telemetryTransformers[type]) {
92
+ if (error && typeof value === 'object') {
93
+ value = util.inspect(value, { customInspect: false, getters: false })
94
+ } else if (telemetryTransformers[type]) {
85
95
  value = telemetryTransformers[type](value)
86
96
  } else if (typeof value === 'object' && value !== null) {
87
97
  // Custom optionsTable entries (no `configurationsTable` row, e.g. `logger`)
@@ -100,7 +110,6 @@ function generateTelemetry (value = null, origin, optionName) {
100
110
  origin,
101
111
  seq_id: seqId++,
102
112
  }
103
- const error = parseErrors.get(`${canonicalName}${origin}`)
104
113
  if (error) {
105
114
  parseErrors.delete(`${canonicalName}${origin}`)
106
115
  telemetryEntry.error = error
@@ -135,12 +144,8 @@ function generateTelemetry (value = null, origin, optionName) {
135
144
  const optionsTable = {
136
145
  // Additional properties that are not supported by the supported-configurations.json file.
137
146
  lookup: {
138
- transformer (value) {
139
- if (typeof value === 'function') {
140
- return value
141
- }
142
- },
143
147
  property: 'lookup',
148
+ type: 'FUNCTION',
144
149
  },
145
150
  logger: {
146
151
  transformer (object) {
@@ -165,12 +170,15 @@ const optionsTable = {
165
170
  }
166
171
  },
167
172
  property: 'logger',
173
+ type: 'MAP',
168
174
  },
169
175
  isCiVisibility: {
170
176
  property: 'isCiVisibility',
177
+ type: 'BOOLEAN',
171
178
  },
172
179
  plugins: {
173
180
  property: 'plugins',
181
+ type: 'BOOLEAN',
174
182
  },
175
183
  }
176
184
 
@@ -185,13 +193,7 @@ const parser = (value, optionName, source) => {
185
193
 
186
194
  /**
187
195
  * @template {import('./config-types').ConfigPath} TPath
188
- * @type {Partial<Record<TPath, {
189
- * property?: string,
190
- * parser: (value: unknown, optionName: string, source: string) => unknown,
191
- * canonicalName?: string,
192
- * transformer?: (value: unknown, optionName: string, source: string) => unknown,
193
- * telemetryTransformer?: (value: unknown) => unknown
194
- * }>>} ConfigurationsTable
196
+ * @type {Partial<Record<TPath, import('./config-types').ConfigurationOption>>} ConfigurationsTable
195
197
  */
196
198
  const configurationsTable = {}
197
199
 
@@ -231,6 +233,7 @@ for (const [canonicalName, entries] of Object.entries(supportedConfigurations))
231
233
  ? `${entry.namespace}.${canonicalName}`
232
234
  : (entry.internalPropertyName ?? entry.configurationNames?.[0] ?? canonicalName)
233
235
  const type = entry.type.toUpperCase()
236
+ assert.ok(programmaticTypeCoercions[type])
234
237
 
235
238
  let transformer = transformers[entry.transform]
236
239
  if (entry.allowed) {
@@ -353,6 +356,8 @@ module.exports = {
353
356
  parseErrors,
354
357
 
355
358
  generateTelemetry,
359
+
360
+ warnInvalidValue,
356
361
  }
357
362
 
358
363
  // `dns` is instrumented, so requiring it pulls in the dns plugin, which loads
@@ -546,6 +546,7 @@ export interface GeneratedConfig {
546
546
  DD_CIVISIBILITY_AGENTLESS_ENABLED: boolean;
547
547
  DD_CIVISIBILITY_AGENTLESS_URL: URL | undefined;
548
548
  DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER: string | undefined;
549
+ DD_CIVISIBILITY_CODE_COVERAGE_REPORT_UPLOAD_ENABLED: boolean;
549
550
  DD_CIVISIBILITY_DANGEROUSLY_FORCE_COVERAGE: boolean;
550
551
  DD_CIVISIBILITY_DANGEROUSLY_FORCE_TEST_SKIPPING: boolean;
551
552
  DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED: boolean;
@@ -561,6 +562,7 @@ export interface GeneratedConfig {
561
562
  DD_CIVISIBILITY_TEST_COMMAND: string | undefined;
562
563
  DD_CIVISIBILITY_TEST_MODULE_ID: string | undefined;
563
564
  DD_CIVISIBILITY_TEST_SESSION_ID: string | undefined;
565
+ DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT: number | undefined;
564
566
  DD_TEST_FAILED_TEST_REPLAY_ENABLED: boolean;
565
567
  DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean | undefined;
566
568
  DD_TEST_FLEET_CONFIG_PATH: string | undefined;
@@ -636,6 +638,7 @@ export interface GeneratedEnvVarConfig {
636
638
  DD_CIVISIBILITY_AGENTLESS_ENABLED: boolean;
637
639
  DD_CIVISIBILITY_AGENTLESS_URL: URL | undefined;
638
640
  DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER: string | undefined;
641
+ DD_CIVISIBILITY_CODE_COVERAGE_REPORT_UPLOAD_ENABLED: boolean;
639
642
  DD_CIVISIBILITY_DANGEROUSLY_FORCE_COVERAGE: boolean;
640
643
  DD_CIVISIBILITY_DANGEROUSLY_FORCE_TEST_SKIPPING: boolean;
641
644
  DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED: boolean;
@@ -794,6 +797,7 @@ export interface GeneratedEnvVarConfig {
794
797
  DD_TELEMETRY_HEARTBEAT_INTERVAL: number;
795
798
  DD_TELEMETRY_LOG_COLLECTION_ENABLED: boolean;
796
799
  DD_TELEMETRY_METRICS_ENABLED: boolean;
800
+ DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT: number | undefined;
797
801
  DD_TEST_FAILED_TEST_REPLAY_ENABLED: boolean;
798
802
  DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean | undefined;
799
803
  DD_TEST_FLEET_CONFIG_PATH: string | undefined;
@@ -35,9 +35,10 @@ const {
35
35
  configWithOrigin,
36
36
  parseErrors,
37
37
  generateTelemetry,
38
+ warnInvalidValue,
38
39
  } = require('./defaults')
39
40
  const { normalizeService } = require('./normalize-service')
40
- const { transformers } = require('./parsers')
41
+ const { programmaticTypeCoercions, transformers } = require('./parsers')
41
42
 
42
43
  const RUNTIME_ID = uuid()
43
44
 
@@ -55,6 +56,7 @@ const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
55
56
  * @typedef {import('../../../../index').TracerOptions} TracerOptions
56
57
  * @typedef {import('./config-types').ConfigKey} ConfigKey
57
58
  * @typedef {import('./config-types').ConfigPath} ConfigPath
59
+ * @typedef {import('./config-types').ConfigurationOption} ConfigurationOption
58
60
  * @typedef {{
59
61
  * value: import('./config-types').ConfigPathValue<ConfigPath>,
60
62
  * source: TelemetrySource
@@ -121,6 +123,9 @@ function setAndTrack (config, name, value, rawValue = value, source = 'calculate
121
123
  if (value == null) {
122
124
  // TODO: This works as before while ignoring undefined programmatic options is not ideal.
123
125
  if (source !== 'default') {
126
+ if (rawValue !== value) {
127
+ generateTelemetry(rawValue, source, name)
128
+ }
124
129
  return
125
130
  }
126
131
  } else if (source === 'calculated' || source === 'remote_config') {
@@ -285,8 +290,13 @@ class Config extends ConfigBase {
285
290
  continue
286
291
  }
287
292
  }
288
- // TODO: Coerce mismatched types to the expected type, if possible. E.g., strings <> numbers
289
- const transformed = value !== undefined && entry.transformer ? entry.transformer(value, fullName, source) : value
293
+ const coerced = programmaticTypeCoercions[entry.type](value)
294
+ if (coerced === undefined && value !== undefined) {
295
+ warnInvalidValue(value, fullName, source, `Invalid ${entry.type} input`)
296
+ }
297
+ const transformed = coerced !== undefined && entry.transformer
298
+ ? entry.transformer(coerced, fullName, source)
299
+ : coerced
290
300
  setAndTrack(this, entry.property ?? name, transformed, value, source)
291
301
  }
292
302
  }
@@ -297,8 +297,87 @@ const parsers = {
297
297
  },
298
298
  }
299
299
 
300
+ const programmaticTypeCoercions = {
301
+ /**
302
+ * @param {unknown} value
303
+ */
304
+ BOOLEAN (value) {
305
+ if (typeof value === 'boolean') {
306
+ return value
307
+ }
308
+ if (typeof value === 'string') {
309
+ return parsers.BOOLEAN(value)
310
+ }
311
+ },
312
+ /**
313
+ * @param {unknown} value
314
+ */
315
+ INT (value) {
316
+ if (typeof value === 'number' || typeof value === 'string' && value.trim() !== '') {
317
+ return parsers.INT(value)
318
+ }
319
+ },
320
+ /**
321
+ * @param {unknown} value
322
+ */
323
+ DECIMAL (value) {
324
+ if (typeof value === 'number' || typeof value === 'string' && value.trim() !== '') {
325
+ return parsers.DECIMAL(value)
326
+ }
327
+ },
328
+ /**
329
+ * @param {unknown} value
330
+ */
331
+ STRING (value) {
332
+ if (typeof value === 'string') {
333
+ return value
334
+ }
335
+ if (typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value)) {
336
+ return String(value)
337
+ }
338
+ },
339
+ /**
340
+ * @param {unknown} value
341
+ */
342
+ FUNCTION (value) {
343
+ if (typeof value === 'function') {
344
+ return value
345
+ }
346
+ },
347
+ /**
348
+ * @param {unknown} value
349
+ */
350
+ ARRAY (value) {
351
+ if (Array.isArray(value)) {
352
+ for (const item of value) {
353
+ if (typeof item !== 'string' && typeof item !== 'number' && typeof item !== 'boolean') {
354
+ return
355
+ }
356
+ }
357
+ return value
358
+ }
359
+ },
360
+ /**
361
+ * @param {unknown} value
362
+ */
363
+ MAP (value) {
364
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
365
+ return value
366
+ }
367
+ },
368
+ /**
369
+ * @param {unknown} value
370
+ */
371
+ JSON (value) {
372
+ if (typeof value === 'object' && value !== null) {
373
+ return value
374
+ }
375
+ },
376
+ }
377
+
300
378
  module.exports = {
301
379
  parsers,
380
+ programmaticTypeCoercions,
302
381
  transformers,
303
382
  telemetryTransformers,
304
383
  setWarnInvalidValue,
@@ -470,6 +470,14 @@
470
470
  "namespace": "testOptimization"
471
471
  }
472
472
  ],
473
+ "DD_CIVISIBILITY_CODE_COVERAGE_REPORT_UPLOAD_ENABLED": [
474
+ {
475
+ "implementation": "A",
476
+ "type": "boolean",
477
+ "default": "true",
478
+ "namespace": "testOptimization"
479
+ }
480
+ ],
473
481
  "DD_CIVISIBILITY_DANGEROUSLY_FORCE_COVERAGE": [
474
482
  {
475
483
  "implementation": "A",
@@ -494,6 +502,15 @@
494
502
  "namespace": "testOptimization"
495
503
  }
496
504
  ],
505
+ "DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT": [
506
+ {
507
+ "implementation": "A",
508
+ "type": "int",
509
+ "allowed": "0|[1-9][0-9]*",
510
+ "default": null,
511
+ "namespace": "testOptimization"
512
+ }
513
+ ],
497
514
  "DD_CIVISIBILITY_ENABLED": [
498
515
  {
499
516
  "implementation": "A",
@@ -31,6 +31,13 @@ module.exports = {
31
31
  ERROR_MESSAGE: 'error.message',
32
32
  ERROR_STACK: 'error.stack',
33
33
  IGNORE_OTEL_ERROR: Symbol('ignore.otel.error'),
34
+ // Marks an `Http2Server` that another instrumentation (currently @grpc/grpc-js)
35
+ // owns and traces through its own span lifecycle over the raw HTTP/2 stream
36
+ // API. The http2 server instrumentation reads this to leave such servers
37
+ // untraced, so a gRPC call keeps a single span with gRPC as the top frame
38
+ // instead of gaining an extra web.request span. A module-local Symbol keeps
39
+ // the mark private to dd-trace and unforgeable from user code.
40
+ FOREIGN_HTTP2_SERVER: Symbol('foreign.http2.server'),
34
41
  COMPONENT: 'component',
35
42
  CLIENT_PORT_KEY: 'network.destination.port',
36
43
  PEER_SERVICE_KEY: 'peer.service',
@@ -136,16 +136,18 @@ const DsmPathwayCodec = {
136
136
  * @param {Buffer} dataStreamsContext.hash
137
137
  * @param {number} dataStreamsContext.pathwayStartNs
138
138
  * @param {number} dataStreamsContext.edgeStartNs
139
- * @param {object} carrier
139
+ * @param {object} [carrier]
140
+ * @returns {object | undefined}
140
141
  */
141
142
  encode (dataStreamsContext, carrier) {
142
- if (!dataStreamsContext || !dataStreamsContext.hash) {
143
- return
144
- }
143
+ if (!dataStreamsContext || !dataStreamsContext.hash) return
144
+ carrier ??= {}
145
145
  carrier[CONTEXT_PROPAGATION_KEY_BASE64] = encodePathwayContextBase64(dataStreamsContext)
146
146
 
147
147
  // eslint-disable-next-line eslint-rules/eslint-log-printf-style
148
148
  log.debug(() => `Injected into DSM carrier: ${JSON.stringify(pick(carrier, logKeys))}.`)
149
+
150
+ return carrier
149
151
  },
150
152
 
151
153
  /**
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @typedef {object} Feature
5
+ * @property {string} name
6
+ * @property {object} noop
7
+ * @property {() => object} factory
8
+ * @property {Function} [remoteConfig]
9
+ * @property {Function} [enable]
10
+ */
11
+
12
+ /** @type {{ [name: string]: Feature }} */
13
+ const features = {}
14
+
15
+ /**
16
+ * @param {Feature} feature
17
+ */
18
+ function registerFeature (feature) {
19
+ features[feature.name] = feature
20
+ }
21
+
22
+ module.exports = { features, registerFeature }
@@ -4,6 +4,7 @@ module.exports = {
4
4
  SPAN_KINDS: ['llm', 'agent', 'workflow', 'task', 'tool', 'embedding', 'retrieval'],
5
5
  SPAN_KIND: '_ml_obs.meta.span.kind',
6
6
  SESSION_ID: '_ml_obs.session_id',
7
+ SESSION_ID_TRACE_DEFAULT_KEY: '_ml_obs.trace_session_id',
7
8
  DECORATOR: '_ml_obs.decorator',
8
9
  INTEGRATION: '_ml_obs.integration',
9
10
  METADATA: '_ml_obs.meta.metadata',
@@ -13,6 +14,7 @@ module.exports = {
13
14
  ML_APP: '_ml_obs.meta.ml_app',
14
15
  PROPAGATED_PARENT_ID_KEY: '_dd.p.llmobs_parent_id',
15
16
  PROPAGATED_ML_APP_KEY: '_dd.p.llmobs_ml_app',
17
+ PROPAGATED_SESSION_ID_KEY: '_dd.p.llmobs_sid',
16
18
  PARENT_ID_KEY: '_ml_obs.llmobs_parent_id',
17
19
  PROPAGATED_SAMPLE_RATE_KEY: '_dd.p.llmobs_sr',
18
20
  PROPAGATED_SAMPLING_DECISION_KEY: '_dd.p.llmobs_sd',
@@ -7,8 +7,11 @@ const { DD_MAJOR } = require('../../../../version')
7
7
  const startupLogs = require('../startup-log')
8
8
  const {
9
9
  ML_APP,
10
+ SESSION_ID,
11
+ SESSION_ID_TRACE_DEFAULT_KEY,
10
12
  PROPAGATED_ML_APP_KEY,
11
13
  PROPAGATED_PARENT_ID_KEY,
14
+ PROPAGATED_SESSION_ID_KEY,
12
15
  SAMPLE_RATE,
13
16
  SAMPLING_DECISION,
14
17
  PROPAGATED_SAMPLE_RATE_KEY,
@@ -110,7 +113,7 @@ function disable () {
110
113
  }
111
114
 
112
115
  // since LLMObs traces can extend between services and be the same trace,
113
- // we need to propagate the parent id, mlApp, and sampling rate/decision.
116
+ // we need to propagate the parent id, mlApp, session id, and sampling rate/decision.
114
117
  function handleLLMObsInjection ({ carrier }) {
115
118
  // Respect the standard propagator's gate: when trace tag propagation is
116
119
  // disabled, don't write `x-datadog-tags` for LLMObs either.
@@ -130,8 +133,12 @@ function handleLLMObsInjection ({ carrier }) {
130
133
  mlObsSpanTags?.[SAMPLE_RATE] ?? parentContext?._trace?.tags?.[PROPAGATED_SAMPLE_RATE_KEY]
131
134
  const samplingDecision =
132
135
  mlObsSpanTags?.[SAMPLING_DECISION] ?? parentContext?._trace?.tags?.[PROPAGATED_SAMPLING_DECISION_KEY]
136
+ const sessionId =
137
+ mlObsSpanTags?.[SESSION_ID] ??
138
+ parentContext?._trace?.tags?.[SESSION_ID_TRACE_DEFAULT_KEY] ??
139
+ parentContext?._trace?.tags?.[PROPAGATED_SESSION_ID_KEY]
133
140
 
134
- if (!parentId && !mlApp && samplingDecision == null) return
141
+ if (!parentId && !mlApp && samplingDecision == null && !sessionId) return
135
142
 
136
143
  // `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*`
137
144
  // tags, so it may be undefined here — coalesce before appending.
@@ -139,6 +146,7 @@ function handleLLMObsInjection ({ carrier }) {
139
146
  let tags = existing || ''
140
147
  if (parentId) tags += `${tags ? ',' : ''}${PROPAGATED_PARENT_ID_KEY}=${parentId}`
141
148
  if (mlApp) tags += `${tags ? ',' : ''}${PROPAGATED_ML_APP_KEY}=${mlApp}`
149
+ if (sessionId) tags += `${tags ? ',' : ''}${PROPAGATED_SESSION_ID_KEY}=${sessionId}`
142
150
  if (sampleRate != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLE_RATE_KEY}=${sampleRate}`
143
151
  if (samplingDecision != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLING_DECISION_KEY}=${samplingDecision}`
144
152
  if (tags !== existing) carrier['x-datadog-tags'] = tags
@@ -7,6 +7,7 @@ const {
7
7
  MODEL_NAME,
8
8
  MODEL_PROVIDER,
9
9
  SESSION_ID,
10
+ SESSION_ID_TRACE_DEFAULT_KEY,
10
11
  ML_APP,
11
12
  SPAN_KIND,
12
13
  INPUT_VALUE,
@@ -35,6 +36,7 @@ const {
35
36
  INTEGRATION,
36
37
  DECORATOR,
37
38
  PROPAGATED_ML_APP_KEY,
39
+ PROPAGATED_SESSION_ID_KEY,
38
40
  DEFAULT_PROMPT_NAME,
39
41
  INTERNAL_CONTEXT_VARIABLE_KEYS,
40
42
  INTERNAL_QUERY_VARIABLE_KEYS,
@@ -139,7 +141,11 @@ class LLMObsTagger {
139
141
  if (modelName) this.tagModelName(span, modelName)
140
142
  if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
141
143
 
142
- sessionId = sessionId || registry.get(parent)?.[SESSION_ID]
144
+ const traceTags = span.context()._trace.tags
145
+ sessionId = sessionId ||
146
+ registry.get(parent)?.[SESSION_ID] ||
147
+ traceTags[SESSION_ID_TRACE_DEFAULT_KEY] ||
148
+ traceTags[PROPAGATED_SESSION_ID_KEY]
143
149
  if (sessionId) this._setTag(span, SESSION_ID, sessionId)
144
150
  if (integration) this._setTag(span, INTEGRATION, integration)
145
151
  if (_decorator) this._setTag(span, DECORATOR, _decorator)
@@ -752,6 +758,18 @@ class LLMObsTagger {
752
758
 
753
759
  const tagsCarrier = registry.get(span)
754
760
  tagsCarrier[key] = value
761
+
762
+ // The first session set in a trace becomes the trace-level default, stored on the trace-shared
763
+ // tags so later spans (incl. those under a session-less parent) inherit it in-process.
764
+ // Established here, the single choke point for session writes, so sessions post-populated by
765
+ // integrations after span start also seed it. First-writer wins, so an explicit session still
766
+ // overrides locally. Cross-service injection is handled centrally in `handleLLMObsInjection`.
767
+ if (key === SESSION_ID && value) {
768
+ const traceTags = span.context()._trace.tags
769
+ if (traceTags[SESSION_ID_TRACE_DEFAULT_KEY] === undefined) {
770
+ traceTags[SESSION_ID_TRACE_DEFAULT_KEY] = value
771
+ }
772
+ }
755
773
  }
756
774
  }
757
775
 
@@ -1,8 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const { features } = require('../feature-registry')
3
4
  const NoopAppsecSdk = require('../appsec/sdk/noop')
4
5
  const NoopLLMObsSDK = require('../llmobs/noop')
5
- const NoopFlaggingProvider = require('../openfeature/noop')
6
6
  const NoopAIGuardSDK = require('../aiguard/noop')
7
7
  const NoopDogStatsDClient = require('./dogstatsd')
8
8
  const NoopTracer = require('./tracer')
@@ -11,7 +11,6 @@ const noop = new NoopTracer()
11
11
  const noopAppsec = new NoopAppsecSdk()
12
12
  const noopDogStatsDClient = new NoopDogStatsDClient()
13
13
  const noopLLMObs = new NoopLLMObsSDK(noop)
14
- const noopOpenFeatureProvider = new NoopFlaggingProvider()
15
14
  const noopAIGuard = new NoopAIGuardSDK()
16
15
  const noopProfiling = {
17
16
  setCustomLabelKeys () {},
@@ -25,8 +24,10 @@ class NoopProxy {
25
24
  this.appsec = noopAppsec
26
25
  this.dogstatsd = noopDogStatsDClient
27
26
  this.llmobs = noopLLMObs
28
- this.openfeature = noopOpenFeatureProvider
29
27
  this.aiguard = noopAIGuard
28
+ for (const { name, noop } of Object.values(features)) {
29
+ this[name] = noop
30
+ }
30
31
  this.setBaggageItem = (key, value) => {}
31
32
  this.getBaggageItem = (key) => {}
32
33
  this.getAllBaggageItems = () => {}
@@ -82,7 +83,7 @@ class NoopProxy {
82
83
  }
83
84
 
84
85
  inject () {
85
- return this._tracer.inject.apply(this._tracer, arguments)
86
+ this._tracer.inject.apply(this._tracer, arguments)
86
87
  }
87
88
 
88
89
  extract () {