dd-trace 5.109.0 → 5.110.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 (128) hide show
  1. package/ci/init.js +4 -3
  2. package/index.d.ts +66 -24
  3. package/loader-hook.mjs +88 -2
  4. package/package.json +18 -14
  5. package/packages/datadog-esbuild/index.js +26 -0
  6. package/packages/datadog-esbuild/src/utils.js +46 -3
  7. package/packages/datadog-instrumentations/src/connect.js +4 -3
  8. package/packages/datadog-instrumentations/src/fastify.js +4 -12
  9. package/packages/datadog-instrumentations/src/graphql.js +17 -6
  10. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
  11. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
  12. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
  13. package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
  14. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
  16. package/packages/datadog-instrumentations/src/koa.js +3 -2
  17. package/packages/datadog-instrumentations/src/mariadb.js +4 -2
  18. package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
  19. package/packages/datadog-instrumentations/src/next.js +10 -10
  20. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
  21. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  22. package/packages/datadog-instrumentations/src/restify.js +4 -3
  23. package/packages/datadog-instrumentations/src/router.js +18 -5
  24. package/packages/datadog-instrumentations/src/selenium.js +1 -2
  25. package/packages/datadog-instrumentations/src/tedious.js +28 -0
  26. package/packages/datadog-instrumentations/src/vitest.js +264 -7
  27. package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
  28. package/packages/datadog-plugin-aws-sdk/src/base.js +13 -9
  29. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
  30. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +83 -91
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -1
  32. package/packages/datadog-plugin-http/src/client.js +8 -3
  33. package/packages/datadog-plugin-http2/src/client.js +5 -2
  34. package/packages/datadog-plugin-mysql/src/index.js +7 -2
  35. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
  36. package/packages/datadog-plugin-undici/src/index.js +5 -2
  37. package/packages/datadog-plugin-vitest/src/index.js +7 -1
  38. package/packages/datadog-webpack/index.js +17 -1
  39. package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
  40. package/packages/dd-trace/src/aiguard/sdk.js +13 -1
  41. package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
  42. package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
  43. package/packages/dd-trace/src/appsec/index.js +10 -6
  44. package/packages/dd-trace/src/appsec/reporter.js +1 -0
  45. package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
  46. package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
  47. package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
  48. package/packages/dd-trace/src/config/defaults.js +22 -3
  49. package/packages/dd-trace/src/config/generated-config-types.d.ts +535 -1
  50. package/packages/dd-trace/src/config/helper.js +133 -33
  51. package/packages/dd-trace/src/config/index.js +4 -1
  52. package/packages/dd-trace/src/config/parsers.js +19 -1
  53. package/packages/dd-trace/src/config/supported-configurations.json +21 -3
  54. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -35
  55. package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
  56. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
  57. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
  58. package/packages/dd-trace/src/debugger/index.js +5 -0
  59. package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
  60. package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
  61. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
  62. package/packages/dd-trace/src/exporters/common/request.js +1 -20
  63. package/packages/dd-trace/src/exporters/common/url.js +34 -0
  64. package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
  65. package/packages/dd-trace/src/guardrails/index.js +8 -5
  66. package/packages/dd-trace/src/index.js +6 -4
  67. package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
  68. package/packages/dd-trace/src/llmobs/index.js +16 -5
  69. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
  70. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
  71. package/packages/dd-trace/src/llmobs/sdk.js +5 -3
  72. package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
  73. package/packages/dd-trace/src/llmobs/tagger.js +55 -0
  74. package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
  75. package/packages/dd-trace/src/log/index.js +18 -20
  76. package/packages/dd-trace/src/openfeature/flagging_provider.js +2 -7
  77. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
  78. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
  79. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
  80. package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
  81. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
  82. package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
  83. package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
  84. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
  85. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
  86. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +11 -4
  87. package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
  88. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  89. package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
  90. package/packages/dd-trace/src/opentelemetry/trace/index.js +2 -1
  91. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
  92. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
  93. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
  94. package/packages/dd-trace/src/opentracing/span_context.js +17 -0
  95. package/packages/dd-trace/src/plugin_manager.js +7 -1
  96. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -1
  97. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
  98. package/packages/dd-trace/src/plugins/util/test.js +80 -0
  99. package/packages/dd-trace/src/plugins/util/url.js +68 -1
  100. package/packages/dd-trace/src/plugins/util/web.js +19 -36
  101. package/packages/dd-trace/src/priority_sampler.js +1 -13
  102. package/packages/dd-trace/src/profiling/config.js +129 -198
  103. package/packages/dd-trace/src/profiling/constants.js +4 -7
  104. package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
  105. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
  106. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
  107. package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
  108. package/packages/dd-trace/src/profiling/oom.js +71 -0
  109. package/packages/dd-trace/src/profiling/profiler.js +35 -23
  110. package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
  111. package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
  112. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
  113. package/packages/dd-trace/src/ritm.js +4 -2
  114. package/packages/dd-trace/src/serverless.js +1 -2
  115. package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
  116. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
  117. package/packages/dd-trace/src/span_format.js +2 -1
  118. package/packages/dd-trace/src/span_processor.js +6 -0
  119. package/packages/dd-trace/src/telemetry/send-data.js +12 -10
  120. package/packages/dd-trace/src/tracer.js +15 -1
  121. package/packages/dd-trace/src/util.js +14 -0
  122. package/register.js +58 -1
  123. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  124. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  125. package/vendor/dist/pprof-format/index.js +1 -1
  126. package/vendor/dist/protobufjs/index.js +1 -1
  127. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  128. package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ const { DD_TRACE_SYMBOL, PROBE_SAMPLER_SYMBOL } = require('../probe_sampler_constants')
