dd-trace 6.9.0 → 6.11.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 (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -8,17 +8,18 @@ const { getLageTestSessionName } = require('../../ci-visibility/lage')
8
8
  const log = require('../../log')
9
9
  const { getEnvironmentVariable, getValueFromEnvSources } = require('../../config/helper')
10
10
  const { getSegment } = require('../../util')
11
+ const { parse } = require('../../../../../vendor/dist/jest-docblock')
11
12
  const satisfies = require('../../../../../vendor/dist/semifies')
12
13
 
13
14
  const istanbul = require('../../../../../vendor/dist/istanbul-lib-coverage')
14
15
 
16
+ const { writeDatadogParentId, writeDatadogTraceId } = require('../../carrier')
15
17
  const id = require('../../id')
16
18
  const {
17
19
  incrementCountMetric,
18
20
  TELEMETRY_GIT_COMMIT_SHA_DISCREPANCY,
19
21
  TELEMETRY_GIT_SHA_MATCH,
20
22
  } = require('../../ci-visibility/telemetry')
21
-
22
23
  const { SPAN_TYPE, RESOURCE_NAME, SAMPLING_PRIORITY } = require('../../../../../ext/tags')
23
24
  const { SAMPLING_RULE_DECISION } = require('../../constants')
24
25
  const { AUTO_KEEP } = require('../../../../../ext/priority')
@@ -195,6 +196,8 @@ const VITEST_WORKER_TRACE_PAYLOAD_CODE = 100
195
196
  const VITEST_WORKER_COVERAGE_PAYLOAD_CODE = 101
196
197
  const VITEST_WORKER_LOGS_PAYLOAD_CODE = 102
197
198
  const VITEST_WORKER_TELEMETRY_PAYLOAD_CODE = 103
199
+ const VITEST_WORKER_EFD_SUITE_ADMISSION_REQUEST_CODE = 104
200
+ const VITEST_WORKER_EFD_SUITE_ADMISSION_RESPONSE_CODE = 105
198
201
 
199
202
  const TEST_IS_TEST_FRAMEWORK_WORKER = 'test.is_test_framework_worker'
200
203
 
@@ -489,6 +492,8 @@ module.exports = {
489
492
  VITEST_WORKER_COVERAGE_PAYLOAD_CODE,
490
493
  VITEST_WORKER_LOGS_PAYLOAD_CODE,
491
494
  VITEST_WORKER_TELEMETRY_PAYLOAD_CODE,
495
+ VITEST_WORKER_EFD_SUITE_ADMISSION_REQUEST_CODE,
496
+ VITEST_WORKER_EFD_SUITE_ADMISSION_RESPONSE_CODE,
492
497
  TEST_IS_TEST_FRAMEWORK_WORKER,
493
498
  TEST_SOURCE_START,
494
499
  TEST_SKIPPED_BY_ITR,
@@ -504,6 +509,7 @@ module.exports = {
504
509
  getTestParametersString,
505
510
  finishAllTraceSpans,
506
511
  getTestParentSpan,
512
+ isMarkedAsUnskippable,
507
513
  getTestSuitePath,
508
514
  getCodeOwnersFileEntries,
509
515
  getCodeOwnersForFilename,
@@ -544,6 +550,7 @@ module.exports = {
544
550
  removeInvalidMetadata,
545
551
  parseAnnotations,
546
552
  getIsFaultyEarlyFlakeDetection,
553
+ getFailedTestReplayPromise,
547
554
  TEST_BROWSER_DRIVER,
548
555
  TEST_BROWSER_DRIVER_VERSION,
549
556
  TEST_BROWSER_NAME,
@@ -844,7 +851,7 @@ function getTestTypeFromFramework (testFramework) {
844
851
  */
845
852
  function finishAllTraceSpans (span) {
846
853
  for (const traceSpan of span.context()._trace.started) {
847
- if (traceSpan !== span) {
854
+ if (traceSpan !== span && traceSpan._duration === undefined) {
848
855
  traceSpan.finish()
849
856
  }
850
857
  }
@@ -901,10 +908,10 @@ function setRumTestCorrelation (context, activeSpan) {
901
908
  * @returns {import('../../opentracing/span_context')}
902
909
  */
903
910
  function getTestParentSpan (tracer, testExecutionId) {
904
- return tracer.extract('text_map', {
905
- 'x-datadog-trace-id': testExecutionId || id().toString(10),
906
- 'x-datadog-parent-id': '0000000000000000',
907
- })
911
+ const carrier = /** @type {Record<string, unknown>} */ ({})
912
+ writeDatadogTraceId(carrier, testExecutionId || id().toString(10))
913
+ writeDatadogParentId(carrier, '0000000000000000')
914
+ return tracer.extract('text_map', carrier)
908
915
  }
909
916
 
910
917
  function getTestCommonTags (name, suite, version, testFramework) {
@@ -941,6 +948,62 @@ function getTestSuitePath (testSuiteAbsolutePath, sourceRoot) {
941
948
  return testSuitePath.replaceAll(path.sep, '/')
942
949
  }
943
950
 
951
+ const globalDocblockRegExp = /^\s*(\/\*\*?(.|\r?\n)*?\*\/)/
952
+ const MAX_COMMENTS_CHECKED = 10
953
+
954
+ function isMarkedAsUnskippable (test) {
955
+ let testSource
956
+
957
+ try {
958
+ testSource = fs.readFileSync(test.path, 'utf8')
959
+ } catch {
960
+ return false
961
+ }
962
+
963
+ const re = globalDocblockRegExp
964
+ re.lastIndex = 0
965
+ let commentsChecked = 0
966
+
967
+ while (testSource.length) {
968
+ const match = re.exec(testSource)
969
+ if (!match) break
970
+ const comment = match[1]
971
+
972
+ let docblocks
973
+ try {
974
+ docblocks = parse(comment)
975
+ } catch {
976
+ // Skip unparsable comment and continue scanning
977
+ if (commentsChecked++ >= MAX_COMMENTS_CHECKED) {
978
+ return false
979
+ }
980
+ continue
981
+ }
982
+
983
+ if (docblocks?.datadog) {
984
+ try {
985
+ // @ts-expect-error The datadog type is defined by us and may only be a string.
986
+ return JSON.parse(docblocks.datadog).unskippable
987
+ } catch {
988
+ // If the @datadog block comment is present but malformed, we'll run the suite
989
+ log.warn('@datadog block comment is malformed.')
990
+ return true
991
+ }
992
+ }
993
+
994
+ if (commentsChecked++ >= MAX_COMMENTS_CHECKED) {
995
+ return false
996
+ }
997
+
998
+ // To stop as soon as no doc blocks are found, slice the source. That way the
999
+ // regexp works by using the `^` anchor. Without it, it would continue
1000
+ // scanning the rest of the file.
1001
+ testSource = testSource.slice(match[0].length)
1002
+ }
1003
+
1004
+ return false
1005
+ }
1006
+
944
1007
  const POSSIBLE_CODEOWNERS_LOCATIONS = [
945
1008
  'CODEOWNERS',
946
1009
  '.github/CODEOWNERS',
@@ -1376,10 +1439,6 @@ function addSkippedCoverageToMap (skippedCoverage, targetMap) {
1376
1439
  }
1377
1440
  }
1378
1441
 
1379
- function hasSkippedCoverage (skippedCoverage) {
1380
- return skippedCoverage && typeof skippedCoverage === 'object' && Object.keys(skippedCoverage).length > 0
1381
- }
1382
-
1383
1442
  function getTestCoverageLinesPercentage (coverage, skippedCoverage, rootDir) {
1384
1443
  const executableLinesByFile = new Map()
1385
1444
  const coveredLinesByFile = new Map()
@@ -1433,10 +1492,10 @@ function applySkippedCoverageToFileCoverage (fileCoverage, skippedBitmap) {
1433
1492
  * @returns {boolean}
1434
1493
  */
1435
1494
  function applySkippedCoverageToCoverage (coverage, skippedCoverage, rootDir) {
1436
- if (!hasSkippedCoverage(skippedCoverage)) return false
1495
+ const skippedCoverageByFilename = getSkippedCoverageByFilename(skippedCoverage)
1496
+ if (skippedCoverageByFilename.size === 0) return false
1437
1497
 
1438
1498
  const coverageMap = getCoverageMap(coverage)
1439
- const skippedCoverageByFilename = getSkippedCoverageByFilename(skippedCoverage)
1440
1499
  let matched = false
1441
1500
 
1442
1501
  for (const filename of coverageMap.files()) {
@@ -1542,6 +1601,22 @@ function parseAnnotations (annotations) {
1542
1601
  }, {})
1543
1602
  }
1544
1603
 
1604
+ /**
1605
+ * Combines the optional Failed Test Replay operations that must settle before a retry.
1606
+ *
1607
+ * @param {{setProbePromise?: Promise<void>, finishTestPromise?: Promise<void>}} promises
1608
+ * @returns {Promise<void>|undefined}
1609
+ */
1610
+ function getFailedTestReplayPromise (promises) {
1611
+ if (promises.setProbePromise && promises.finishTestPromise) {
1612
+ return Promise.all([
1613
+ promises.setProbePromise,
1614
+ promises.finishTestPromise,
1615
+ ]).then(() => {})
1616
+ }
1617
+ return promises.setProbePromise || promises.finishTestPromise
1618
+ }
1619
+
1545
1620
  function getIsFaultyEarlyFlakeDetection (projectSuites, testsBySuiteName, faultyThresholdPercentage) {
1546
1621
  let newSuites = 0
1547
1622
  for (const suite of projectSuites) {
@@ -1741,6 +1816,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) {
1741
1816
  }
1742
1817
 
1743
1818
  const metrics = {}
1819
+ let hasMetrics = false
1744
1820
  for (const candidate of candidateBranches) {
1745
1821
  // Find common ancestor
1746
1822
  const baseSha = getMergeBase(candidate, sourceBranch)
@@ -1759,6 +1835,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) {
1759
1835
  ahead,
1760
1836
  baseSha,
1761
1837
  }
1838
+ hasMetrics = true
1762
1839
  }
1763
1840
 
1764
1841
  function isDefaultBranch (branch) {
@@ -1767,7 +1844,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) {
1767
1844
  )
1768
1845
  }
1769
1846
 
1770
- if (Object.keys(metrics).length === 0) {
1847
+ if (!hasMetrics) {
1771
1848
  return null
1772
1849
  }
1773
1850
  // Find branch with smallest "ahead" value, preferring default branch on tie
@@ -1793,6 +1870,7 @@ function getPullRequestDiff (baseCommit, targetCommit) {
1793
1870
  function getModifiedFilesFromDiff (diff) {
1794
1871
  if (!diff) return null
1795
1872
  const result = {}
1873
+ let hasModifiedFiles = false
1796
1874
 
1797
1875
  const filesRegex = /^diff --git a\/(?<file>.+) b\/(?<file2>.+)$/g
1798
1876
  const linesRegex = /^@@ -\d+(,\d+)? \+(?<start>\d+)(,(?<count>\d+))? @@/g
@@ -1807,6 +1885,7 @@ function getModifiedFilesFromDiff (diff) {
1807
1885
  if (fileMatch && fileMatch.groups.file) {
1808
1886
  currentFile = fileMatch.groups.file
1809
1887
  result[currentFile] = []
1888
+ hasModifiedFiles = true
1810
1889
  continue
1811
1890
  }
1812
1891
 
@@ -1825,7 +1904,7 @@ function getModifiedFilesFromDiff (diff) {
1825
1904
  linesRegex.lastIndex = 0
1826
1905
  }
1827
1906
 
1828
- if (Object.keys(result).length === 0) {
1907
+ if (!hasModifiedFiles) {
1829
1908
  return null
1830
1909
  }
1831
1910
  return result
@@ -35,6 +35,11 @@ const MANUAL_DROP = tags.MANUAL_DROP
35
35
 
36
36
  const contexts = new WeakMap()
37
37
  const requests = new WeakMap()
38
+ const statusCodeRangesPattern = /^[1-5]\d{2}(?:-[1-5]\d{2})?(?:,[1-5]\d{2}(?:-[1-5]\d{2})?)*$/
39
+ const whitespacePattern = /\s/g
40
+ const MAX_HTTP_STATUS_CODE = 599
41
+
42
+ /** @typedef {(code: number) => boolean} StatusValidator */
38
43
 
39
44
  // TODO: change this to no longer rely on creating a dummy plugin to be able to access startSpan
40
45
  function createWebPlugin (tracer, config = {}) {
@@ -371,6 +376,9 @@ function addAllowHeaders (req, res, headers) {
371
376
  const allowHeaders = splitHeader(headers['access-control-allow-headers'])
372
377
  const requestHeaders = splitHeader(req.headers['access-control-request-headers'])
373
378
  const contextHeaders = [
379
+ 'baggage',
380
+ 'traceparent',
381
+ 'tracestate',
374
382
  'x-datadog-origin',
375
383
  'x-datadog-parent-id',
376
384
  'x-datadog-sampled', // Deprecated, but still accept it in case it's sent.
@@ -559,13 +567,59 @@ function isNot500ErrorCode (code) {
559
567
  return code < 500
560
568
  }
561
569
 
570
+ /**
571
+ * @param {{ validateStatus?: unknown, DD_TRACE_HTTP_SERVER_ERROR_STATUSES?: unknown }} config
572
+ * @returns {StatusValidator}
573
+ */
562
574
  function getStatusValidator (config) {
563
575
  if (typeof config.validateStatus === 'function') {
564
- return config.validateStatus
576
+ return /** @type {StatusValidator} */ (config.validateStatus)
565
577
  } else if (config.hasOwnProperty('validateStatus')) {
566
578
  log.error('Expected `validateStatus` to be a function.')
567
579
  }
568
- return isNot500ErrorCode
580
+
581
+ const { DD_TRACE_HTTP_SERVER_ERROR_STATUSES } = config
582
+ if (DD_TRACE_HTTP_SERVER_ERROR_STATUSES === undefined) return isNot500ErrorCode
583
+ if (typeof DD_TRACE_HTTP_SERVER_ERROR_STATUSES !== 'string') {
584
+ log.error('Expected `DD_TRACE_HTTP_SERVER_ERROR_STATUSES` to be a string.')
585
+ return isNot500ErrorCode
586
+ }
587
+ if (DD_TRACE_HTTP_SERVER_ERROR_STATUSES === '500-599') return isNot500ErrorCode
588
+
589
+ const normalized = DD_TRACE_HTTP_SERVER_ERROR_STATUSES.replaceAll(whitespacePattern, '')
590
+ if (normalized === '500-599') return isNot500ErrorCode
591
+ if (!statusCodeRangesPattern.test(normalized)) {
592
+ log.error(
593
+ '`DD_TRACE_HTTP_SERVER_ERROR_STATUSES` must contain comma-separated status codes or ranges from 100 to 599.'
594
+ )
595
+ return isNot500ErrorCode
596
+ }
597
+
598
+ const errorStatusCodes = new Uint8Array(MAX_HTTP_STATUS_CODE + 1)
599
+ const ranges = normalized.split(',')
600
+ for (const range of ranges) {
601
+ const separator = range.indexOf('-')
602
+ if (separator === -1) {
603
+ errorStatusCodes[Number(range)] = 1
604
+ continue
605
+ }
606
+
607
+ const first = Number(range.slice(0, separator))
608
+ const second = Number(range.slice(separator + 1))
609
+ const start = Math.min(first, second)
610
+ const end = Math.max(first, second)
611
+ errorStatusCodes.fill(1, start, end + 1)
612
+ }
613
+
614
+ /**
615
+ * @param {number} code
616
+ * @returns {boolean}
617
+ */
618
+ function isValidStatusCode (code) {
619
+ return errorStatusCodes[code] !== 1
620
+ }
621
+
622
+ return isValidStatusCode
569
623
  }
570
624
 
571
625
  const noop = () => {}
@@ -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
  }