dd-trace 5.121.0 → 5.122.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 (137) hide show
  1. package/README.md +0 -17
  2. package/index.d.ts +108 -3
  3. package/initialize.mjs +28 -17
  4. package/openfeature.js +2 -2
  5. package/package.json +11 -8
  6. package/packages/datadog-esbuild/index.js +0 -34
  7. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  9. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  10. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  11. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  12. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  13. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  14. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  15. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  16. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  17. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  18. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +9 -4
  19. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  20. package/packages/datadog-instrumentations/src/jest.js +25 -17
  21. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  22. package/packages/datadog-instrumentations/src/mocha/main.js +539 -55
  23. package/packages/datadog-instrumentations/src/mocha/utils.js +130 -17
  24. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  25. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  26. package/packages/datadog-instrumentations/src/router.js +2 -0
  27. package/packages/datadog-instrumentations/src/vitest-main.js +11 -3
  28. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  29. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  30. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -8
  31. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  32. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  33. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  34. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  35. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  36. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  37. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +81 -29
  38. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  39. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  40. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  41. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  42. package/packages/datadog-plugin-electron/src/index.js +2 -0
  43. package/packages/datadog-plugin-fs/src/index.js +1 -1
  44. package/packages/datadog-plugin-jest/src/index.js +6 -0
  45. package/packages/datadog-plugin-mocha/src/index.js +56 -1
  46. package/packages/datadog-plugin-nats/src/index.js +1 -1
  47. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  48. package/packages/datadog-plugin-openai-agents/src/integration.js +1 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  50. package/packages/datadog-plugin-vitest/src/index.js +6 -0
  51. package/packages/datadog-webpack/index.js +0 -11
  52. package/packages/dd-trace/index.js +0 -1
  53. package/packages/dd-trace/src/agent/info.js +6 -5
  54. package/packages/dd-trace/src/aiguard/client.js +100 -1
  55. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  56. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  57. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  58. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  59. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  60. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  61. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  62. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  63. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  64. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  65. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  66. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  67. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  68. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  69. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  70. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  71. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  72. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  73. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  74. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  75. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  76. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  77. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  78. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  79. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  80. package/packages/dd-trace/src/config/index.js +13 -2
  81. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  82. package/packages/dd-trace/src/config/supported-configurations.json +14 -0
  83. package/packages/dd-trace/src/datastreams/index.js +2 -0
  84. package/packages/dd-trace/src/datastreams/manager.js +9 -2
  85. package/packages/dd-trace/src/datastreams/processor.js +15 -13
  86. package/packages/dd-trace/src/datastreams/size.js +5 -0
  87. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  88. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  89. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  90. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  91. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  92. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  93. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  94. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  95. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  96. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  97. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  98. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  99. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  100. package/packages/dd-trace/src/noop/proxy.js +3 -4
  101. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  102. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  103. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  104. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  105. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  106. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  107. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  108. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  109. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  110. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  111. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  112. package/packages/dd-trace/src/plugin_manager.js +3 -3
  113. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  114. package/packages/dd-trace/src/plugins/index.js +1 -0
  115. package/packages/dd-trace/src/plugins/util/jest.js +4 -3
  116. package/packages/dd-trace/src/plugins/util/test.js +9 -9
  117. package/packages/dd-trace/src/plugins/util/web.js +3 -0
  118. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  119. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  120. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  121. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  122. package/packages/dd-trace/src/proxy.js +38 -29
  123. package/packages/dd-trace/src/ritm.js +9 -2
  124. package/packages/dd-trace/src/serverless.js +37 -0
  125. package/packages/dd-trace/src/span_format.js +1 -0
  126. package/packages/dd-trace/src/span_stats.js +30 -3
  127. package/packages/dd-trace/src/storage-channels.js +86 -0
  128. package/packages/dd-trace/src/tracer.js +9 -2
  129. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  130. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  131. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  132. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  133. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  134. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  135. package/packages/dd-trace/src/feature-registry.js +0 -29
  136. package/packages/dd-trace/src/openfeature/register.js +0 -35
  137. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -68,6 +68,8 @@ function getJestTestName (test) {
68
68
  function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRootDir) {
69
69
  const unskippableSuites = {}
70
70
  const forcedToRunSuites = {}
71
+ let hasUnskippableSuites = false
72
+ let hasForcedToRunSuites = false
71
73
 
72
74
  const skippedSuites = []
73
75
  const suitesToRun = []
@@ -87,11 +89,13 @@ function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRo
87
89
  if (isMarkedAsUnskippable(test)) {
88
90
  suitesToRun.push(test)
89
91
  unskippableSuites[relativePath] = true
92
+ hasUnskippableSuites = true
90
93
  if (fallbackRelativePath !== undefined) {
91
94
  unskippableSuites[fallbackRelativePath] = true
92
95
  }
93
96
  if (skippedSuite !== undefined) {
94
97
  forcedToRunSuites[relativePath] = true
98
+ hasForcedToRunSuites = true
95
99
  if (fallbackRelativePath !== undefined) {
96
100
  forcedToRunSuites[fallbackRelativePath] = true
97
101
  }
@@ -105,9 +109,6 @@ function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRo
105
109
  }
106
110
  }
107
111
 
108
- const hasUnskippableSuites = Object.keys(unskippableSuites).length > 0
109
- const hasForcedToRunSuites = Object.keys(forcedToRunSuites).length > 0
110
-
111
112
  if (originalTests.length) {
112
113
  // The config object is shared by all tests, so we can just take the first one
113
114
  const [test] = originalTests
@@ -851,7 +851,7 @@ function getTestTypeFromFramework (testFramework) {
851
851
  */
852
852
  function finishAllTraceSpans (span) {
853
853
  for (const traceSpan of span.context()._trace.started) {
854
- if (traceSpan !== span) {
854
+ if (traceSpan !== span && traceSpan._duration === undefined) {
855
855
  traceSpan.finish()
856
856
  }
857
857
  }
@@ -1439,10 +1439,6 @@ function addSkippedCoverageToMap (skippedCoverage, targetMap) {
1439
1439
  }
1440
1440
  }
1441
1441
 
1442
- function hasSkippedCoverage (skippedCoverage) {
1443
- return skippedCoverage && typeof skippedCoverage === 'object' && Object.keys(skippedCoverage).length > 0
1444
- }
1445
-
1446
1442
  function getTestCoverageLinesPercentage (coverage, skippedCoverage, rootDir) {
1447
1443
  const executableLinesByFile = new Map()
1448
1444
  const coveredLinesByFile = new Map()
@@ -1496,10 +1492,10 @@ function applySkippedCoverageToFileCoverage (fileCoverage, skippedBitmap) {
1496
1492
  * @returns {boolean}
1497
1493
  */
1498
1494
  function applySkippedCoverageToCoverage (coverage, skippedCoverage, rootDir) {
1499
- if (!hasSkippedCoverage(skippedCoverage)) return false
1495
+ const skippedCoverageByFilename = getSkippedCoverageByFilename(skippedCoverage)
1496
+ if (skippedCoverageByFilename.size === 0) return false
1500
1497
 
1501
1498
  const coverageMap = getCoverageMap(coverage)
1502
- const skippedCoverageByFilename = getSkippedCoverageByFilename(skippedCoverage)
1503
1499
  let matched = false
1504
1500
 
1505
1501
  for (const filename of coverageMap.files()) {
@@ -1820,6 +1816,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) {
1820
1816
  }
1821
1817
 
1822
1818
  const metrics = {}
1819
+ let hasMetrics = false
1823
1820
  for (const candidate of candidateBranches) {
1824
1821
  // Find common ancestor
1825
1822
  const baseSha = getMergeBase(candidate, sourceBranch)
@@ -1838,6 +1835,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) {
1838
1835
  ahead,
1839
1836
  baseSha,
1840
1837
  }
1838
+ hasMetrics = true
1841
1839
  }
1842
1840
 
1843
1841
  function isDefaultBranch (branch) {
@@ -1846,7 +1844,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) {
1846
1844
  )
1847
1845
  }
1848
1846
 
1849
- if (Object.keys(metrics).length === 0) {
1847
+ if (!hasMetrics) {
1850
1848
  return null
1851
1849
  }
1852
1850
  // Find branch with smallest "ahead" value, preferring default branch on tie
@@ -1872,6 +1870,7 @@ function getPullRequestDiff (baseCommit, targetCommit) {
1872
1870
  function getModifiedFilesFromDiff (diff) {
1873
1871
  if (!diff) return null
1874
1872
  const result = {}
1873
+ let hasModifiedFiles = false
1875
1874
 
1876
1875
  const filesRegex = /^diff --git a\/(?<file>.+) b\/(?<file2>.+)$/g
1877
1876
  const linesRegex = /^@@ -\d+(,\d+)? \+(?<start>\d+)(,(?<count>\d+))? @@/g
@@ -1886,6 +1885,7 @@ function getModifiedFilesFromDiff (diff) {
1886
1885
  if (fileMatch && fileMatch.groups.file) {
1887
1886
  currentFile = fileMatch.groups.file
1888
1887
  result[currentFile] = []
1888
+ hasModifiedFiles = true
1889
1889
  continue
1890
1890
  }
1891
1891
 
@@ -1904,7 +1904,7 @@ function getModifiedFilesFromDiff (diff) {
1904
1904
  linesRegex.lastIndex = 0
1905
1905
  }
1906
1906
 
1907
- if (Object.keys(result).length === 0) {
1907
+ if (!hasModifiedFiles) {
1908
1908
  return null
1909
1909
  }
1910
1910
  return result
@@ -376,6 +376,9 @@ function addAllowHeaders (req, res, headers) {
376
376
  const allowHeaders = splitHeader(headers['access-control-allow-headers'])
377
377
  const requestHeaders = splitHeader(req.headers['access-control-request-headers'])
378
378
  const contextHeaders = [
379
+ 'baggage',
380
+ 'traceparent',
381
+ 'tracestate',
379
382
  'x-datadog-origin',
380
383
  'x-datadog-parent-id',
381
384
  'x-datadog-sampled', // Deprecated, but still accept it in case it's sent.
@@ -35,7 +35,7 @@ function profileHasMissingSourceMaps (profile) {
35
35
  }
36
36
 
37
37
  function processInfo (infos, info, type) {
38
- if (Object.keys(info).length > 0) {
38
+ if (info !== undefined) {
39
39
  infos[type] = info
40
40
  }
41
41
  }
@@ -66,9 +66,7 @@ class NativeSpaceProfiler {
66
66
  return profile
67
67
  }
68
68
 
69
- getInfo () {
70
- return {}
71
- }
69
+ getInfo () {}
72
70
 
73
71
  encode (profile) {
74
72
  return encodeProfileAsync(profile)
@@ -1,13 +1,18 @@
1
1
  'use strict'
2
2
 
3
- const dc = require('dc-polyfill')
4
-
5
- const { storage } = require('../../../../datadog-core')
6
3
  const log = require('../../log')
7
4
  const runtimeMetrics = require('../../runtime_metrics')
8
5
  const telemetryMetrics = require('../../telemetry/metrics')
9
- const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils')
6
+ const { endpointNameFromTags, finalEndpoint, getStartedSpans } = require('../webspan-utils')
10
7
  const { SAMPLING_INTERVAL } = require('../constants')
8
+ const {
9
+ enterCh,
10
+ beforeCh,
11
+ spanFinishCh,
12
+ getActiveSpan,
13
+ ensureChannelsActivated,
14
+ } = require('../../storage-channels')
15
+ const webTagsCache = require('../../web-tags-cache')
11
16
 
12
17
  const {
13
18
  END_TIMESTAMP_LABEL,
@@ -21,21 +26,12 @@ const TRACE_ENDPOINT_LABEL = 'trace endpoint'
21
26
 
22
27
  /** @typedef {import('../../config/config-base')} TracerConfig */
23
28
 
24
- let beforeCh
25
- const enterCh = dc.channel('dd-trace:storage:enter')
26
- const spanFinishCh = dc.channel('dd-trace:span:finish')
27
- const tagsUpdateCh = dc.channel('dd-trace:span:tags:update')
28
29
  const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
29
30
 
30
31
  const ProfilingContext = Symbol('NativeWallProfiler.ProfilingContext')
31
32
 
32
33
  let kSampleCount
33
34
 
34
- function getActiveSpan () {
35
- const store = storage('legacy').getStore()
36
- return store && store.span
37
- }
38
-
39
35
  function toBigInt (spanId) {
40
36
  return spanId !== null && typeof spanId === 'object' ? spanId.toBigInt() : spanId
41
37
  }
@@ -53,58 +49,13 @@ function updateContext (context) {
53
49
  }
54
50
  if (context.webTags !== undefined && context.endpoint === undefined) {
55
51
  // endpoint may not be determined yet, but keep it as fallback
56
- // if tags are not available anymore during serialization
57
- context.endpoint = endpointNameFromTags(context.webTags)
58
- }
59
- }
60
-
61
- let channelsActivated = false
62
- function ensureChannelsActivated (asyncContextFrameEnabled) {
63
- if (channelsActivated) return
64
-
65
- const shimmer = require('../../../../datadog-shimmer')
66
-
67
- // We need to instrument enterWith() on the legacy storage — that's the storage
68
- // carrying span data and the only one the profiler cares about.
69
- const legacyStorage = storage('legacy')
70
- let inRun = false
71
- shimmer.wrap(legacyStorage, 'enterWith', function (original) {
72
- return function (store) {
73
- const retVal = original.call(this, store)
74
- if (!inRun) enterCh.publish()
75
- return retVal
76
- }
77
- })
78
-
79
- // When not using AsyncContextFrame, we need additional instrumentation.
80
- if (!asyncContextFrameEnabled) {
81
- // We need async_hooks.createHook to create a "before" callback.
82
- const { createHook } = require('async_hooks')
83
- beforeCh = dc.channel('dd-trace:storage:before')
84
- createHook({ before: () => beforeCh.publish() }).enable()
85
-
86
- // In ACF-based implementation run() delegates to enterWith() so it doesn't
87
- // need to be separately instrumented. in non-ACF implementation run()
88
- // doesn't delegate to enterWith(), so separate instrumentation is necessary.
89
- shimmer.wrap(legacyStorage, 'run', function (original) {
90
- return function (store, callback, ...args) {
91
- const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
92
- inRun = false
93
- enterCh.publish()
94
- const retVal = cb.apply(this, args)
95
- inRun = true
96
- return retVal
97
- })
98
- inRun = true
99
- const retVal = original.call(this, store, wrappedCb, ...args)
100
- enterCh.publish()
101
- inRun = false
102
- return retVal
103
- }
104
- })
52
+ // if tags are not available anymore during serialization. Only snapshot a
53
+ // settled value: routing tags land over the course of the request, and this
54
+ // field is never refreshed once set, so capturing an interim placeholder
55
+ // (a bare `GET`) would pin the fallback to it. Staying undefined until then
56
+ // just means the next sample rotation tries again.
57
+ context.endpoint = finalEndpoint(context.webTags)
105
58
  }
106
-
107
- channelsActivated = true
108
59
  }
109
60
 
110
61
  class NativeWallProfiler {
@@ -216,7 +167,13 @@ class NativeWallProfiler {
216
167
  enterCh.subscribe(this.#boundEnter)
217
168
  spanFinishCh.subscribe(this.#boundSpanFinished)
218
169
  if (this.#endpointCollectionEnabled) {
219
- tagsUpdateCh.subscribe(this.#boundSpanTagsUpdated)
170
+ // Web-tags cache publishes once per span at the moment its
171
+ // walk-result transitions from undefined to a real value —
172
+ // exactly when we need to refresh the ProfilingContext snapshot.
173
+ webTagsCache.resolvedCh.subscribe(this.#boundSpanTagsUpdated)
174
+ // Turn on the cache's own tagsUpdate subscription — it's
175
+ // ref-counted, so this composes with any other active consumer.
176
+ webTagsCache.activate()
220
177
  }
221
178
  }
222
179
  }
@@ -299,32 +256,20 @@ class NativeWallProfiler {
299
256
  let profilingContext = span[ProfilingContext]
300
257
  if (profilingContext === undefined) {
301
258
  const context = span.context()
302
- const startedSpans = getStartedSpans(context)
303
259
 
304
260
  let spanId
305
261
  let rootSpanId
306
262
  if (this.#codeHotspotsEnabled) {
263
+ const startedSpans = getStartedSpans(context)
307
264
  spanId = context._spanId
308
265
  rootSpanId = startedSpans.length ? startedSpans[0].context()._spanId : context._spanId
309
266
  }
310
267
 
311
- let webTags
312
- if (this.#endpointCollectionEnabled) {
313
- const tags = context.getTags()
314
- if (isWebServerSpan(tags)) {
315
- webTags = tags
316
- } else {
317
- // Get parent's context's web tags
318
- const parentId = context._parentId
319
- for (let i = startedSpans.length; --i >= 0;) {
320
- const ispan = startedSpans[i]
321
- if (ispan.context()._spanId === parentId) {
322
- webTags = this.#getProfilingContext(ispan).webTags
323
- break
324
- }
325
- }
326
- }
327
- }
268
+ // webTags is snapshotted into the sample context at getProfilingContext
269
+ // time; if the answer turns out to be undefined and the span later gets
270
+ // web-server tags, #spanTagsUpdated refreshes this field via the shared
271
+ // cache (see web-tags-cache.js).
272
+ const webTags = this.#endpointCollectionEnabled ? webTagsCache.getCachedWebTags(span) : undefined
328
273
 
329
274
  profilingContext = { spanId, rootSpanId, webTags }
330
275
  span[ProfilingContext] = profilingContext
@@ -345,14 +290,15 @@ class NativeWallProfiler {
345
290
  }
346
291
  }
347
292
 
293
+ // Invoked (via webTagsCache.resolvedCh) once per span at the moment the
294
+ // shared cache promotes a previously-undefined webTags answer into a
295
+ // real value. Refresh the ProfilingContext snapshot so future samples
296
+ // pick it up.
348
297
  #spanTagsUpdated (span) {
349
298
  if (!this.#started) return
350
299
  const profilingContext = span[ProfilingContext]
351
- if (profilingContext === undefined || profilingContext.webTags !== undefined) return
352
- const tags = span.context().getTags()
353
- if (isWebServerSpan(tags)) {
354
- profilingContext.webTags = tags
355
- }
300
+ if (profilingContext === undefined) return
301
+ profilingContext.webTags = webTagsCache.getCachedWebTags(span)
356
302
  }
357
303
 
358
304
  #reportV8bug (maybeBug) {
@@ -401,7 +347,8 @@ class NativeWallProfiler {
401
347
  enterCh.unsubscribe(this.#boundEnter)
402
348
  spanFinishCh.unsubscribe(this.#boundSpanFinished)
403
349
  if (this.#endpointCollectionEnabled) {
404
- tagsUpdateCh.unsubscribe(this.#boundSpanTagsUpdated)
350
+ webTagsCache.resolvedCh.unsubscribe(this.#boundSpanTagsUpdated)
351
+ webTagsCache.deactivate()
405
352
  }
406
353
  this.#profilerState = undefined
407
354
  }
@@ -470,7 +417,7 @@ class NativeWallProfiler {
470
417
  if (rootSpanId !== undefined) {
471
418
  labels[LOCAL_ROOT_SPAN_ID_LABEL] = toBigInt(rootSpanId)
472
419
  }
473
- if (webTags !== undefined && Object.keys(webTags).length !== 0) {
420
+ if (webTags !== undefined) {
474
421
  labels[TRACE_ENDPOINT_LABEL] = endpointNameFromTags(webTags)
475
422
  } else if (endpoint) {
476
423
  // fallback to endpoint computed when sample was taken
@@ -14,6 +14,50 @@ function endpointNameFromTags (tags) {
14
14
  ].filter(Boolean).join(' ')
15
15
  }
16
16
 
17
+ // The endpoint name a web-server span's tag bag yields changes over the span's
18
+ // lifetime: plugins set `http.method` when the request arrives and only add
19
+ // `http.route` (and often `resource.name`) once framework routing has resolved
20
+ // the URL. Returns the endpoint name once its value looks stable, or undefined
21
+ // while it is still an interim placeholder.
22
+ //
23
+ // Consumers that read the tag bag lazily (the wall profiler resolves endpoint
24
+ // labels at serialization time) see the settled value for free and only need
25
+ // this for fallbacks captured mid-request. Consumers that have to materialize
26
+ // the value early — the OTEP-4947 thread-context writer writes it into a record
27
+ // an out-of-process reader samples at any moment — need it to decide when
28
+ // publishing is safe.
29
+ //
30
+ // The test is on the computed value rather than on which tags are present,
31
+ // because endpointNameFromTags prefers `resource.name` over
32
+ // `http.method` + `http.route` and some plugins seed `resource.name` with the
33
+ // bare request method before routing resolves: datadog-plugin-next starts its
34
+ // request span with `resource.name: req.method` and only replaces it with
35
+ // `${req.method} ${page}` once the page is known. A value equal to the bare
36
+ // method is therefore a placeholder no matter which tags produced it.
37
+ function finalEndpoint (tags) {
38
+ if (tags == null) return
39
+ // Presence gate first: nothing but these two can contribute routing
40
+ // information, and checking them keeps the common not-yet-routed case free of
41
+ // the string building inside endpointNameFromTags.
42
+ if (tags[RESOURCE_NAME] == null && tags[HTTP_ROUTE] == null) return
43
+ const endpoint = endpointNameFromTags(tags)
44
+ if (!endpoint || endpoint === tags[HTTP_METHOD]) return
45
+ return endpoint
46
+ }
47
+
48
+ // The trace's started-spans list, whose first entry is the local root span by
49
+ // repo-wide convention — priority_sampler, span_format, the wall profiler's
50
+ // local-root-span-id label, the event plugins and the OTEP-4947 writer all read
51
+ // it that way, and web-tags-cache walks it to find a span's parent.
52
+ //
53
+ // Partial flush weakens that convention for every one of them alike:
54
+ // span_processor's _erase() replaces the list with the still-active spans only,
55
+ // so once a trace crosses DD_TRACE_PARTIAL_FLUSH_MIN_SPANS a local root that has
56
+ // already finished drops out, and the first entry becomes merely the oldest span
57
+ // still running — while a finished web-server ancestor stops being reachable for
58
+ // the parent-chain walk. Correcting it needs a local-root reference the tracer
59
+ // core keeps across flushes; until there is one, consumers share the same skew
60
+ // on large traces rather than each compensating differently.
17
61
  function getStartedSpans (context) {
18
62
  return context._trace.started
19
63
  }
@@ -21,5 +65,6 @@ function getStartedSpans (context) {
21
65
  module.exports = {
22
66
  isWebServerSpan,
23
67
  endpointNameFromTags,
68
+ finalEndpoint,
24
69
  getStartedSpans,
25
70
  }
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const NoopProxy = require('./noop/proxy')
4
- const { features } = require('./feature-registry')
5
4
  const DatadogTracer = require('./tracer')
6
5
  const getConfig = require('./config')
7
6
  const { getEnvironmentVariable } = require('./config/helper')
@@ -38,9 +37,9 @@ const OFFLINE_VALIDATION_EXPORTERS = new Set([
38
37
  'playwright_worker',
39
38
  'vitest_worker',
40
39
  ])
41
- const FEATURE_STATE_NOOP = 0
42
- const FEATURE_STATE_LAZY = 1
43
- const FEATURE_STATE_ACTIVE = 2
40
+ const OPENFEATURE_STATE_NOOP = 0
41
+ const OPENFEATURE_STATE_LAZY = 1
42
+ const OPENFEATURE_STATE_ACTIVE = 2
44
43
 
45
44
  class LazyModule {
46
45
  constructor (provider) {
@@ -91,8 +90,7 @@ function defineLazily (obj, property, getClass, ...args) {
91
90
  }
92
91
 
93
92
  class Tracer extends NoopProxy {
94
- /** @type {Record<string, number> | undefined} */
95
- #featureStates
93
+ #openfeatureState = OPENFEATURE_STATE_NOOP
96
94
 
97
95
  constructor () {
98
96
  super()
@@ -115,12 +113,9 @@ class Tracer extends NoopProxy {
115
113
  aiguard: new LazyModule(() => require('./aiguard')),
116
114
  iast: new LazyModule(() => require('./appsec/iast')),
117
115
  llmobs: new LazyModule(() => require('./llmobs')),
116
+ openfeature: new LazyModule(() => require('./openfeature')),
118
117
  rewriter: new LazyModule(() => require('./appsec/iast/taint-tracking/rewriter')),
119
118
  }
120
-
121
- for (const feature of Object.values(features)) {
122
- this._modules[feature.name] = new LazyModule(feature.factory)
123
- }
124
119
  }
125
120
 
126
121
  /**
@@ -213,9 +208,10 @@ class Tracer extends NoopProxy {
213
208
  DynamicInstrumentation.start(config, rc)
214
209
  }
215
210
 
216
- for (const feature of Object.values(features)) {
217
- feature.remoteConfig?.(rc, config, this)
218
- }
211
+ const openfeatureRemoteConfig = require('./openfeature/remote_config')
212
+ const subscribeOpenfeatureToRemoteConfig = config.featureFlags.DD_FEATURE_FLAGS_ENABLED &&
213
+ config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE === 'remote_config'
214
+ openfeatureRemoteConfig.enable(rc, () => this.openfeature, subscribeOpenfeatureToRemoteConfig)
219
215
  }
220
216
 
221
217
  if (config.profiling.DD_PROFILING_ENABLED === 'true') {
@@ -233,6 +229,17 @@ class Tracer extends NoopProxy {
233
229
  }
234
230
  }
235
231
 
232
+ // Experimental: mirror the active trace ID, span ID and endpoint into
233
+ // an OTEP-4947 thread-local context record an out-of-process eBPF
234
+ // reader can consume.
235
+ if (config.DD_TRACE_OTEL_CTX_ENABLED) {
236
+ try {
237
+ require('./otel-thread-ctx').start()
238
+ } catch (e) {
239
+ log.error('Error starting OTEP-4947 thread context writer', e)
240
+ }
241
+ }
242
+
236
243
  // OTel logs/metrics pipelines must be initialized BEFORE runtimeMetrics.start so that
237
244
  // when the OTLP runtime metrics module calls metrics.getMeterProvider(), it gets the
238
245
  // real provider, otherwise instruments register on the noop provider and never export.
@@ -305,28 +312,32 @@ class Tracer extends NoopProxy {
305
312
  }
306
313
 
307
314
  /**
308
- * @param {(typeof features)[string]} feature
315
+ * `tracer.openfeature` is only reachable through this proxy. SSI itself (auto-injecting
316
+ * the tracer, resolving the vendored provider regardless of the customer's own
317
+ * `node_modules` layout) is unaffected -- that is what this PR fixes. The remaining gap is
318
+ * narrower: there is no `dd-trace-api` handoff for `openfeature` yet (see
319
+ * `packages/datadog-plugin-dd-trace-api`), so an application that calls into the tracer
320
+ * exclusively through the `dd-trace-api` shim -- instead of `require('dd-trace')` -- cannot
321
+ * reach the flagging provider through that shim.
322
+ *
309
323
  * @param {import('./config/config-base')} config
310
324
  */
311
- #enableFeature (feature, config) {
312
- const states = this.#featureStates ??= {}
313
- const state = states[feature.name] ?? FEATURE_STATE_NOOP
325
+ #enableOpenfeature (config) {
326
+ if (this.#openfeatureState !== OPENFEATURE_STATE_NOOP) return
327
+ this.#openfeatureState = OPENFEATURE_STATE_LAZY
314
328
 
315
- if (state === FEATURE_STATE_ACTIVE || state === FEATURE_STATE_LAZY) return
316
- states[feature.name] = FEATURE_STATE_LAZY
317
-
318
- Reflect.defineProperty(this, feature.name, {
329
+ Reflect.defineProperty(this, 'openfeature', {
319
330
  get: () => {
320
- const Provider = feature.provider()
321
- const provider = new Provider(this._tracer, config)
331
+ const FlaggingProvider = require('./openfeature/flagging_provider')
332
+ const provider = new FlaggingProvider(this._tracer, config)
322
333
 
323
- this._modules[feature.name].enable(config)
324
- Reflect.defineProperty(this, feature.name, {
334
+ this._modules.openfeature.enable(config)
335
+ Reflect.defineProperty(this, 'openfeature', {
325
336
  value: provider,
326
337
  configurable: true,
327
338
  enumerable: true,
328
339
  })
329
- states[feature.name] = FEATURE_STATE_ACTIVE
340
+ this.#openfeatureState = OPENFEATURE_STATE_ACTIVE
330
341
  return provider
331
342
  },
332
343
  configurable: true,
@@ -373,9 +384,7 @@ class Tracer extends NoopProxy {
373
384
  this._modules.llmobs.disable()
374
385
  }
375
386
 
376
- for (const feature of Object.values(features)) {
377
- if (feature.isEnabled(config)) this.#enableFeature(feature, config)
378
- }
387
+ if (config.featureFlags.DD_FEATURE_FLAGS_ENABLED) this.#enableOpenfeature(config)
379
388
 
380
389
  if (this._tracingInitialized) {
381
390
  this._tracer.configure(config)
@@ -20,6 +20,7 @@ const origRequire = Module.prototype.require
20
20
  module.exports = Hook
21
21
 
22
22
  let moduleHooks = Object.create(null)
23
+ let hookedModuleCount = 0
23
24
  let cache = Object.create(null)
24
25
  let patching = Object.create(null)
25
26
  let patchedRequire = null
@@ -64,6 +65,7 @@ function Hook (modules, options, onrequire) {
64
65
  hooks.push(onrequire)
65
66
  } else {
66
67
  moduleHooks[mod] = [onrequire]
68
+ hookedModuleCount++
67
69
  }
68
70
  }
69
71
  }
@@ -205,6 +207,7 @@ Hook.reset = function () {
205
207
  patching = Object.create(null)
206
208
  cache = Object.create(null)
207
209
  moduleHooks = Object.create(null)
210
+ hookedModuleCount = 0
208
211
  }
209
212
 
210
213
  function findProjectRoot (startDir) {
@@ -221,16 +224,20 @@ function findProjectRoot (startDir) {
221
224
 
222
225
  Hook.prototype.unhook = function () {
223
226
  for (const mod of this.modules) {
224
- const hooks = (moduleHooks[mod] || []).filter(hook => hook !== this.onrequire)
227
+ const registeredHooks = moduleHooks[mod]
228
+ if (registeredHooks === undefined) continue
229
+
230
+ const hooks = registeredHooks.filter(hook => hook !== this.onrequire)
225
231
 
226
232
  if (hooks.length > 0) {
227
233
  moduleHooks[mod] = hooks
228
234
  } else {
229
235
  delete moduleHooks[mod]
236
+ hookedModuleCount--
230
237
  }
231
238
  }
232
239
 
233
- if (Object.keys(moduleHooks).length === 0) {
240
+ if (hookedModuleCount === 0) {
234
241
  Hook.reset()
235
242
  }
236
243
  }
@@ -42,7 +42,44 @@ function isInServerlessEnvironment () {
42
42
  return inAWSLambda || isGCPFunction || isAzureFunction
43
43
  }
44
44
 
45
+ /**
46
+ * Gets tags describing the serverless platform where the tracer is running.
47
+ *
48
+ * @returns {string[]|undefined}
49
+ */
50
+ function getServerlessPlatformTags () {
51
+ if (getEnvironmentVariable('VERCEL') === '1') {
52
+ return getVercelPlatformTags()
53
+ }
54
+ }
55
+
56
+ /**
57
+ * @returns {string[]|undefined}
58
+ */
59
+ function getVercelPlatformTags () {
60
+ let tags
61
+ const projectId = getEnvironmentVariable('VERCEL_PROJECT_ID')
62
+ if (projectId) {
63
+ tags = ['vercel.project_id', projectId]
64
+ }
65
+
66
+ const environment = getEnvironmentVariable('VERCEL_ENV')
67
+ if (environment) {
68
+ tags ??= []
69
+ tags.push('vercel.environment', environment)
70
+ }
71
+
72
+ const region = getEnvironmentVariable('VERCEL_REGION')
73
+ if (region) {
74
+ tags ??= []
75
+ tags.push('vercel.region', region)
76
+ }
77
+
78
+ return tags
79
+ }
80
+
45
81
  module.exports = {
82
+ getServerlessPlatformTags,
46
83
  getIsGCPFunction,
47
84
  getIsAzureFunction,
48
85
  enableGCPPubSubPushSubscription,
@@ -450,3 +450,4 @@ function isUrl (obj) {
450
450
  }
451
451
 
452
452
  module.exports = format
453
+ module.exports.addError = extractError