4
+
5
+ const SAMPLER_EXPRESSION = `globalThis[Symbol.for(${JSON.stringify(DD_TRACE_SYMBOL)})]?.` +
6
+ `[Symbol.for(${JSON.stringify(PROBE_SAMPLER_SYMBOL)})]`
7
+
8
+ module.exports = {
9
+ compileBreakpointCondition,
10
+ getRemoveProbeExpression,
11
+ }
12
+
13
+ /**
14
+ * Build the expression that removes a probe from runtime sampler state. Called by the devtools worker and evaluated on
15
+ * the debuggee.
16
+ *
17
+ * @param {string} id - The probe id.
18
+ * @returns {string}
19
+ */
20
+ function getRemoveProbeExpression (id) {
21
+ return `${SAMPLER_EXPRESSION}?.remove(${JSON.stringify(id)})`
22
+ }
23
+
24
+ /**
25
+ * Build a Chrome DevTools breakpoint condition that samples all matching probes at a location. Called by the devtools
26
+ * worker.
27
+ *
28
+ * @param {{
29
+ * id: string,
30
+ * samplingIndex: number,
31
+ * nsBetweenSampling: bigint,
32
+ * condition?: string,
33
+ * captureSnapshot?: boolean,
34
+ * compiledCaptureExpressions?: object[]
35
+ * }[]} probes - The probes at the breakpoint location.
36
+ * @returns {string}
37
+ */
38
+ function compileBreakpointCondition (probes) {
39
+ const probeConditions = []
40
+ for (const probe of probes) {
41
+ probeConditions.push(compileProbeCondition(probe))
42
+ }
43
+
44
+ // NOTE: $dd_sampler is read from the realm-local `globalThis` where it was installed (the main
45
+ // realm). A probe whose code runs in a different V8 realm (e.g. a `vm.createContext` script with a
46
+ // file-path filename) won't see it and will silently never fire. Known limitation: a breakpoint
47
+ // condition has no realm-independent handle to reach, so we degrade rather than crash.
48
+ return `(() => {
49
+ const $dd_sampler = ${SAMPLER_EXPRESSION}
50
+ if ($dd_sampler === undefined) return false
51
+ let $dd_sampled = false
52
+ ${probeConditions.join('\n ')}
53
+ return $dd_sampled
54
+ })()`
55
+ }
56
+
57
+ /**
58
+ * Build the condition fragment for a single probe. Called by the devtools worker while building breakpoint conditions.
59
+ *
60
+ * @param {{
61
+ * id: string,
62
+ * samplingIndex: number,
63
+ * nsBetweenSampling: bigint,
64
+ * condition?: string,
65
+ * captureSnapshot?: boolean,
66
+ * compiledCaptureExpressions?: object[]
67
+ * }} probe - The probe to sample.
68
+ * @returns {string}
69
+ */
70
+ function compileProbeCondition (probe) {
71
+ const sample = `$dd_sampler.makeSampleDecision(${probe.samplingIndex}, ${JSON.stringify(probe.id)}, ` +
72
+ `${probe.nsBetweenSampling}n, ${probe.captureSnapshot === true || probe.compiledCaptureExpressions !== undefined})`
73
+
74
+ if (probe.condition === undefined) {
75
+ return `$dd_sampled = ${sample} || $dd_sampled`
76
+ }
77
+
78
+ return `try {
79
+ if ((${probe.condition}) === true) {
80
+ $dd_sampled = ${sample} || $dd_sampled
81
+ }
82
+ } catch {}`
83
+ }
@@ -33,6 +33,7 @@ module.exports = {
33
33
  locationToBreakpoint: new Map(),
34
34
  breakpointToProbes: new Map(),
35
35
  probeToLocation: new Map(),
36
+ samplingIndexToProbe: new Map(),
36
37
 
37
38
  _loadedScripts: loadedScripts, // Only exposed for testing
38
39
  _scriptUrls: scriptUrls, // Only exposed for testing
@@ -8,6 +8,7 @@ const log = require('../log')
8
8
  const { fetchAgentInfo } = require('../agent/info')
9
9
  const getDebuggerConfig = require('./config')
10
10
  const { DEBUGGER_DIAGNOSTICS_V1, DEBUGGER_INPUT_V2 } = require('./constants')
11
+ const { installProbeSampler, uninstallProbeSampler } = require('./probe_sampler')
11
12
 
12
13
  /**
13
14
  * @typedef {ReturnType<import('../config')>} Config
@@ -65,6 +66,8 @@ function start (config, rcInstance) {
65
66
 
66
67
  globalThis[Symbol.for('dd-trace')].utilTypes = types
67
68
 
69
+ const probeSamplerBuffer = installProbeSampler()
70
+
68
71
  readProbeFile(config.dynamicInstrumentation.probeFile, (probes) => {
69
72
  const action = 'apply'
70
73
  for (const probe of probes) {
@@ -110,6 +113,7 @@ function start (config, rcInstance) {
110
113
  probePort: probeChannel.port1,
111
114
  logPort: logChannel.port1,
112
115
  configPort: configChannel.port1,
116
+ probeSamplerBuffer,
113
117
  },
114
118
  transferList: [probeChannel.port1, logChannel.port1, configChannel.port1],
115
119
  }
@@ -187,6 +191,7 @@ function cleanup (error) {
187
191
  worker.removeAllListeners()
188
192
  worker = null
189
193
  }
194
+ uninstallProbeSampler()
190
195
  configChannel = null
191
196
  inputPath = null
192
197
 
@@ -0,0 +1,112 @@
1
+ 'use strict'
2
+
3
+ const { MAX_SNAPSHOTS_PER_SECOND_GLOBALLY } = require('./devtools_client/defaults')
4
+ const {
5
+ DD_TRACE_SYMBOL,
6
+ MAX_SAMPLED_PROBES_PER_PAUSE,
7
+ PROBE_SAMPLER_SYMBOL,
8
+ SAMPLED_PROBE_COUNT_INDEX,
9
+ SAMPLED_PROBE_INDEXES_START,
10
+ SAMPLED_PROBE_OVERFLOW_INDEX,
11
+ } = require('./probe_sampler_constants')
12
+
13
+ const ddTraceGlobal = /** @type {Record<symbol, SharedArrayBuffer | object | undefined>} */ (
14
+ /** @type {Record<symbol, unknown>} */ (globalThis)[Symbol.for(DD_TRACE_SYMBOL)]
15
+ )
16
+
17
+ module.exports = {
18
+ installProbeSampler,
19
+ uninstallProbeSampler,
20
+ }
21
+
22
+ /**
23
+ * Install the runtime sampler in the debuggee context.
24
+ *
25
+ * @returns {SharedArrayBuffer} The shared sampler buffer to pass to the debugger worker.
26
+ */
27
+ function installProbeSampler () {
28
+ const buffer = createProbeSamplerBuffer()
29
+
30
+ const lastCaptureNsByProbeId = new Map()
31
+ const sampledProbeIndexes = new Int32Array(buffer)
32
+ Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_COUNT_INDEX, 0)
33
+ Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_OVERFLOW_INDEX, 0)
34
+
35
+ const oneSecondNs = 1_000_000_000n
36
+ let globalSnapshotSamplingRateWindowStart = 0n
37
+ let snapshotsSampledWithinTheLastSecond = 0
38
+
39
+ ddTraceGlobal[Symbol.for(PROBE_SAMPLER_SYMBOL)] = {
40
+ /**
41
+ * Decide if a probe should be sampled and store sampled probe indexes for the debugger worker.
42
+ *
43
+ * @param {number} probeIndex - The worker-side probe sampling index.
44
+ * @param {string} probeId - The probe id.
45
+ * @param {bigint} nsBetweenSampling - Minimum nanoseconds between samples for this probe.
46
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe counts toward the global snapshot sample limit.
47
+ * @returns {boolean} Whether this probe should make the breakpoint condition pause.
48
+ */
49
+ makeSampleDecision (probeIndex, probeId, nsBetweenSampling, isSnapshotProducingProbe) {
50
+ const now = process.hrtime.bigint()
51
+ const lastCaptureNs = lastCaptureNsByProbeId.get(probeId)
52
+ if (lastCaptureNs !== undefined && now - lastCaptureNs < nsBetweenSampling) return false
53
+
54
+ let shouldResetGlobalSnapshotRateWindow = false
55
+ if (isSnapshotProducingProbe === true) {
56
+ if (now - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
57
+ shouldResetGlobalSnapshotRateWindow = true
58
+ } else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
59
+ return false
60
+ }
61
+ }
62
+
63
+ const sampledProbeCount = Atomics.add(sampledProbeIndexes, SAMPLED_PROBE_COUNT_INDEX, 1)
64
+ if (sampledProbeCount >= MAX_SAMPLED_PROBES_PER_PAUSE) {
65
+ Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_OVERFLOW_INDEX, 1)
66
+ return false
67
+ }
68
+
69
+ if (isSnapshotProducingProbe === true) {
70
+ if (shouldResetGlobalSnapshotRateWindow === true) {
71
+ snapshotsSampledWithinTheLastSecond = 1
72
+ globalSnapshotSamplingRateWindowStart = now
73
+ } else {
74
+ snapshotsSampledWithinTheLastSecond++
75
+ }
76
+ }
77
+
78
+ lastCaptureNsByProbeId.set(probeId, now)
79
+ Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_INDEXES_START + sampledProbeCount, probeIndex)
80
+ return true
81
+ },
82
+
83
+ /**
84
+ * Remove cached sampling state for a probe.
85
+ *
86
+ * @param {string} probeId - The probe id.
87
+ */
88
+ remove (probeId) {
89
+ lastCaptureNsByProbeId.delete(probeId)
90
+ },
91
+ }
92
+
93
+ return buffer
94
+ }
95
+
96
+ /**
97
+ * Remove the runtime sampler from the debuggee context.
98
+ */
99
+ function uninstallProbeSampler () {
100
+ delete ddTraceGlobal[Symbol.for(PROBE_SAMPLER_SYMBOL)]
101
+ }
102
+
103
+ /**
104
+ * Create the shared buffer used to hand sampled probe indexes from breakpoint conditions to the debugger worker.
105
+ *
106
+ * @returns {SharedArrayBuffer}
107
+ */
108
+ function createProbeSamplerBuffer () {
109
+ return new SharedArrayBuffer(
110
+ (SAMPLED_PROBE_INDEXES_START + MAX_SAMPLED_PROBES_PER_PAUSE) * Int32Array.BYTES_PER_ELEMENT
111
+ )
112
+ }
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const DD_TRACE_SYMBOL = 'dd-trace'
4
+ const PROBE_SAMPLER_SYMBOL = 'dd-trace.debugger.probeSampler'
5
+
6
+ // Shared sampler contract used by the main debugger bootstrap and the devtools worker.
7
+ const MAX_SAMPLED_PROBES_PER_PAUSE = 256
8
+ const SAMPLED_PROBE_COUNT_INDEX = 0
9
+ const SAMPLED_PROBE_OVERFLOW_INDEX = 1
10
+ const SAMPLED_PROBE_INDEXES_START = 2
11
+
12
+ module.exports = {
13
+ DD_TRACE_SYMBOL,
14
+ MAX_SAMPLED_PROBES_PER_PAUSE,
15
+ PROBE_SAMPLER_SYMBOL,
16
+ SAMPLED_PROBE_COUNT_INDEX,
17
+ SAMPLED_PROBE_INDEXES_START,
18
+ SAMPLED_PROBE_OVERFLOW_INDEX,
19
+ }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
  const { version: ddTraceVersion } = require('../../../../package.json')
