dd-trace 5.87.0 → 5.89.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 (119) hide show
  1. package/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/ext/tags.js +2 -0
  5. package/index.d.ts +234 -4
  6. package/package.json +18 -11
  7. package/packages/datadog-instrumentations/src/ai.js +54 -90
  8. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +27 -110
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/compiler.js +74 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/index.js +43 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/matcher.js +49 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/transformer.js +121 -0
  17. package/packages/datadog-instrumentations/src/helpers/rewriter/{transforms.js → orchestrion/transforms.js} +143 -17
  18. package/packages/datadog-instrumentations/src/jest.js +176 -54
  19. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  20. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  21. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  22. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  23. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  24. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  25. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  26. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +62 -5
  27. package/packages/datadog-plugin-cypress/src/source-map-utils.js +297 -0
  28. package/packages/datadog-plugin-cypress/src/support.js +52 -9
  29. package/packages/datadog-plugin-jest/src/index.js +12 -2
  30. package/packages/datadog-plugin-jest/src/util.js +2 -1
  31. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  32. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  33. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  34. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  35. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  36. package/packages/dd-trace/src/aiguard/sdk.js +5 -1
  37. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  38. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  39. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  40. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  41. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  42. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  45. package/packages/dd-trace/src/azure_metadata.js +0 -2
  46. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  47. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +3 -0
  48. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  49. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  50. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  51. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  52. package/packages/dd-trace/src/config/defaults.js +148 -197
  53. package/packages/dd-trace/src/config/helper.js +43 -1
  54. package/packages/dd-trace/src/config/index.js +38 -14
  55. package/packages/dd-trace/src/config/supported-configurations.json +4125 -512
  56. package/packages/dd-trace/src/constants.js +0 -2
  57. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  58. package/packages/dd-trace/src/datastreams/checkpointer.js +13 -0
  59. package/packages/dd-trace/src/datastreams/index.js +3 -0
  60. package/packages/dd-trace/src/datastreams/manager.js +9 -0
  61. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  62. package/packages/dd-trace/src/datastreams/processor.js +140 -4
  63. package/packages/dd-trace/src/encode/agentless-json.js +155 -0
  64. package/packages/dd-trace/src/exporter.js +2 -0
  65. package/packages/dd-trace/src/exporters/agent/writer.js +21 -8
  66. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  67. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  68. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  69. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  70. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  71. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  72. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  73. package/packages/dd-trace/src/opentracing/span.js +6 -4
  74. package/packages/dd-trace/src/pkg.js +1 -1
  75. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  76. package/packages/dd-trace/src/plugins/database.js +15 -2
  77. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  78. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  79. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  80. package/packages/dd-trace/src/proxy.js +6 -1
  81. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  82. package/packages/dd-trace/src/startup-log.js +53 -19
  83. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  84. package/vendor/dist/@datadog/source-map/index.js +1 -1
  85. package/vendor/dist/@isaacs/ttlcache/index.js +1 -1
  86. package/vendor/dist/@opentelemetry/core/index.js +1 -1
  87. package/vendor/dist/@opentelemetry/resources/index.js +1 -1
  88. package/vendor/dist/astring/index.js +1 -1
  89. package/vendor/dist/crypto-randomuuid/index.js +1 -1
  90. package/vendor/dist/escape-string-regexp/index.js +1 -1
  91. package/vendor/dist/esquery/index.js +1 -1
  92. package/vendor/dist/ignore/index.js +1 -1
  93. package/vendor/dist/istanbul-lib-coverage/index.js +1 -1
  94. package/vendor/dist/jest-docblock/index.js +1 -1
  95. package/vendor/dist/jsonpath-plus/index.js +1 -1
  96. package/vendor/dist/limiter/index.js +1 -1
  97. package/vendor/dist/lodash.sortby/index.js +1 -1
  98. package/vendor/dist/lru-cache/index.js +1 -1
  99. package/vendor/dist/meriyah/index.js +1 -1
  100. package/vendor/dist/module-details-from-path/index.js +1 -1
  101. package/vendor/dist/mutexify/promise/index.js +1 -1
  102. package/vendor/dist/opentracing/index.js +1 -1
  103. package/vendor/dist/path-to-regexp/index.js +1 -1
  104. package/vendor/dist/pprof-format/index.js +1 -1
  105. package/vendor/dist/protobufjs/index.js +1 -1
  106. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  107. package/vendor/dist/retry/index.js +1 -1
  108. package/vendor/dist/rfdc/index.js +1 -1
  109. package/vendor/dist/semifies/index.js +1 -1
  110. package/vendor/dist/shell-quote/index.js +1 -1
  111. package/vendor/dist/source-map/index.js +1 -1
  112. package/vendor/dist/source-map/lib/util/index.js +1 -1
  113. package/vendor/dist/tlhunter-sorted-set/index.js +1 -1
  114. package/vendor/dist/ttl-set/index.js +1 -1
  115. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +0 -33
  116. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  117. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  118. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  119. package/packages/dd-trace/src/scope/noop/scope.js +0 -21
