dd-trace 5.110.0 → 5.112.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 (164) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/README.md +13 -8
  3. package/ci/init.js +21 -2
  4. package/ci/vitest-no-worker-init-setup.mjs +430 -0
  5. package/ext/tags.js +2 -0
  6. package/index.d.ts +34 -1
  7. package/initialize.mjs +5 -6
  8. package/loader-hook.mjs +76 -55
  9. package/package.json +37 -34
  10. package/packages/datadog-instrumentations/src/ai.js +45 -0
  11. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  12. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  13. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  14. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  15. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  16. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  17. package/packages/datadog-instrumentations/src/fs.js +8 -6
  18. package/packages/datadog-instrumentations/src/graphql.js +26 -495
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  20. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  25. package/packages/datadog-instrumentations/src/jest.js +713 -63
  26. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  27. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  28. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  29. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  30. package/packages/datadog-instrumentations/src/mongoose.js +13 -2
  31. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  32. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1297 -0
  33. package/packages/datadog-instrumentations/src/vitest-main.js +1594 -0
  34. package/packages/datadog-instrumentations/src/vitest-util.js +254 -0
  35. package/packages/datadog-instrumentations/src/vitest-worker.js +823 -0
  36. package/packages/datadog-instrumentations/src/vitest.js +11 -1855
  37. package/packages/datadog-plugin-ai/src/index.js +1 -1
  38. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  39. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  40. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +19 -10
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  43. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
  44. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  45. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  46. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  47. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  48. package/packages/datadog-plugin-cucumber/src/index.js +15 -24
  49. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +360 -14
  50. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  51. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  52. package/packages/datadog-plugin-cypress/src/support.js +45 -1
  53. package/packages/datadog-plugin-express/src/code_origin.js +1 -1
  54. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
  55. package/packages/datadog-plugin-graphql/src/execute.js +639 -12
  56. package/packages/datadog-plugin-graphql/src/index.js +37 -9
  57. package/packages/datadog-plugin-graphql/src/parse.js +24 -4
  58. package/packages/datadog-plugin-graphql/src/utils.js +9 -2
  59. package/packages/datadog-plugin-graphql/src/validate.js +17 -6
  60. package/packages/datadog-plugin-http/src/index.js +6 -8
  61. package/packages/datadog-plugin-jest/src/index.js +31 -15
  62. package/packages/datadog-plugin-mocha/src/index.js +40 -15
  63. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  64. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  65. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  66. package/packages/datadog-plugin-openai/src/services.js +2 -2
  67. package/packages/datadog-plugin-playwright/src/index.js +4 -3
  68. package/packages/datadog-plugin-vitest/src/index.js +120 -72
  69. package/packages/datadog-shimmer/src/shimmer.js +37 -16
  70. package/packages/dd-trace/src/aiguard/index.js +9 -14
  71. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  72. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  73. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  74. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  75. package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
  76. package/packages/dd-trace/src/appsec/channels.js +3 -1
  77. package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
  78. package/packages/dd-trace/src/appsec/graphql.js +14 -11
  79. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +23 -23
  80. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  81. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
  82. package/packages/dd-trace/src/appsec/index.js +10 -1
  83. package/packages/dd-trace/src/appsec/lambda.js +135 -0
  84. package/packages/dd-trace/src/appsec/reporter.js +49 -10
  85. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
  86. package/packages/dd-trace/src/appsec/waf/index.js +16 -4
  87. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
  88. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  89. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  90. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
  91. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +5 -2
  92. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  93. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  94. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +6 -2
  95. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  96. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +130 -20
  97. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
  98. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
  99. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
  100. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
  101. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
  102. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
  103. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  104. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
  105. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
  106. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
  107. package/packages/dd-trace/src/config/defaults.js +3 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +71 -57
  109. package/packages/dd-trace/src/config/helper.js +1 -0
  110. package/packages/dd-trace/src/config/index.js +35 -22
  111. package/packages/dd-trace/src/config/major-overrides.js +4 -2
  112. package/packages/dd-trace/src/config/remote_config.js +1 -1
  113. package/packages/dd-trace/src/config/supported-configurations.json +127 -56
  114. package/packages/dd-trace/src/constants.js +7 -0
  115. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  116. package/packages/dd-trace/src/datastreams/processor.js +11 -3
  117. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  118. package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
  119. package/packages/dd-trace/src/llmobs/index.js +1 -1
  120. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  121. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  122. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  123. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  124. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
  125. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  126. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  127. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  128. package/packages/dd-trace/src/llmobs/sdk.js +3 -3
  129. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  130. package/packages/dd-trace/src/llmobs/tagger.js +3 -3
  131. package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
  132. package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
  133. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
  134. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
  135. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
  136. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  137. package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
  138. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  139. package/packages/dd-trace/src/opentracing/tracer.js +6 -1
  140. package/packages/dd-trace/src/plugin_manager.js +23 -7
  141. package/packages/dd-trace/src/plugins/ci_plugin.js +159 -10
  142. package/packages/dd-trace/src/plugins/index.js +2 -0
  143. package/packages/dd-trace/src/plugins/util/llm.js +6 -1
  144. package/packages/dd-trace/src/plugins/util/test.js +15 -9
  145. package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
  146. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
  147. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
  148. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  149. package/packages/dd-trace/src/proxy.js +8 -8
  150. package/packages/dd-trace/src/span_processor.js +5 -6
  151. package/packages/dd-trace/src/span_stats.js +96 -78
  152. package/packages/dd-trace/src/startup-log.js +2 -1
  153. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  154. package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
  155. package/packages/dd-trace/src/telemetry/index.js +2 -2
  156. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  157. package/packages/dd-trace/src/telemetry/send-data.js +8 -8
  158. package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
  159. package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
  160. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  161. package/vendor/dist/protobufjs/index.js +1 -1
  162. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  163. package/vendor/dist/shell-quote/index.js +1 -1
  164. package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