3
- const { ITR_CORRELATION_ID } = require('../../src/plugins/util/test')
3
+ const { ITR_CORRELATION_ID, TEST_LEVELS_METADATA } = require('../../src/plugins/util/test')
4
4
  const id = require('../../src/id')
5
5
  const {
6
6
  distributionMetric,
@@ -9,10 +9,15 @@ const {
9
9
  } = require('../ci-visibility/telemetry')
10
10
  const { MsgpackChunk } = require('../msgpack')
11
11
  const { AgentEncoder } = require('./0.4')
12
- const { truncateSpanTestOpt, normalizeSpan } = require('./tags-processors')
12
+ const {
13
+ truncateSpanTestOpt,
14
+ normalizeSpan,
15
+ MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION,
16
+ } = require('./tags-processors')
13
17
 
14
18
  const ENCODING_VERSION = 1
15
19
  const ALLOWED_CONTENT_TYPES = new Set(['test_session_end', 'test_module_end', 'test_suite_end', 'test'])
20
+ const ALLOWED_METADATA_TARGETS = new Set([...ALLOWED_CONTENT_TYPES, TEST_LEVELS_METADATA])
16
21
 
17
22
  const TEST_SUITE_KEYS_LENGTH = 12
18
23
  const TEST_MODULE_KEYS_LENGTH = 11
@@ -36,6 +41,33 @@ function formatSpan (span) {
36
41
  }
37
42
  }