@@ -21,6 +21,7 @@ const {
21
21
  getFormattedJestTestParameters,
22
22
  getJestTestName,
23
23
  getJestSuitesToRun,
24
+ getEfdRetryCount,
24
25
  } = require('../../datadog-plugin-jest/src/util')
25
26
  const { addHook, channel } = require('./helpers/instrument')
26
27
 
@@ -68,7 +69,9 @@ const RETRY_TIMES = Symbol.for('RETRY_TIMES')
68
69
  let skippableSuites = []
69
70
  let knownTests = {}
70
71
  let isCodeCoverageEnabled = false
72
+ let isCodeCoverageEnabledBecauseOfUs = false
71
73
  let isSuitesSkippingEnabled = false
74
+ let isKeepingCoverageConfiguration = false
72
75
  let isUserCodeCoverageEnabled = false
73
76
  let isSuitesSkipped = false
74
77
  let numSkippedSuites = 0
@@ -76,6 +79,7 @@ let hasUnskippableSuites = false
76
79
  let hasForcedToRunSuites = false
77
80
  let isEarlyFlakeDetectionEnabled = false
78
81
  let earlyFlakeDetectionNumRetries = 0
82
+ let earlyFlakeDetectionSlowTestRetries = {}
79
83
  let earlyFlakeDetectionFaultyThreshold = 30
80
84
  let isEarlyFlakeDetectionFaulty = false
81
85
  let hasFilteredSkippableSuites = false
@@ -95,6 +99,12 @@ const attemptToFixRetriedTestsStatuses = new Map()
95
99
  const wrappedWorkers = new WeakSet()
96
100
  const testSuiteMockedFiles = new Map()
97
101
  const testsToBeRetried = new Set()
102
+ // Per-test: how many EFD retries were determined after the first execution.
103
+ const efdDeterminedRetries = new Map()
104
+ // Tests whose first run exceeded the 5-min threshold — tagged "slow".
105
+ const efdSlowAbortedTests = new Set()
106
+ // Tests added as EFD new-test candidates (not ATF, not impacted).
107
+ const efdNewTestCandidates = new Set()
98
108
  const testSuiteAbsolutePathsWithFastCheck = new Set()
99
109
  const testSuiteJestObjects = new Map()
100
110
 
@@ -197,7 +207,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
197
207
  this.isImpactedTestsEnabled = this.testEnvironmentOptions._ddIsImpactedTestsEnabled
198
208
 