@@ -36,6 +36,7 @@ const {
36
36
  getOnTestEndHandler,
37
37
  getOnTestRetryHandler,
38
38
  getOnHookEndHandler,
39
+ patchFailedTestReplayHookUp,
39
40
  getOnFailHandler,
40
41
  getOnPendingHandler,
41
42
  testFileToSuiteCtx,
@@ -382,6 +383,10 @@ function applyTestManagementTestsResponse ({ err, testManagementTests: receivedT
382
383
  }
383
384
  }
384
385
 
386
+ function isFailedTestReplayEnabled () {
387
+ return config.isTestDynamicInstrumentationEnabled && config.isDiEnabled
388
+ }
389
+
385
390
  function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFinishRequest, localSuites) {
386
391
  const ctx = {
387
392
  isParallel,
@@ -471,7 +476,7 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
471
476
  }
472
477
  }
473
478
 
474
- const onReceivedConfiguration = ({ err, libraryConfig, repositoryRoot }) => {
479
+ const onReceivedConfiguration = ({ err, isTestDynamicInstrumentationEnabled, libraryConfig, repositoryRoot }) => {
475
480
  if (err || !skippableSuitesCh.hasSubscribers || !knownTestsCh.hasSubscribers) {
476
481
  return mochaGlobalRunCh.runStores(ctx, () => {
477
482
  onFinishRequest()
@@ -492,6 +497,8 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
492
497
  config.isSuitesSkippingEnabled = config.isItrEnabled && libraryConfig.isSuitesSkippingEnabled
493
498
  config.isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
494
499
  config.flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
500
+ config.isDiEnabled = libraryConfig.isDiEnabled
501
+ config.isTestDynamicInstrumentationEnabled = isTestDynamicInstrumentationEnabled
495
502
 
496
503
  getTestOptimizationRequestResults({
497
504
  isKnownTestsEnabled: config.isKnownTestsEnabled,
@@ -555,6 +562,9 @@ addHook({
555
562
  })
556
563
 
557
564
  getExecutionConfiguration(runner, false, frameworkVersion, () => {
565
+ if (isFailedTestReplayEnabled()) {
566
+ patchFailedTestReplayHookUp(runner.constructor)
567
+ }
558
568
  if (config.isKnownTestsEnabled) {
559
569
  const testSuites = this.files.map(file => getTestSuitePath(file, process.cwd()))
560
570
  const isFaulty = getIsFaultyEarlyFlakeDetection(
@@ -780,7 +790,13 @@ addHook({
780
790
  this.on('retry', getOnTestRetryHandler(config))
781
791
 
782
792
  // If the hook passes, 'hook end' will be emitted. Otherwise, 'fail' will be emitted
783
- this.on('hook end', getOnHookEndHandler(config))
793
+ this.on('hook end', getOnHookEndHandler(config, {
794
+ onStart: incrementPendingRootFinalization,
795
+ onFinish: function (test) {
796
+ finishRootSuiteAfterFinalAttempt(test)
797
+ decrementPendingRootFinalization(test)
798
+ },
799
+ }))
784
800
 
785
801
  this.on('hook end', function (hook) {
786
802
  const test = hook.ctx?.currentTest
@@ -1095,7 +1111,8 @@ addHook({
1095
1111
  (!config.isKnownTestsEnabled &&
1096
1112
  !config.isTestManagementTestsEnabled &&
1097
1113
  !config.isImpactedTestsEnabled &&
1098
- !config.isFlakyTestRetriesEnabled)) {
1114
+ !config.isFlakyTestRetriesEnabled &&
1115
+ !isFailedTestReplayEnabled())) {
1099
1116
  return run.apply(this, arguments)
1100
1117
  }
1101
1118
 
@@ -1146,6 +1163,10 @@ addHook({
1146
1163
  newWorkerArgs._ddFlakyTestRetriesCount = config.flakyTestRetriesCount
1147
1164
  }
1148
1165
 
1166
+ if (isFailedTestReplayEnabled()) {
1167
+ newWorkerArgs._ddIsFailedTestReplayEnabled = true
1168
+ }
1169
+
1149
1170
  // We pass the known tests for the test file to the worker
1150
1171
  const testFileResult = await run.apply(
1151
1172
  this,
@@ -2,9 +2,6 @@
2
2
 
3
3
  const { performance } = require('node:perf_hooks')
4
4
 
5
- // Capture real timers at module load time, before any test can install fake timers.
6
- const realSetTimeout = setTimeout
7
-
8
5
  const {
9
6
  getTestSuitePath,
10
7
  DYNAMIC_NAME_RE,
@@ -19,6 +16,7 @@ const shimmer = require('../../../datadog-shimmer')
19
16
  // test channels
20
17
  const testStartCh = channel('ci:mocha:test:start')
21
18
  const testFinishCh = channel('ci:mocha:test:finish')
19
+ const testDiWaitCh = channel('ci:mocha:test:di:wait')
22
20
  // after a test has failed, we'll publish to this channel
23
21
  const testRetryCh = channel('ci:mocha:test:retry')
24
22
  const errorCh = channel('ci:mocha:test:error')
@@ -28,7 +26,6 @@ const isModifiedCh = channel('ci:mocha:test:is-modified')
28
26
  // suite channels
29
27
  const testSuiteErrorCh = channel('ci:mocha:test-suite:error')
30
28
 
31
- const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200
32
29
  const testToContext = new WeakMap()
33
30
  const originalFns = new WeakMap()
34
31
  const testToStartLine = new WeakMap()
@@ -43,6 +40,12 @@ const testsStatuses = new Map()
43
40
  const efdRetryCountByTestFullName = new Map()
44
41
  const efdSlowAbortedTests = new Set()
45
42
  const attemptToFixExecutions = new Map()
43
+
44
+ function waitForHitProbe () {
45
+ const promises = {}
46
+ testDiWaitCh.publish({ promises })
47
+ return promises.hitBreakpointPromise
48
+ }
46
49
  const loggedAttemptToFixTests = new Set()
47
50
 
48
51
  function getAfterEachHooks (testOrHook) {
@@ -639,14 +642,8 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
639
642
  finalAttemptHandlers?.onStart?.(test)
640
643
  }
641
644
 
642
- // After finishing it might take a bit for the snapshot to be handled.
643
- // This means that tests retried with DI are BREAKPOINT_HIT_GRACE_PERIOD_MS slower at least.
644
- if (test._ddShouldWaitForHitProbe || test._retriedTest?._ddShouldWaitForHitProbe) {
645
- await new Promise((resolve) => {
646
- realSetTimeout(() => {
647
- resolve()
648
- }, BREAKPOINT_HIT_GRACE_PERIOD_MS)
649
- })
645
+ if (test._retriedTest?._ddShouldWaitForHitProbe) {
646
+ await waitForHitProbe()
650
647
  }
651
648
 
652
649
  if (shouldFinishTest) {
@@ -665,7 +662,7 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
665
662
  }
666
663
  }
667
664
 
668
- function getOnHookEndHandler (config) {
665
+ function getOnHookEndHandler (config, finalAttemptHandlers) {
669
666
  return function (hook) {
670
667
  const test = hook.ctx.currentTest
671
668
  const afterEachHooks = getAfterEachHooks(hook)
@@ -678,22 +675,118 @@ function getOnHookEndHandler (config) {
678
675
  // skip to avoid double-publishing
679
676
  if (ctx && (!test._ddIsDisabled || test._ddIsAttemptToFix)) {
680
677
  const testFinishInfo = getTestFinishInfo(test, status, config, ctx.err || test.err)
681
- if (testFinishInfo.finalStatus !== undefined) {
682
- test._ddIsFinalAttempt = true
678
+ const isFinalAttempt = testFinishInfo.finalStatus !== undefined
679
+ const publishTestFinish = () => {
680
+ testFinishCh.publish({
681
+ status,
682
+ hasBeenRetried: isMochaRetry(test),
683
+ isLastRetry: getIsLastRetry(test),
684
+ ...testFinishInfo,
685
+ ...ctx.currentStore,
686
+ })
687
+ if (isFinalAttempt) {
688
+ test._ddIsFinalAttempt = true
689
+ }
683
690
  }
684
- testFinishCh.publish({
685
- status,
686
- hasBeenRetried: isMochaRetry(test),
687
- isLastRetry: getIsLastRetry(test),
688
- ...testFinishInfo,
689
- ...ctx.currentStore,
690
- })
691
+ if (test._retriedTest?._ddShouldWaitForHitProbe) {
692
+ if (isFinalAttempt) {
693
+ finalAttemptHandlers?.onStart?.(test)
694
+ }
695
+ test._ddDeferredHookEnd = {
696
+ waitForHitProbePromise: waitForHitProbe(),
697
+ publishTestFinish,
698
+ onFinish: isFinalAttempt ? () => finalAttemptHandlers?.onFinish?.(test) : undefined,
699
+ }
700
+ return
701
+ }
702
+ publishTestFinish()
691
703
  }
692
704
  }
693
705
  }
694
706
  }
695
707
  }
696
708
 
709
+ function finishDeferredHookEnd (test) {
710
+ const deferredHookEnd = test?._ddDeferredHookEnd
711
+ if (!deferredHookEnd) return
712
+
713
+ const finish = () => {
714
+ try {
715
+ return deferredHookEnd.publishTestFinish()
716
+ } finally {
717
+ deferredHookEnd.onFinish?.()
718
+ }
719
+ }
720
+
721
+ delete test._ddDeferredHookEnd
722
+ if (!deferredHookEnd.waitForHitProbePromise) return finish()
723
+
724
+ return deferredHookEnd.waitForHitProbePromise.then(
725
+ finish,
726
+ finish
727
+ )
728
+ }
729
+
730
+ /**
731
+ * Runs a Failed Test Replay hookUp callback after pending DI operations that must happen first.
732
+ *
733
+ * @param {(...args: unknown[]) => unknown} fn - Original hookUp completion callback.
734
+ * @param {object} test - Mocha test currently owning the hook.
735
+ * @param {Promise<void>|undefined} failedTestReplayPromise - Pending Failed Test Replay wait, if any.
736
+ * @param {unknown} hookThis - Callback receiver.
737
+ * @param {IArguments} args - Arguments passed by Mocha.
738
+ * @returns {unknown}
739
+ */
740
+ function runFailedTestReplayHookUpCallback (fn, test, failedTestReplayPromise, hookThis, args) {
741
+ const continueAfterProbe = () => {
742
+ const deferredHookEndPromise = finishDeferredHookEnd(test)
743
+ if (deferredHookEndPromise) {
744
+ return deferredHookEndPromise.then(() => fn.apply(hookThis, args), () => fn.apply(hookThis, args))
745
+ }
746
+ return fn.apply(hookThis, args)
747
+ }
748
+
749
+ if (failedTestReplayPromise) {
750
+ return failedTestReplayPromise.then(continueAfterProbe, continueAfterProbe)
751
+ }
752
+ return continueAfterProbe()
753
+ }
754
+
755
+ /**
756
+ * Wraps Mocha's hookUp completion callback so retries wait for DI before continuing.
757
+ *
758
+ * @param {(...args: unknown[]) => unknown} fn - Original hookUp completion callback.
759
+ * @param {object} test - Mocha test currently owning the hook.
760
+ * @param {Promise<void>|undefined} failedTestReplayPromise - Pending Failed Test Replay wait, if any.
761
+ * @returns {(...args: unknown[]) => unknown}
762
+ */
763
+ function wrapFailedTestReplayHookUpCallback (fn, test, failedTestReplayPromise) {
764
+ return shimmer.wrapCallback(fn, fn => function () {
765
+ return runFailedTestReplayHookUpCallback(fn, test, failedTestReplayPromise, this, arguments)
766
+ })
767
+ }
768
+
769
+ const patchedFailedTestReplayHookUp = new WeakSet()
770
+
771
+ function patchFailedTestReplayHookUp (Runner) {
772
+ if (patchedFailedTestReplayHookUp.has(Runner)) return
773
+
774
+ patchedFailedTestReplayHookUp.add(Runner)
775
+ shimmer.wrap(Runner.prototype, 'hookUp', hookUp => function (name, fn) {
776
+ const test = name === 'afterEach' && this.test
777
+ if (!test) {
778
+ return hookUp.apply(this, arguments)
779
+ }
780
+
781
+ const failedTestReplayPromise = test._ddFailedTestReplayPromise
782
+ if (failedTestReplayPromise) {
783
+ delete test._ddFailedTestReplayPromise
784
+ }
785
+
786
+ return hookUp.call(this, name, wrapFailedTestReplayHookUpCallback(fn, test, failedTestReplayPromise))
787
+ })
788
+ }
789
+
697
790
  function getOnFailHandler (isMain, config) {
698
791
  return function (testOrHook, err) {
699
792
  const testFile = testOrHook.file
@@ -764,14 +857,24 @@ function getOnTestRetryHandler (config) {
764
857
  config.isFlakyTestRetriesEnabled &&
765
858
  !test._ddIsAttemptToFix &&
766
859
  !test._ddIsEfdRetry
860
+ const promises = {}
767
861
  testRetryCh.publish({
768
862
  isFirstAttempt,
769
863
  err,
770
864
  willBeRetried,
771
865
  test,
772
866
  isAtrRetry,
867
+ promises,
773
868
  ...ctx.currentStore,
774
869
  })
870
+ if (promises.setProbePromise && promises.finishTestPromise) {
871
+ test._ddFailedTestReplayPromise = Promise.all([
872
+ promises.setProbePromise,
873
+ promises.finishTestPromise,
874
+ ]).then(() => {})
875
+ } else if (promises.setProbePromise || promises.finishTestPromise) {
876
+ test._ddFailedTestReplayPromise = promises.setProbePromise || promises.finishTestPromise
877
+ }
775
878
  }
776
879
  const key = getTestToContextKey(test)
777
880
  testToContext.delete(key)
@@ -901,6 +1004,9 @@ module.exports = {
901
1004
  getOnTestEndHandler,
902
1005
  getOnTestRetryHandler,
903
1006
  getOnHookEndHandler,
1007
+ finishDeferredHookEnd,
1008
+ wrapFailedTestReplayHookUpCallback,
1009
+ patchFailedTestReplayHookUp,
904
1010
  getOnFailHandler,
905
1011
  getOnPendingHandler,
906
1012
  testFileToSuiteCtx,
@@ -13,6 +13,7 @@ const {
13
13
  getOnPendingHandler,
14
14
  getOnTestRetryHandler,
15
15
  getRunTestsWrapper,
16
+ patchFailedTestReplayHookUp,
16
17
  } = require('./utils')
17
18
  require('./common')
18
19
 
@@ -22,6 +23,10 @@ const workerFinishCh = channel('ci:mocha:worker:finish')
22
23
 
23
24
  const config = {}
24
25
 
26
+ function isFailedTestReplayEnabled () {
27
+ return config.isTestDynamicInstrumentationEnabled && config.isDiEnabled
28
+ }
29
+
25
30
  addHook({
26
31
  name: 'mocha',
27
32
  versions: ['>=8.0.0'],
@@ -60,6 +65,11 @@ addHook({
60
65
  delete this.options._ddIsFlakyTestRetriesEnabled
61
66
  delete this.options._ddFlakyTestRetriesCount
62
67
  }
68
+ if (this.options._ddIsFailedTestReplayEnabled) {
69
+ config.isTestDynamicInstrumentationEnabled = true
70
+ config.isDiEnabled = true
71
+ delete this.options._ddIsFailedTestReplayEnabled
72
+ }
63
73
  return run.apply(this, args)
64
74
  })
65
75
 
@@ -78,6 +88,9 @@ addHook({
78
88
  if (!workerFinishCh.hasSubscribers) {
79
89
  return run.apply(this, args)
80
90
  }
91
+ if (isFailedTestReplayEnabled()) {
92
+ patchFailedTestReplayHookUp(Runner)
93
+ }
81
94
  // We flush when the worker ends with its test file (a mocha instance in a worker runs a single test file)
82
95
  this.once('end', () => {
83
96
  workerFinishCh.publish()
@@ -28,6 +28,10 @@ const collectionMethodsWithTwoFilters = [
28
28
 
29
29
  const startCh = channel('datadog:mongodb:collection:filter:start')
30
30
 
31
+ const bulkWriteStartCh = channel('apm:mongodb:bulkwrite:start')
32
+ const bulkWriteFinishCh = channel('apm:mongodb:bulkwrite:finish')
33
+ const bulkWriteErrorCh = channel('apm:mongodb:bulkwrite:error')
34
+
31
35
  addHook({ name: 'mongodb', versions: ['>=3.3 <5', '5', '>=6'] }, mongodb => {
32
36
  for (const methodName of [...collectionMethodsWithFilter, ...collectionMethodsWithTwoFilters]) {
33
37
  if (!(methodName in mongodb.Collection.prototype)) continue
@@ -55,5 +59,75 @@ addHook({ name: 'mongodb', versions: ['>=3.3 <5', '5', '>=6'] }, mongodb => {
55
59
  }
56
60
  })
57
61
  }
62
+
63
+ // `bulkWrite` fans out into separate `insert`/`update`/`delete` wire commands, so wrap
64
+ // it to open one parent span those per-type commands nest under as children.
65
+ if ('bulkWrite' in mongodb.Collection.prototype) {
66
+ shimmer.wrap(mongodb.Collection.prototype, 'bulkWrite', wrapBulkWrite)
67
+ }
68
+
58
69
  return mongodb
59
70
  })
71
+
72
+ /**
73
+ * @param {Function} bulkWrite
74
+ * @returns {Function}
75
+ */
76
+ function wrapBulkWrite (bulkWrite) {
77
+ return function (...args) {
78
+ /* istanbul ignore if: plugin stays subscribed for the whole suite; the disabled fast path is unreachable */
79
+ if (!bulkWriteStartCh.hasSubscribers) {
80
+ return bulkWrite.apply(this, args)
81
+ }
82
+
83
+ const ctx = { ns: this.namespace }
84
+
85
+ return bulkWriteStartCh.runStores(ctx, () => {
86
+ // Pre-v5 drivers take a trailing callback and return `undefined`; v5+ ignore any
87
+ // extra argument and always return a promise. Wrap a trailing callback for the
88
+ // legacy path and finish on the returned promise when there is one, so both APIs are
89
+ // covered without sniffing the version. Pre-v5 also validates arguments synchronously,
90
+ // so guard the call to finish the span instead of leaking it on a throw.
91
+ const lastIndex = args.length - 1
92
+ const callback = args[lastIndex]
93
+ if (typeof callback === 'function') {
94
+ args[lastIndex] = shimmer.wrapCallback(callback, callback => function (error) {
95
+ if (error) {
96
+ ctx.error = error
97
+ bulkWriteErrorCh.publish(ctx)
98
+ }
99
+ return bulkWriteFinishCh.runStores(ctx, callback, this, ...arguments)
100
+ })
101
+ }
102
+
103
+ let result
104
+ try {
105
+ result = bulkWrite.apply(this, args)
106
+ } catch (error) {
107
+ finishBulkWriteError(ctx, error)
108
+ throw error
109
+ }
110
+
111
+ if (result !== undefined && typeof result.then === 'function') {
112
+ result.then(function (value) {
113
+ ctx.result = value
114
+ bulkWriteFinishCh.publish(ctx)
115
+ }, function (error) {
116
+ finishBulkWriteError(ctx, error)
117
+ })
118
+ }
119
+
120
+ return result
121
+ })
122
+ }
123
+ }
124
+
125
+ /**
126
+ * @param {{ error?: unknown }} ctx
127
+ * @param {unknown} error
128
+ */
129
+ function finishBulkWriteError (ctx, error) {
130
+ ctx.error = error
131
+ bulkWriteErrorCh.publish(ctx)
132
+ bulkWriteFinishCh.publish(ctx)
133
+ }
@@ -6,6 +6,11 @@ const { wrapThen } = require('./helpers/promise')
6
6
 
7
7
  const startCh = channel('datadog:mongoose:model:filter:start')
8
8
  const finishCh = channel('datadog:mongoose:model:filter:finish')
9
+ // Bound around the deferred query execution. The `bindStore` transform returns a
10
+ // child store with the analysis marker set, covering the whole async scope that
11
+ // reaches the mongodb driver. `runStores` enters the child only for that scope
12
+ // and restores the parent on its own.
13
+ const execCh = channel('datadog:mongoose:model:filter:exec')
9
14
  // this channel is for wrapping the callback of exec methods and handling store context
10
15
  const execStartCh = channel('apm:mongoose:exec:start')
11
16
  const execFinishCh = channel('apm:mongoose:exec:finish')
@@ -103,7 +108,13 @@ addHook({
103
108
  return startCh.runStores(ctx, () => {
104
109
  wrapCallbackIfExist(args, ctx)
105
110
 
106
- const res = method.apply(this, args)
111
+ // A callback query executes synchronously inside `method.apply`, so the
112
+ // exec scope (and the deferred driver call it spawns) must be entered
113
+ // here. A promise query defers execution to a later `exec`/`then`, which
114
+ // is bound separately below.
115
+ const res = callbackWrapped
116
+ ? execCh.runStores(ctx, () => method.apply(this, args))
117
+ : method.apply(this, args)
107
118
 
108
119
  // if it is not callback, wrap exec method and its then
109
120
  if (!callbackWrapped) {
@@ -113,7 +124,7 @@ addHook({
113
124
  wrapCallbackIfExist(args, ctx)
114
125
  }
115
126
 
116
- const execResult = originalExec.apply(this, args)
127
+ const execResult = execCh.runStores(ctx, () => originalExec.apply(this, args))
117
128
 
118
129
  if (callbackWrapped || typeof execResult?.then !== 'function') {
119
130
  return execResult
@@ -840,6 +840,8 @@ function testEndHandler ({
840
840
  test._ddHasFailedAllRetries = true
841
841
  }
842
842
 
843
+ const willRetry = testWillRetry(test, testStatus)
844
+
843
845
  // this handles tests that do not go through the worker process (because they're skipped)
844
846
  if (shouldCreateTestSpan) {
845
847
  const testResult = results.at(-1)
@@ -850,7 +852,7 @@ function testEndHandler ({
850
852
  !test._ddIsEfdRetry
851
853
 
852
854
  const finalStatus = getFinalStatus({
853
- isFinalExecution: !testWillRetry(test, testStatus),
855
+ isFinalExecution: !willRetry,
854
856
  isDisabled: test._ddIsDisabled,
855
857
  isQuarantined: test._ddIsQuarantined,
856
858
  isAtrRetry,
@@ -888,17 +890,19 @@ function testEndHandler ({
888
890
  }
889
891
  }
890
892
 
891
- if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
892
- testSuiteToTestStatuses.get(testSuiteAbsolutePath).push(testStatus)
893
- } else {
894
- testSuiteToTestStatuses.set(testSuiteAbsolutePath, [testStatus])
895
- }
893
+ if (!willRetry) {
894
+ if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
895
+ testSuiteToTestStatuses.get(testSuiteAbsolutePath).push(testStatus)
896
+ } else {
897
+ testSuiteToTestStatuses.set(testSuiteAbsolutePath, [testStatus])
898
+ }
896
899
 
897
- if (error) {
898
- addErrorToTestSuite(testSuiteAbsolutePath, error)
900
+ if (error) {
901
+ addErrorToTestSuite(testSuiteAbsolutePath, error)
902
+ }
899
903
  }
900
904
 
901
- if (!testWillRetry(test, testStatus)) {
905
+ if (!willRetry) {
902
906
  remainingTestsByFile[testSuiteAbsolutePath] = remainingTestsByFile[testSuiteAbsolutePath]
903
907
  .filter(currentTest => currentTest !== test)
904
908
  }