38
43
 
44
+ function isAllowedMetadataTarget (target) {
45
+ return ALLOWED_METADATA_TARGETS.has(target)
46
+ }
47
+
48
+ function isEncodableMetadataValue (value) {
49
+ return typeof value === 'string' || typeof value === 'number'
50
+ }
51
+
52
+ function truncateTestLevelMetadataValue (value) {
53
+ if (typeof value !== 'string' || value.length <= MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION) {
54
+ return value
55
+ }
56
+ return `${value.slice(0, MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION)}...`
57
+ }
58
+
59
+ function truncateTestLevelMetadataTags (tags) {
60
+ const truncatedTags = {}
61
+ let hasTags = false
62
+ for (const key of Object.keys(tags)) {
63
+ const value = truncateTestLevelMetadataValue(tags[key])
64
+ if (!isEncodableMetadataValue(value)) continue
65
+ truncatedTags[key] = value
66
+ hasTags = true
67
+ }
68
+ return hasTags ? truncatedTags : undefined
69
+ }
70
+
39
71
  class AgentlessCiVisibilityEncoder extends AgentEncoder {
40
72
  constructor (writer, { runtimeId, service, env }) {
41
73
  super(writer, INTAKE_SOFT_LIMIT)
@@ -49,6 +81,7 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
49
81
 
50
82
  this.metadataTags = {}
51
83
  this.wildcardMetadataTags = {}
84
+ this.testLevelsMetadataKeys = []
52
85
 
53
86
  this.reset()
54
87
  }
@@ -60,12 +93,39 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
60
93
  ...tags['*'],
61
94
  }