199
209
  if (this.isKnownTestsEnabled) {
200
- earlyFlakeDetectionNumRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionNumRetries
210
+ earlyFlakeDetectionSlowTestRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionSlowTestRetries ?? {}
201
211
  try {
202
212
  this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
203
213
 
@@ -466,7 +476,13 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
466
476
  testContexts.set(event.test, ctx)
467
477
 
468
478
  testStartCh.runStores(ctx, () => {
469
- for (const hook of event.test.parent.hooks) {
479
+ let p = event.test.parent
480
+ const hooks = []
481
+ while (p != null) {
482
+ hooks.push(...p.hooks)
483
+ p = p.parent
484
+ }
485
+ for (const hook of hooks) {
470
486
  let hookFn = hook.fn
471
487
  if (originalHookFns.has(hook)) {
472
488
  hookFn = originalHookFns.get(hook)
@@ -537,11 +553,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
537
553
  retriedTestsToNumAttempts.set(testFullName, 0)
538
554
  if (this.isEarlyFlakeDetectionEnabled) {
539
555
  testsToBeRetried.add(testFullName)
540
- this.retryTest({
541
- jestEvent: event,
542
- retryCount: earlyFlakeDetectionNumRetries,
543
- retryType: 'Early flake detection',
544
- })
556
+ efdNewTestCandidates.add(testFullName)
557
+ // Cloning is deferred to test_done after the first execution,
558
+ // when we know the duration and can choose the right retry count.
545
559
  }
546
560
  }
547
561
  }
@@ -566,8 +580,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
566
580
  let attemptToFixFailed = false
567
581
  let failedAllTests = false
568
582
  let isAttemptToFix = false
583
+ const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
569
584
  if (this.isTestManagementTestsEnabled) {
570
- const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
571
585
  isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(testName)
572
586
  if (isAttemptToFix) {
573
587
  if (attemptToFixRetriedTestsStatuses.has(testName)) {
@@ -592,9 +606,53 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
592
606
  }
593
607
  }
594
608
 
609
+ // EFD dynamic cloning: on first execution of a new EFD candidate,
610
+ // determine the retry count from the test's duration.
611
+ if (
612
+ this.isEarlyFlakeDetectionEnabled &&
613
+ this.isKnownTestsEnabled &&
614
+ efdNewTestCandidates.has(testName) &&
615
+ event.test.invocations === 1 &&
616
+ !efdDeterminedRetries.has(testName)
617
+ ) {
618
+ const durationMs = event.test.duration ?? 0
619
+ const retryCount = getEfdRetryCount(durationMs, earlyFlakeDetectionSlowTestRetries)
620
+ efdDeterminedRetries.set(testName, retryCount)
621
+ if (retryCount > 0) {
622
+ // Temporarily adjust jest-circus state so that retry tests are registered
623
+ // into the correct describe block and bypass the "tests have started" guard.
624
+ //
625
+ // Problem 1 (jest-circus ≤24): currentDescribeBlock points to ROOT during
626
+ // execution, and ROOT's tests loop already finished before children ran.
627
+ //
628
+ // Problem 2 (jest-circus ≥27): `hasStarted = true` causes `test()` to throw
629
+ // "Cannot add a test after tests have started running".
630
+ //
631
+ // Fix: temporarily point currentDescribeBlock to the test's parent (so retries
632
+ // land in the still-iterating children array) and set hasStarted = false (so the
633
+ // guard is bypassed). Both are restored immediately after scheduling the retries.
634
+ const originalDescribeBlock = state.currentDescribeBlock
635
+ const originalHasStarted = state.hasStarted
636
+ state.currentDescribeBlock = event.test.parent ?? originalDescribeBlock
637
+ state.hasStarted = false
638
+ this.retryTest({
639
+ jestEvent: {
640
+ testName: event.test.name,
641
+ fn: event.test.fn,
642
+ timeout: event.test.timeout,
643
+ },
644
+ retryCount,
645
+ retryType: 'Early flake detection',
646
+ })
647
+ state.currentDescribeBlock = originalDescribeBlock
648
+ state.hasStarted = originalHasStarted
649
+ } else {
650
+ efdSlowAbortedTests.add(testName)
651
+ }
652
+ }
653
+
595
654
  let isEfdRetry = false
596
655
  // We'll store the test statuses of the retries
597
- const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
598
656
  if (this.isKnownTestsEnabled) {
599
657
  const isNewTest = retriedTestsToNumAttempts.has(testName)
600
658
  if (isNewTest) {
@@ -607,7 +665,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
607
665
  const testStatuses = newTestsTestStatuses.get(testName)
608
666
  // Check if this is the last EFD retry.
609
667
  // If it is, we'll set the failedAllTests flag to true if all the tests failed
610
- if (testStatuses.length === earlyFlakeDetectionNumRetries + 1 &&
668
+ const efdRetryCount = efdDeterminedRetries.get(testName) ?? 0
669
+ if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
611
670
  testStatuses.every(status => status === 'fail')) {
612
671
  failedAllTests = true
613
672
  }
@@ -665,6 +724,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
665
724
  attemptToFixFailed,
666
725
  isAtrRetry,
667
726
  finalStatus,
727
+ earlyFlakeAbortReason: efdSlowAbortedTests.has(testName) ? 'slow' : undefined,
668
728
  })
669
729
 
670
730
  if (promises.isProbeReady) {
@@ -676,6 +736,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
676
736
  test.errors = errors
677
737
  }
678
738
  atrSuppressedErrors.clear()
739
+ efdDeterminedRetries.clear()
740
+ efdSlowAbortedTests.clear()
741
+ efdNewTestCandidates.clear()
679
742
  }
680
743
  if (event.name === 'test_skip' || event.name === 'test_todo') {
681
744
  const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
@@ -696,7 +759,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
696
759
  getEfdResult ({ testName, isNewTest, isModifiedTest, isEfdRetry, numberOfExecutedRetries }) {
697
760
  const isEfdEnabled = this.isEarlyFlakeDetectionEnabled
698
761
  const isEfdActive = isEfdEnabled && (isNewTest || isModifiedTest)
699
- const isLastEfdRetry = isEfdRetry && numberOfExecutedRetries >= (earlyFlakeDetectionNumRetries + 1)
762
+ const retryCount = efdDeterminedRetries.get(testName) ?? 0
763
+ const isSlowAbort = efdSlowAbortedTests.has(testName)
764
+ const isLastEfdRetry = (isEfdRetry && numberOfExecutedRetries >= (retryCount + 1)) || isSlowAbort
700
765
  const isFinalEfdTestExecution = isEfdActive && isLastEfdRetry
701
766
 
702
767
  let finalStatus
@@ -931,8 +996,11 @@ function getCliWrapper (isNewJestVersion) {
931
996
  if (!err) {
932
997
  isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled
933
998
  isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
999
+ isKeepingCoverageConfiguration =
1000
+ libraryConfig.isKeepingCoverageConfiguration ?? isKeepingCoverageConfiguration
934
1001
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
935
1002
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
1003
+ earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
936
1004
  earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
937
1005
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
938
1006
  isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
@@ -1263,9 +1331,10 @@ function coverageReporterWrapper (coverageReporter) {
1263
1331
  */
1264
1332
  // `_addUntestedFiles` is an async function
1265
1333
  shimmer.wrap(CoverageReporter.prototype, '_addUntestedFiles', addUntestedFiles => function () {
1266
- // If the user has added coverage manually, they're willing to pay the price of this execution, so
1267
- // we will not skip it.
1268
- if (isSuitesSkippingEnabled && !isUserCodeCoverageEnabled) {
1334
+ if (isKeepingCoverageConfiguration) {
1335
+ return addUntestedFiles.apply(this, arguments)
1336
+ }
1337
+ if (isCodeCoverageEnabledBecauseOfUs) {
1269
1338
  return Promise.resolve()
1270
1339
  }
1271
1340
  return addUntestedFiles.apply(this, arguments)
@@ -1445,12 +1514,13 @@ function configureTestEnvironment (readConfigsResult) {
1445
1514
  }
1446
1515
 
1447
1516
  isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
1517
+ isCodeCoverageEnabledBecauseOfUs = isCodeCoverageEnabled && !isUserCodeCoverageEnabled
1448
1518
 
1449
1519
  if (readConfigsResult.globalConfig.forceExit) {
1450
1520
  log.warn("Jest's '--forceExit' flag has been passed. This may cause loss of data.")
1451
1521
  }
1452
1522
 
1453
- if (isCodeCoverageEnabled) {
1523
+ if (isCodeCoverageEnabledBecauseOfUs) {
1454
1524
  const globalConfig = {
1455
1525
  ...readConfigsResult.globalConfig,
1456
1526
  collectCoverage: true,
@@ -1458,14 +1528,18 @@ function configureTestEnvironment (readConfigsResult) {
1458
1528
  readConfigsResult.globalConfig = globalConfig
1459
1529
  }
1460
1530
  if (isSuitesSkippingEnabled) {
1461
- // If suite skipping is enabled, the code coverage results are not going to be relevant,
1462
- // so we do not show them.
1463
- // Also, we might skip every test, so we need to pass `passWithNoTests`
1531
+ // If suite skipping is enabled, we pass `passWithNoTests` in case every test gets skipped.
1464
1532
  const globalConfig = {
1465
1533
  ...readConfigsResult.globalConfig,
1466
- coverageReporters: ['none'],
1467
1534
  passWithNoTests: true,
1468
1535
  }
1536
+ if (isCodeCoverageEnabledBecauseOfUs && !isKeepingCoverageConfiguration) {
1537
+ globalConfig.coverageReporters = ['none']
1538
+ readConfigsResult.configs = configs.map(config => ({
1539
+ ...config,
1540
+ coverageReporters: ['none'],
1541
+ }))
1542
+ }
1469
1543
  readConfigsResult.globalConfig = globalConfig
1470
1544
  }
1471
1545
 
@@ -1488,49 +1562,97 @@ function jestConfigSyncWrapper (jestConfig) {
1488
1562
  })
1489
1563
  }
1490
1564
 
1565
+ const DD_TEST_ENVIRONMENT_OPTION_KEYS = [
1566
+ '_ddTestModuleId',
1567
+ '_ddTestSessionId',
1568
+ '_ddTestCommand',
1569
+ '_ddTestSessionName',
1570
+ '_ddForcedToRun',
1571
+ '_ddUnskippable',
1572
+ '_ddItrCorrelationId',
1573
+ '_ddKnownTests',
1574
+ '_ddIsEarlyFlakeDetectionEnabled',
1575
+ '_ddEarlyFlakeDetectionSlowTestRetries',
1576
+ '_ddRepositoryRoot',
1577
+ '_ddIsFlakyTestRetriesEnabled',
1578
+ '_ddFlakyTestRetriesCount',
1579
+ '_ddIsDiEnabled',
1580
+ '_ddIsKnownTestsEnabled',
1581
+ '_ddIsTestManagementTestsEnabled',
1582
+ '_ddTestManagementTests',
1583
+ '_ddTestManagementAttemptToFixRetries',
1584
+ '_ddModifiedFiles',
1585
+ ]
1586
+
1587
+ function removeDatadogTestEnvironmentOptions (testEnvironmentOptions) {
1588
+ const removedEntries = []
1589
+
1590
+ for (const key of DD_TEST_ENVIRONMENT_OPTION_KEYS) {
1591
+ if (!Object.hasOwn(testEnvironmentOptions, key)) {
1592
+ continue
1593
+ }
1594
+
1595
+ removedEntries.push([key, testEnvironmentOptions[key]])
1596
+ delete testEnvironmentOptions[key]
1597
+ }
1598
+
1599
+ return function restoreDatadogTestEnvironmentOptions () {
1600
+ for (const [key, value] of removedEntries) {
1601
+ testEnvironmentOptions[key] = value
1602
+ }
1603
+ }
1604
+ }
1605
+
1606
+ /**
1607
+ * Wrap `createScriptTransformer` to temporarily hide Datadog-specific
1608
+ * `testEnvironmentOptions` keys while Jest builds its transform config.
1609
+ *
1610
+ * @param {Function} createScriptTransformer
1611
+ * @returns {Function}
1612
+ */
1613
+ function wrapCreateScriptTransformer (createScriptTransformer) {
1614
+ return function (config) {
1615
+ const testEnvironmentOptions = config?.testEnvironmentOptions
1616
+
1617
+ if (!testEnvironmentOptions) {
1618
+ return createScriptTransformer.apply(this, arguments)
1619
+ }
1620
+
1621
+ const restoreTestEnvironmentOptions = removeDatadogTestEnvironmentOptions(testEnvironmentOptions)
1622
+
1623
+ try {
1624
+ const result = createScriptTransformer.apply(this, arguments)
1625
+
1626
+ if (result?.then) {
1627
+ return result.finally(restoreTestEnvironmentOptions)
1628
+ }
1629
+
1630
+ restoreTestEnvironmentOptions()
1631
+ return result
1632
+ } catch (e) {
1633
+ restoreTestEnvironmentOptions()
1634
+ throw e
1635
+ }
1636
+ }
1637
+ }
1638
+
1491
1639
  addHook({
1492
1640
  name: '@jest/transform',
1493
- versions: ['>=24.8.0'],
1641
+ versions: ['>=24.8.0 <30.0.0'],
1494
1642
  file: 'build/ScriptTransformer.js',
1495
1643
  }, transformPackage => {
1496
- const originalCreateScriptTransformer = transformPackage.createScriptTransformer
1497
-
1498
- // `createScriptTransformer` is an async function
1499
- transformPackage.createScriptTransformer = function (config) {
1500
- const { testEnvironmentOptions, ...restOfConfig } = config
1501
- const {
1502
- _ddTestModuleId,
1503
- _ddTestSessionId,
1504
- _ddTestCommand,
1505
- _ddTestSessionName,
1506
- _ddForcedToRun,
1507
- _ddUnskippable,
1508
- _ddItrCorrelationId,
1509
- _ddKnownTests,
1510
- _ddIsEarlyFlakeDetectionEnabled,
1511
- _ddEarlyFlakeDetectionNumRetries,
1512
- _ddRepositoryRoot,
1513
- _ddIsFlakyTestRetriesEnabled,
1514
- _ddFlakyTestRetriesCount,
1515
- _ddIsDiEnabled,
1516
- _ddIsKnownTestsEnabled,
1517
- _ddIsTestManagementTestsEnabled,
1518
- _ddTestManagementTests,
1519
- _ddTestManagementAttemptToFixRetries,
1520
- _ddModifiedFiles,
1521
- ...restOfTestEnvironmentOptions
1522
- } = testEnvironmentOptions
1523
-
1524
- restOfConfig.testEnvironmentOptions = restOfTestEnvironmentOptions
1525
-
1526
- arguments[0] = restOfConfig
1527
-
1528
- return originalCreateScriptTransformer.apply(this, arguments)
1529
- }
1644
+ transformPackage.createScriptTransformer = wrapCreateScriptTransformer(transformPackage.createScriptTransformer)
1530
1645
 
1531
1646
  return transformPackage
1532
1647
  })
1533
1648
 
1649
+ addHook({
1650
+ name: '@jest/transform',
1651
+ versions: ['>=30.0.0'],
1652
+ }, transformPackage => {
1653
+ return shimmer.wrap(transformPackage, 'createScriptTransformer', wrapCreateScriptTransformer, { replaceGetter: true })
1654
+ })
1655
+
1534
1656
  /**
1535
1657
  * Hook to remove the test paths (test suite) that are part of `skippableSuites`
1536
1658
  */
@@ -24,22 +24,6 @@ const batchConsumerErrorCh = channel('apm:kafkajs:consume-batch:error')
24
24
 
25
25
  const disabledHeaderWeakSet = new WeakSet()
26
26
 
27
- function commitsFromEvent (event) {
28
- const { payload: { groupId, topics } } = event
29
- const commitList = []
30
- for (const { topic, partitions } of topics) {
31
- for (const { partition, offset } of partitions) {
32
- commitList.push({
33
- groupId,
34
- partition,
35
- offset,
36
- topic,
37
- })
38
- }
39
- }
40
- consumerCommitCh.publish(commitList)
41
- }
42
-
43
27
  addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKafka) => {
44
28
  class Kafka extends BaseKafka {
45
29
  constructor (options) {
@@ -132,6 +116,7 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
132
116
  }
133
117
 
134
118
  const kafkaClusterIdPromise = getKafkaClusterId(this)
119
+ let resolvedClusterId = null
135
120
 
136
121
  const eachMessageExtractor = (args, clusterId) => {
137
122
  const { topic, partition, message } = args[0]
@@ -146,13 +131,31 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
146
131
 
147
132
  const consumer = createConsumer.apply(this, arguments)
148
133
 
149
- consumer.on(consumer.events.COMMIT_OFFSETS, commitsFromEvent)
134
+ consumer.on(consumer.events.COMMIT_OFFSETS, (event) => {
135
+ const { payload: { groupId: commitGroupId, topics } } = event
136
+ const commitList = []
137
+ for (const { topic, partitions } of topics) {
138
+ for (const { partition, offset } of partitions) {
139
+ commitList.push({
140
+ groupId: commitGroupId,
141
+ partition,
142
+ offset,
143
+ topic,
144
+ clusterId: resolvedClusterId,
145
+ })
146
+ }
147
+ }
148
+ consumerCommitCh.publish(commitList)
149
+ })
150
150
 
151
151
  const run = consumer.run
152
152
  const groupId = arguments[0].groupId
153
153
 
154
154
  consumer.run = function ({ eachMessage, eachBatch, ...runArgs }) {
155
155
  const wrapConsume = (clusterId) => {
156
+ // In kafkajs COMMIT_OFFSETS always happens in the context of one synchronous run
157
+ // So this will always reference a correct cluster id
158
+ resolvedClusterId = clusterId
156
159
  return run({
157
160
  eachMessage: wrappedCallback(
158
161
  eachMessage,
@@ -41,7 +41,7 @@ const testSuiteToTestStatuses = new Map()
41
41
  const testSuiteToErrors = new Map()
42
42
  const testsToTestStatuses = new Map()
43
43
 
44
- const RUM_FLUSH_WAIT_TIME = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')) || 1000
44
+ const RUM_FLUSH_WAIT_TIME = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')) || 500
45
45
 
46
46
  let applyRepeatEachIndex = null
47
47
 
@@ -9,6 +9,19 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
9
9
  static id = 'amqplib'
10
10
  static operation = 'consume'
11
11
 
12
+ start (ctx) {
13
+ if (!this.config.dsmEnabled) return
14
+ const { fields = {}, message, queue } = ctx
15
+ if (!message?.properties?.headers) return
16
+
17
+ const { span } = ctx.currentStore
18
+ const queueName = queue || fields.queue || fields.routingKey
19
+ const payloadSize = getAmqpMessageSize({ headers: message.properties.headers, content: message.content })
20
+ this.tracer.decodeDataStreamsContext(message.properties.headers)
21
+ this.tracer
22
+ .setCheckpoint(['direction:in', `topic:${queueName}`, 'type:rabbitmq'], span, payloadSize)
23
+ }
24
+
12
25
  bindStart (ctx) {
13
26
  const { method, fields = {}, message, queue } = ctx
14
27
 
@@ -17,7 +30,7 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
17
30
  const childOf = extract(this.tracer, message)
18
31
 
19
32
  const queueName = queue || fields.queue || fields.routingKey
20
- const span = this.startSpan({
33
+ this.startSpan({
21
34
  childOf,
22
35
  resource: getResourceName(method, fields),
23
36
  type: 'worker',
@@ -31,15 +44,6 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
31
44
  },
32
45
  }, ctx)
33
46
 
34
- if (
35
- this.config.dsmEnabled && message?.properties?.headers
36
- ) {
37
- const payloadSize = getAmqpMessageSize({ headers: message.properties.headers, content: message.content })
38
- this.tracer.decodeDataStreamsContext(message.properties.headers)
39
- this.tracer
40
- .setCheckpoint(['direction:in', `topic:${queueName}`, 'type:rabbitmq'], span, payloadSize)
41
- }
42
-
43
47
  return ctx.currentStore
44
48
  }
45
49
  }
@@ -10,8 +10,30 @@ class AmqplibProducerPlugin extends ProducerPlugin {
10
10
  static id = 'amqplib'
11
11
  static operation = 'publish'
12
12
 
13
+ start (ctx) {
14
+ if (!this.config.dsmEnabled) return
15
+ const { fields, message } = ctx
16
+ const { span } = ctx.currentStore
17
+
18
+ const hasRoutingKey = fields.routingKey != null
19
+ const payloadSize = getAmqpMessageSize({ content: message, headers: fields.headers })
20
+
21
+ // there are two ways to send messages in RabbitMQ:
22
+ // 1. using an exchange and a routing key in which DSM connects via the exchange
23
+ // 2. using an unnamed exchange and a routing key in which DSM connects via the topic
24
+ const exchangeOrTopicTag = hasRoutingKey && !fields.exchange
25
+ ? `topic:${fields.routingKey}`
26
+ : `exchange:${fields.exchange}`
27
+
28
+ const dataStreamsContext = this.tracer.setCheckpoint(
29
+ ['direction:out', exchangeOrTopicTag, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq'],
30
+ span, payloadSize
31
+ )
32
+ DsmPathwayCodec.encode(dataStreamsContext, fields.headers)
33
+ }
34
+
13
35
  bindStart (ctx) {
14
- const { channel = {}, method, fields, message } = ctx
36
+ const { channel = {}, method, fields } = ctx
15
37
 
16
38
  if (method !== 'basic.publish') return
17
39
 
@@ -34,24 +56,6 @@ class AmqplibProducerPlugin extends ProducerPlugin {
34
56
 
35
57
  this.tracer.inject(span, TEXT_MAP, fields.headers)
36
58
 
37
- if (this.config.dsmEnabled) {
38
- const hasRoutingKey = fields.routingKey != null
39
- const payloadSize = getAmqpMessageSize({ content: message, headers: fields.headers })
40
-
41
- // there are two ways to send messages in RabbitMQ:
42
- // 1. using an exchange and a routing key in which DSM connects via the exchange
43
- // 2. using an unnamed exchange and a routing key in which DSM connects via the topic
44
- const exchangeOrTopicTag = hasRoutingKey && !fields.exchange
45
- ? `topic:${fields.routingKey}`
46
- : `exchange:${fields.exchange}`
47
-
48
- const dataStreamsContext = this.tracer
49
- .setCheckpoint(
50
- ['direction:out', exchangeOrTopicTag, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq']
51
- , span, payloadSize)
52
- DsmPathwayCodec.encode(dataStreamsContext, fields.headers)
53
- }
54
-
55
59
  return ctx.currentStore
56
60
  }
57
61
  }
@@ -11,17 +11,24 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
11
11
  ctx.currentStore?.span?.finish()
12
12
  }
13
13
 
14
+ start (ctx) {
15
+ if (!this.config.dsmEnabled) return
16
+ const { span } = ctx.currentStore
17
+ this.setConsumerCheckpoint(span, ctx)
18
+ }
19
+
14
20
  bindStart (ctx) {
15
21
  const job = ctx.arguments?.[0]
16
22
  const queueName = job?.queueName || job?.queue?.name || 'bullmq'
17
23
 
18
24
  let childOf
19
- const datadogContext = job?.data?._datadog
20
- if (datadogContext) {
21
- childOf = this.tracer.extract('text_map', datadogContext)
25
+ const ddCarrier = this._extractDatadog(job)
26
+ if (ddCarrier) {
27
+ ctx._ddCarrier = ddCarrier
28
+ childOf = this.tracer.extract('text_map', ddCarrier)
22
29
  }
23
30
 
24
- const span = this.startSpan({
31
+ this.startSpan({
25
32
  childOf,
26
33
  resource: queueName,
27
34
  meta: {
@@ -33,10 +40,6 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
33
40
  },
34
41
  }, ctx)
35
42
 
36
- if (this.config.dsmEnabled) {
37
- this.setConsumerCheckpoint(span, ctx)
38
- }
39
-
40
43
  return ctx.currentStore
41
44
  }
42
45
 
@@ -47,14 +50,33 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
47
50
  const queueName = job.queueName || job.queue?.name || 'bullmq'
48
51
  const payloadSize = job.data ? getMessageSize(job.data) : 0
49
52
 
50
- const datadogContext = job.data?._datadog
51
- if (datadogContext) {
52
- this.tracer.decodeDataStreamsContext(datadogContext)
53
+ const ddCarrier = ctx._ddCarrier
54
+ if (ddCarrier) {
55
+ this.tracer.decodeDataStreamsContext(ddCarrier)
53
56
  }
54
57
 
55
58
  const edgeTags = ['direction:in', `topic:${queueName}`, 'type:bullmq']
56
59
  this.tracer.setCheckpoint(edgeTags, span, payloadSize)
57
60
  }
61
+
62
+ _extractDatadog (job) {
63
+ const metadataStr = job?.opts?.telemetry?.metadata
64
+ if (!metadataStr) return
65
+
66
+ try {
67
+ const metadata = JSON.parse(metadataStr)
68
+ const ddCarrier = metadata._datadog
69
+ if (!ddCarrier) return
70
+
71
+ // Clean up only our _datadog key, preserve other metadata
72
+ delete metadata._datadog
73
+ job.opts.telemetry.metadata = JSON.stringify(metadata)
74
+
75
+ return ddCarrier
76
+ } catch {
77
+ // Ignore malformed metadata
78
+ }
79
+ }
58
80
  }
59
81
 
60
82
  module.exports = BullmqConsumerPlugin