62
95
  }
63
- for (const type of ALLOWED_CONTENT_TYPES) {
64
- if (tags[type]) {
65
- this.metadataTags[type] = {
66
- ...this.metadataTags[type],
67
- ...tags[type],
68
- }
96
+
97
+ for (const target of Object.keys(tags)) {
98
+ if (target === '*' || !tags[target] || !isAllowedMetadataTarget(target)) continue
99
+
100
+ const targetTags = target === TEST_LEVELS_METADATA
101
+ ? truncateTestLevelMetadataTags(tags[target])
102
+ : tags[target]
103
+ if (!targetTags) continue
104
+
105
+ this.metadataTags[target] = {
106
+ ...this.metadataTags[target],
107
+ ...targetTags,
108
+ }
109
+
110
+ if (target === TEST_LEVELS_METADATA) {
111
+ this.testLevelsMetadataKeys = Object.keys(this.metadataTags[target])
112
+ }
113
+ }
114
+ }
115
+
116
+ _removeDuplicateTestLevelsMetadata (event) {
117
+ if (!ALLOWED_CONTENT_TYPES.has(event.type)) return
118
+
119
+ const meta = event.content.meta
120
+ if (!meta) return
121
+
122
+ const testLevelsMetadataTags = this.metadataTags[TEST_LEVELS_METADATA]
123
+ const testLevelsMetadataKeys = this.testLevelsMetadataKeys
124
+
125
+ for (let i = 0; i < testLevelsMetadataKeys.length; i++) {
126
+ const key = testLevelsMetadataKeys[i]
127
+ if (meta[key] === testLevelsMetadataTags[key]) {
128
+ delete meta[key]
69
129
  }
70
130
  }
71
131
  }
@@ -276,7 +336,11 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
276
336
 
277
337
  this._eventCount += events.length
278
338
 
339
+ const hasTestLevelsMetadata = this.testLevelsMetadataKeys.length !== 0
279
340
  for (const event of events) {
341
+ if (hasTestLevelsMetadata) {
342
+ this._removeDuplicateTestLevelsMetadata(event)
343
+ }
280
344
  this._encodeEvent(bytes, event)
281
345
  }
282
346
  distributionMetric(
@@ -343,24 +407,11 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
343
407
  this._encodeString(bytes, 'version')
344
408
  bytes.writeNumber(payload.version)
345
409
  this._encodeString(bytes, 'metadata')
346
- bytes.writeMapPrefix(Object.keys(payload.metadata).length)
347
- this._encodeString(bytes, '*')
348
- this._encodeMap(bytes, payload.metadata['*'])
349
- if (payload.metadata.test) {
350
- this._encodeString(bytes, 'test')
351
- this._encodeMap(bytes, payload.metadata.test)
352
- }
353
- if (payload.metadata.test_suite_end) {
354
- this._encodeString(bytes, 'test_suite_end')
355
- this._encodeMap(bytes, payload.metadata.test_suite_end)
356
- }
357
- if (payload.metadata.test_module_end) {
358
- this._encodeString(bytes, 'test_module_end')
359
- this._encodeMap(bytes, payload.metadata.test_module_end)
360
- }
361
- if (payload.metadata.test_session_end) {
362
- this._encodeString(bytes, 'test_session_end')
363
- this._encodeMap(bytes, payload.metadata.test_session_end)
410
+ const metadataKeys = Object.keys(payload.metadata)
411
+ bytes.writeMapPrefix(metadataKeys.length)
412
+ for (const metadataKey of metadataKeys) {
413
+ this._encodeString(bytes, metadataKey)
414
+ this._encodeMap(bytes, payload.metadata[metadataKey])
364
415
  }
365
416
  this._encodeString(bytes, 'events')
366
417
  this._eventsOffset = bytes.length
@@ -11,7 +11,7 @@ const zlib = require('zlib')
11
11
 
12
12
  const { storage } = require('../../../../datadog-core')
13
13
  const log = require('../../log')
14
- const { urlToHttpOptions } = require('./url-to-http-options-polyfill')
14
+ const { parseUrl } = require('./url')
15
15
  const docker = require('./docker')
16
16
  const { httpAgent, httpsAgent } = require('./agents')
17
17
  const {
@@ -27,25 +27,6 @@ const maxActiveBufferSize = 1024 * 1024 * 64
27
27
 
28
28
  let activeBufferSize = 0
29
29
 
30
- /**
31
- * @param {string|URL|object} urlObjOrString
32
- * @returns {object}
33
- */
34
- function parseUrl (urlObjOrString) {
35
- if (urlObjOrString !== null && typeof urlObjOrString === 'object') return urlToHttpOptions(urlObjOrString)
36
-
37
- const url = urlToHttpOptions(new URL(urlObjOrString))
38
-
39
- // Special handling if we're using named pipes on Windows
40
- if (url.protocol === 'unix:' && url.hostname === '.') {
41
- const udsPath = urlObjOrString.slice(5)
42
- url.path = udsPath
43
- url.pathname = udsPath
44
- }
45
-
46
- return url
47
- }
48
-
49
30
  /**
50
31
  * @param {string} hostname Host as resolved by {@link parseUrl}; IPv6 is unbracketed (`::1`).
51
32
  */
@@ -0,0 +1,34 @@
1
+ 'use strict'
2
+
3
+ const { urlToHttpOptions } = require('./url-to-http-options-polyfill')
4
+
5
+ /**
6
+ * Convert an agent/intake URL into Node http(s) request options.
7
+ *
8
+ * A Windows named pipe (`unix://./pipe/<name>`) parses the `.` as the URL
9
+ * authority, dropping it from the path. Fold it back so the socket path stays
10
+ * `//./pipe/<name>`; otherwise it collapses to `/pipe/<name>` and misses the
11
+ * pipe. Exporters receive `config.url` as a URL object, so this must run for
12
+ * both string and object input.
13
+ *
14
+ * @param {string|URL|object} urlObjOrString
15
+ * @returns {object}
16
+ */
17
+ function parseUrl (urlObjOrString) {
18
+ // `urlToHttpOptions` returns `pathname` at runtime, but @types/node narrows it
19
+ // to `ClientRequestArgs`, which omits it; cast so the named-pipe fold below can
20
+ // read and rewrite it.
21
+ const url = /** @type {import('node:http').ClientRequestArgs & { pathname: string }} */ (
22
+ urlObjOrString !== null && typeof urlObjOrString === 'object'
23
+ ? urlToHttpOptions(urlObjOrString)
24
+ : urlToHttpOptions(new URL(urlObjOrString))
25
+ )
26
+
27
+ if (url.protocol === 'unix:' && url.hostname === '.') {
28
+ url.path = url.pathname = `//.${url.pathname}`
29
+ }
30
+
31
+ return url
32
+ }
33
+
34
+ module.exports = { parseUrl }
@@ -37,9 +37,7 @@ function makeRequest (data, url, cb) {
37
37
  'Datadog-Meta-Tracer-Version': pkg.version,
38
38
  'Content-Type': 'application/msgpack',
39
39
  },
40
- protocol: url.protocol,
41
- hostname: url.hostname,
42
- port: url.port,
40
+ url,
43
41
  }
44
42
 
45
43
  log.debug('Request to the intake: %j', options)
@@ -14,11 +14,13 @@ function guard (fn) {
14
14
  var initBailout = false
15
15
  var clobberBailout = false
16
16
  var forced = isTrue(process.env.DD_INJECT_FORCE)
17
- var engines = require('../../../../package.json').engines
18
- var versions = engines.node.match(/^>=(\d+) <(\d+)$/)
17
+ var pkg = require('../../../../package.json')
18
+ var engines = pkg.engines
19
+ var versions = engines.node.match(/^>=(\d+)$/)
19
20
  var minMajor = versions[1]
20
- var nextMajor = versions[2]
21
+ var nextMajor = pkg.nodeMaxMajor
21
22
  var version = process.versions.node
23
+ var supportedRange = engines.node + ' <' + nextMajor
22
24
 
23
25
  if (process.env.DD_INJECTION_ENABLED) {
24
26
  // If we're running via single-step install, and we're in the app's
@@ -45,16 +47,17 @@ function guard (fn) {
45
47
  // should not initialize the tracer.
46
48
  if (!clobberBailout && (NODE_MAJOR < minMajor || NODE_MAJOR >= nextMajor)) {
47
49
  initBailout = true
50
+ var runtimeInfo = 'Incompatible runtime Node.js ' + version + ', supported runtimes: Node.js ' + supportedRange
48
51
  telemetry([
49
52
  { name: 'abort', tags: ['reason:incompatible_runtime'] },
50
53
  { name: 'abort.runtime', tags: [] }
51
54
  ], undefined, {
52
55
  result: 'abort',
53
56
  result_class: 'incompatible_runtime',
54
- result_reason: 'Incompatible runtime Node.js ' + version + ', supported runtimes: Node.js ' + engines.node
57
+ result_reason: runtimeInfo
55
58
  })
56
59
  log.info('Aborting application instrumentation due to incompatible_runtime.')
57
- log.info('Found incompatible runtime Node.js %s, Supported runtimes: Node.js %s.', version, engines.node)
60
+ log.info('Found incompatible runtime Node.js %s, Supported runtimes: Node.js %s.', version, supportedRange)
58
61
  if (forced) {
59
62
  log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.')
60
63
  }
@@ -1,14 +1,16 @@
1
1
  'use strict'
2
2
 
3
3
  const { getValueFromEnvSources } = require('./config/helper')
4
- const { isFalse } = require('./util')
5
4
 
6
5
  // Global `jest` is only present in Jest workers.
7
6
  const inJestWorker = typeof jest !== 'undefined'
8
7
 
9
- const ddTraceDisabled = getValueFromEnvSources('DD_TRACE_ENABLED')
10
- ? isFalse(getValueFromEnvSources('DD_TRACE_ENABLED'))
11
- : String(getValueFromEnvSources('OTEL_TRACES_EXPORTER')).toLowerCase() === 'none'
8
+ // skipDefault: distinguish an unset DD_TRACE_ENABLED (fall back to the OTel signal) from an
9
+ // explicit value; the registered default would otherwise mask the OTEL_TRACES_EXPORTER check.
10
+ const ddTraceEnabled = getValueFromEnvSources('DD_TRACE_ENABLED', true)
11
+ const ddTraceDisabled = ddTraceEnabled === undefined
12
+ ? getValueFromEnvSources('OTEL_TRACES_EXPORTER') === 'none'
13
+ : ddTraceEnabled === false
12
14
 
13
15
  module.exports = ddTraceDisabled || inJestWorker
14
16
  ? require('./noop/proxy')
@@ -14,6 +14,12 @@ module.exports = {
14
14
  PROPAGATED_PARENT_ID_KEY: '_dd.p.llmobs_parent_id',
15
15
  PROPAGATED_ML_APP_KEY: '_dd.p.llmobs_ml_app',
16
16
  PARENT_ID_KEY: '_ml_obs.llmobs_parent_id',
17
+ PROPAGATED_SAMPLE_RATE_KEY: '_dd.p.llmobs_sr',
18
+ PROPAGATED_SAMPLING_DECISION_KEY: '_dd.p.llmobs_sd',
19
+ SAMPLE_RATE: '_ml_obs.sample_rate',
20
+ SAMPLING_DECISION: '_ml_obs.sampling_decision',
21
+ SAMPLING_DECISION_SAMPLED: '1',
22
+ SAMPLING_DECISION_DROPPED: '0',
17
23
  TAGS: '_ml_obs.tags',
18
24
  NAME: '_ml_obs.name',
19
25
  TRACE_ID: '_ml_obs.trace_id',
@@ -9,6 +9,10 @@ const {
9
9
  ML_APP,
10
10
  PROPAGATED_ML_APP_KEY,
11
11
  PROPAGATED_PARENT_ID_KEY,
12
+ SAMPLE_RATE,
13
+ SAMPLING_DECISION,
14
+ PROPAGATED_SAMPLE_RATE_KEY,
15
+ PROPAGATED_SAMPLING_DECISION_KEY,
12
16
  } = require('./constants/tags')
13
17
  const { storage } = require('./storage')
14
18
  const telemetry = require('./telemetry')
@@ -68,7 +72,7 @@ function enable (config) {
68
72
  spanFinishCh.subscribe(handleSpanProcess)
69
73
 
70
74
  // distributed tracing for llmobs
71
- injectCh.subscribe(handleLLMObsParentIdInjection)
75
+ injectCh.subscribe(handleLLMObsInjection)
72
76
 
73
77
  setAgentStrategy(config, useAgentless => {
74
78
  if (useAgentless && !(config.apiKey && config.site)) {
@@ -92,7 +96,7 @@ function disable () {
92
96
  if (evalMetricAppendCh.hasSubscribers) evalMetricAppendCh.unsubscribe(handleEvalMetricAppend)
93
97
  if (flushCh.hasSubscribers) flushCh.unsubscribe(handleFlush)
94
98
  if (spanFinishCh.hasSubscribers) spanFinishCh.unsubscribe(handleSpanProcess)
95
- if (injectCh.hasSubscribers) injectCh.unsubscribe(handleLLMObsParentIdInjection)
99
+ if (injectCh.hasSubscribers) injectCh.unsubscribe(handleLLMObsInjection)
96
100
  if (registerUserSpanProcessorCh.hasSubscribers) registerUserSpanProcessorCh.unsubscribe(handleRegisterProcessor)
97
101
 
98
102
  spanWriter?.destroy()
@@ -106,8 +110,8 @@ function disable () {
106
110
  }
107
111
 
108
112
  // since LLMObs traces can extend between services and be the same trace,
109
- // we need to propagate the parent id and mlApp.
110
- function handleLLMObsParentIdInjection ({ carrier }) {
113
+ // we need to propagate the parent id, mlApp, and sampling rate/decision.
114
+ function handleLLMObsInjection ({ carrier }) {
111
115
  // Respect the standard propagator's gate: when trace tag propagation is
112
116
  // disabled, don't write `x-datadog-tags` for LLMObs either.
113
117
  if (globalTracerConfig.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH === 0) return
@@ -122,7 +126,12 @@ function handleLLMObsParentIdInjection ({ carrier }) {
122
126
  parentContext?._trace?.tags?.[PROPAGATED_ML_APP_KEY] ||
123
127
  globalTracerConfig.llmobs.mlApp
124
128
 
125
- if (!parentId && !mlApp) return
129
+ const sampleRate =
130
+ mlObsSpanTags?.[SAMPLE_RATE] ?? parentContext?._trace?.tags?.[PROPAGATED_SAMPLE_RATE_KEY]
131
+ const samplingDecision =
132
+ mlObsSpanTags?.[SAMPLING_DECISION] ?? parentContext?._trace?.tags?.[PROPAGATED_SAMPLING_DECISION_KEY]
133
+
134
+ if (!parentId && !mlApp && samplingDecision == null) return
126
135
 
127
136
  // `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*`
128
137
  // tags, so it may be undefined here — coalesce before appending.
@@ -130,6 +139,8 @@ function handleLLMObsParentIdInjection ({ carrier }) {
130
139
  let tags = existing || ''
131
140
  if (parentId) tags += `${tags ? ',' : ''}${PROPAGATED_PARENT_ID_KEY}=${parentId}`
132
141
  if (mlApp) tags += `${tags ? ',' : ''}${PROPAGATED_ML_APP_KEY}=${mlApp}`
142
+ if (sampleRate != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLE_RATE_KEY}=${sampleRate}`
143
+ if (samplingDecision != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLING_DECISION_KEY}=${samplingDecision}`
133
144
  if (tags !== existing) carrier['x-datadog-tags'] = tags
134
145
  }
135
146
 
@@ -114,7 +114,7 @@ function extractMetrics (response) {
114
114
  }
115
115
 
116
116
  const totalTokens = tokenUsage.totalTokenCount ||
117
- (tokenUsage.promptTokenCount || 0) + (tokenUsage.candidatesTokenCount || 0)
117
+ (tokenUsage.promptTokenCount || 0) + (tokenUsage.candidatesTokenCount || 0)
118
118
  if (totalTokens) {
119
119
  metrics.totalTokens = totalTokens
120
120
  }
@@ -458,8 +458,8 @@ function formatStreamingOutput (response) {
458
458
 
459
459
  // Skip special cases in streaming (handle them as non-streaming)
460
460
  if (content.parts.some(part => part.functionCall ||
461
- part.executableCode ||
462
- part.codeExecutionResult)) {
461
+ part.executableCode ||
462
+ part.codeExecutionResult)) {
463
463
  messages.push(...formatNonStreamingCandidate(candidate))
464
464
  continue
465
465
  }
@@ -9,7 +9,11 @@ const ANTHROPIC_PROVIDER_NAME = 'anthropic'
9
9
  const BEDROCK_PROVIDER_NAME = 'amazon_bedrock'
10
10
  const OPENAI_PROVIDER_NAME = 'openai'
11
11
 
12
- const SUPPORTED_INTEGRATIONS = new Set(['openai'])
12
+ // Providers that ship their own LLMObs integration. When one of these is also
13
+ // enabled, the LangChain model span is demoted to `workflow` so the provider
14
+ // integration emits the single `llm` span — avoiding two llm spans (and
15
+ // double-counted tokens/cost) for one underlying call.
16
+ const SUPPORTED_INTEGRATIONS = new Set(['openai', 'anthropic'])
13
17
  const LLM_SPAN_TYPES = new Set(['llm', 'chat_model', 'embedding'])
14
18
  const LLM = 'llm'
15
19
  const WORKFLOW = 'workflow'