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
@@ -11,6 +11,7 @@ const {
11
11
  const {
12
12
  getTestSuitePath,
13
13
  DYNAMIC_NAME_RE,
14
+ getFailedTestReplayPromise,
14
15
  recordTestManagementExecution,
15
16
  recordAttemptToFixExecution,
16
17
  logAttemptToFixTestExecution,
@@ -34,9 +35,13 @@ const testSuiteErrorCh = channel('ci:mocha:test-suite:error')
34
35
  /** @typedef {{ length: number, [index: number]: unknown } & Iterable<unknown>} ArgumentsLike */
35
36
 
36
37
  const testToContext = new WeakMap()
38
+ const originalEfdFns = new WeakMap()
37
39
  const originalFns = new WeakMap()
40
+ const originalPendingByTest = new WeakMap()
41
+ const originalRetriesByTest = new WeakMap()
38
42
  const testToStartLine = new WeakMap()
39
43
  const testFileToSuiteCtx = new Map()
44
+ const datadogRetryOriginals = new WeakMap()
40
45
  const wrappedFunctions = new WeakSet()
41
46
  const newTests = {}
42
47
  const efdTests = {}
@@ -168,6 +173,7 @@ function wrapOriginalEfdTest (test, retryPolicy) {
168
173
  }
169
174
  test._ddEfdDurationWrapped = true
170
175
  const originalFn = test.fn
176
+ originalEfdFns.set(test, originalFn)
171
177
  test.fn = shimmer.wrapFunction(originalFn, originalFn => function () {
172
178
  const start = performance.now()
173
179
  const recordDuration = () => {
@@ -264,6 +270,7 @@ function isEarlyFlakeDetectionTest (test, config) {
264
270
  function retryTest (test, numRetries, tags, retryPolicy) {
265
271
  const suite = test.parent
266
272
  const isEfdRetry = tags.includes('_ddIsEfdRetry')
273
+ if (!originalRetriesByTest.has(test)) originalRetriesByTest.set(test, test.retries())
267
274
  disableMochaRetries(test)
268
275
  if (isEfdRetry) {
269
276
  wrapOriginalEfdTest(test, retryPolicy)
@@ -271,6 +278,7 @@ function retryTest (test, numRetries, tags, retryPolicy) {
271
278
  for (let retryIndex = 0; retryIndex < numRetries; retryIndex++) {
272
279
  const clonedTest = test.clone()
273
280
  disableMochaRetries(clonedTest)
281
+ datadogRetryOriginals.set(clonedTest, test)
274
282
  suite.addTest(clonedTest)
275
283
  if (isEfdRetry) {
276
284
  clonedTest._ddEfdRetryIndex = retryIndex + 1
@@ -294,6 +302,91 @@ function retryTest (test, numRetries, tags, retryPolicy) {
294
302
  }
295
303
  }
296
304
 
305
+ /**
306
+ * Restores a runnable function wrapped with its Test Optimization context.
307
+ *
308
+ * @param {import('mocha').Runnable} runnable
309
+ * @returns {void}
310
+ */
311
+ function restoreRunnableFunction (runnable) {
312
+ const wrappedFunction = runnable.fn
313
+ if (!wrappedFunctions.has(wrappedFunction)) return
314
+
315
+ runnable.fn = originalFns.get(wrappedFunction)
316
+ }
317
+
318
+ /**
319
+ * Restores a test function wrapped to measure its EFD duration.
320
+ *
321
+ * @param {import('mocha').Test} test
322
+ * @returns {void}
323
+ */
324
+ function restoreEfdTestFunction (test) {
325
+ if (!originalEfdFns.has(test)) return
326
+
327
+ test.fn = originalEfdFns.get(test)
328
+ originalEfdFns.delete(test)
329
+ delete test._ddEfdDurationWrapped
330
+ }
331
+
332
+ /**
333
+ * Clears state that belongs to one Mocha runner execution and removes retry clones from prior executions.
334
+ *
335
+ * @param {import('mocha').Suite} rootSuite
336
+ * @returns {void}
337
+ */
338
+ function resetRunState (rootSuite) {
339
+ for (const key of Object.keys(newTests)) delete newTests[key]
340
+ for (const key of Object.keys(efdTests)) delete efdTests[key]
341
+ newTestsWithDynamicNames.clear()
342
+ testsAttemptToFix.clear()
343
+ testsQuarantined.clear()
344
+ testsStatuses.clear()
345
+ efdRetryCountByTestFullName.clear()
346
+ efdSlowAbortedTests.clear()
347
+ attemptToFixExecutions.clear()
348
+ loggedAttemptToFixTests.clear()
349
+
350
+ const suites = [rootSuite]
351
+ while (suites.length) {
352
+ const suite = suites.pop()
353
+ const originalTests = []
354
+ const retainedTests = new Set()
355
+ for (const runnable of suite.tests) {
356
+ const test = datadogRetryOriginals.get(runnable) || runnable
357
+ if (retainedTests.has(test)) continue
358
+ retainedTests.add(test)
359
+
360
+ restoreRunnableFunction(test)
361
+ restoreEfdTestFunction(test)
362
+ if (originalPendingByTest.has(test)) {
363
+ test.pending = originalPendingByTest.get(test)
364
+ originalPendingByTest.delete(test)
365
+ }
366
+ if (originalRetriesByTest.has(test)) {
367
+ test.retries(originalRetriesByTest.get(test))
368
+ originalRetriesByTest.delete(test)
369
+ }
370
+
371
+ delete test._ddIsAttemptToFix
372
+ delete test._ddIsDisabled
373
+ delete test._ddIsQuarantined
374
+ delete test._ddIsModified
375
+ delete test._ddIsNew
376
+ delete test._ddShouldSkipEfdRetry
377
+ delete test._ddTestFinishStarted
378
+ delete test._ddTestFinishPublished
379
+ delete test._ddIsFinalAttempt
380
+ delete test._ddHookFailed
381
+ delete test._ddReporterStartFailed
382
+ delete test._ddReporterTerminalFailed
383
+ originalTests.push(test)
384
+ }
385
+ suite.tests = originalTests
386
+ suites.push(...suite.suites)
387
+ }
388
+ }
389
+
297
390
  function getSuitesByTestFile (root) {
298
391
  const suitesByTestFile = {}
299
392
  function getSuites (suite) {
@@ -370,6 +463,19 @@ function getTestContext (test) {
370
463
  return testToContext.get(key)
371
464
  }
372
465
 
466
+ /**
467
+ * Claims publication of a test finish event across Mocha's terminal event paths.
468
+ *
469
+ * @param {object} test
470
+ * @returns {boolean}
471
+ */
472
+ function startTestFinish (test) {
473
+ if (test._ddTestFinishStarted || test._ddTestFinishPublished) return false
474
+
475
+ test._ddTestFinishStarted = true
476
+ return true
477
+ }
478
+
373
479
  /**
374
480
  * Copies Test Management metadata from Mocha's original runnable to its native retry clone.
375
481
  * @param {{
@@ -412,11 +518,7 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
412
518
  const test = isTestHook ? this.ctx.currentTest : this
413
519
 
414
520
  // we restore the original user defined function
415
- if (wrappedFunctions.has(this.fn)) {
416
- const originalFn = originalFns.get(this.fn)
417
- this.fn = originalFn
418
- wrappedFunctions.delete(this.fn)
419
- }
521
+ restoreRunnableFunction(this)
420
522
 
421
523
  if (isDatadogManagedRetryTest(test, libraryConfig)) {
422
524
  disableMochaRetries(this)
@@ -459,11 +561,7 @@ function getOnTestHandler (isMain) {
459
561
 
460
562
  // This may be a retry. If this is the case, `test.fn` is already wrapped,
461
563
  // so we need to restore it.
462
- if (wrappedFunctions.has(test.fn)) {
463
- const originalFn = originalFns.get(test.fn)
464
- test.fn = originalFn
465
- wrappedFunctions.delete(test.fn)
466
- }
564
+ restoreRunnableFunction(test)
467
565
 
468
566
  inheritDatadogPropertiesFromRetriedTest(test)
469
567
 
@@ -526,6 +624,9 @@ function getOnTestHandler (isMain) {
526
624
  }
527
625
 
528
626
  if (!isAttemptToFix && isDisabled) {
627
+ if (!originalPendingByTest.has(test)) {
628
+ originalPendingByTest.set(test, test.pending)
629
+ }
529
630
  test.pending = true
530
631
  }
531
632
 
@@ -700,9 +801,16 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
700
801
  if (test._ddShouldSkipEfdRetry) {
701
802
  return
702
803
  }
804
+ const shouldWaitForHitProbe = test._retriedTest?._ddShouldWaitForHitProbe
703
805
  const ctx = getTestContext(test)
704
806
  const status = getTestStatus(test)
705
- const shouldFinishTest = ctx && (!getAfterEachHooks(test).length || (test._ddIsDisabled && !test._ddIsAttemptToFix))
807
+ const shouldFinishTest = ctx && (
808
+ test._ddReporterStartFailed ||
809
+ test._ddReporterTerminalFailed ||
810
+ !getAfterEachHooks(test).length ||
811
+ (test._ddIsDisabled && !test._ddIsAttemptToFix)
812
+ )
813
+ if (shouldFinishTest && !startTestFinish(test)) return
706
814
  let testFinishInfo
707
815
  let isFinalAttempt = false
708
816
 
@@ -728,11 +836,12 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
728
836
  finalAttemptHandlers?.onStart?.(test)
729
837
  }
730
838
 
731
- if (test._retriedTest?._ddShouldWaitForHitProbe) {
839
+ if (shouldWaitForHitProbe) {
732
840
  await waitForHitProbe()
733
841
  }
734
842
 
735
843
  if (shouldFinishTest) {
844
+ test._ddTestFinishPublished = true
736
845
  testFinishCh.publish({
737
846
  status,
738
847
  hasBeenRetried: isMochaRetry(test),
@@ -759,10 +868,11 @@ function getOnHookEndHandler (config, finalAttemptHandlers) {
759
868
  const ctx = getTestContext(test)
760
869
  // Disabled tests are already finished in getOnTestEndHandler,
761
870
  // skip to avoid double-publishing
762
- if (ctx && (!test._ddIsDisabled || test._ddIsAttemptToFix)) {
871
+ if (ctx && (!test._ddIsDisabled || test._ddIsAttemptToFix) && startTestFinish(test)) {
763
872
  const testFinishInfo = getTestFinishInfo(test, status, config, ctx.err || test.err)
764
873
  const isFinalAttempt = testFinishInfo.finalStatus !== undefined
765
874
  const publishTestFinish = () => {
875
+ test._ddTestFinishPublished = true
766
876
  testFinishCh.publish({
767
877
  status,
768
878
  hasBeenRetried: isMochaRetry(test),
@@ -886,11 +996,13 @@ function getOnFailHandler (isMain, config) {
886
996
  testContext = getTestContext(test)
887
997
  }
888
998
  if (testContext) {
889
- if (isHook) {
890
- err.message = `${testOrHook.fullTitle()}: ${err.message}`
891
- testContext.err = err
999
+ if (isHook && startTestFinish(test)) {
1000
+ const hookError = new Error(`${testOrHook.fullTitle()}: ${err.message}`, { cause: err })
1001
+ hookError.name = err.name
1002
+ hookError.stack = err.stack
1003
+ testContext.err = hookError
892
1004
  errorCh.runStores(testContext, () => {})
893
- const testFinishInfo = getTestFinishInfo(test, 'fail', config, err)
1005
+ const testFinishInfo = getTestFinishInfo(test, 'fail', config, hookError)
894
1006
  // ATR never retries hook failures: this.retries(N) is set in runnableWrapper
895
1007
  // which only runs when the test function executes — hooks bypass that path,
896
1008
  // so _retries stays at -1 and getIsLastRetry returns false, leaving finalStatus
@@ -905,6 +1017,7 @@ function getOnFailHandler (isMain, config) {
905
1017
  // test.state is never set to 'failed' for hook failures (Mocha marks the hook,
906
1018
  // not the test). Flag it so finishRootSuiteForFile can compute the correct status.
907
1019
  test._ddHookFailed = true
1020
+ test._ddTestFinishPublished = true
908
1021
  testFinishCh.publish({
909
1022
  status: 'fail',
910
1023
  hasBeenRetried: isMochaRetry(test),
@@ -953,14 +1066,7 @@ function getOnTestRetryHandler (config) {
953
1066
  promises,
954
1067
  ...ctx.currentStore,
955
1068
  })
956
- if (promises.setProbePromise && promises.finishTestPromise) {
957
- test._ddFailedTestReplayPromise = Promise.all([
958
- promises.setProbePromise,
959
- promises.finishTestPromise,
960
- ]).then(() => {})
961
- } else if (promises.setProbePromise || promises.finishTestPromise) {
962
- test._ddFailedTestReplayPromise = promises.setProbePromise || promises.finishTestPromise
963
- }
1069
+ test._ddFailedTestReplayPromise = getFailedTestReplayPromise(promises)
964
1070
  }
965
1071
  const key = getTestToContextKey(test)
966
1072
  testToContext.delete(key)
@@ -1094,6 +1200,7 @@ module.exports = {
1094
1200
  getOnPendingHandler,
1095
1201
  testFileToSuiteCtx,
1096
1202
  getRunTestsWrapper,
1203
+ resetRunState,
1097
1204
  newTests,
1098
1205
  efdTests,
1099
1206
  newTestsWithDynamicNames,
@@ -2,6 +2,13 @@
2
2
 
3
3
  const shimmer = require('../../datadog-shimmer')
4
4
  const { channel, addHook } = require('./helpers/instrument')
5
+ const {
6
+ dispatchesAcquireSynchronously,
7
+ takePoolWaitTime,
8
+ wrapPoolClusterQueryMethod,
9
+ wrapPoolGetConnection,
10
+ wrapPoolQueryMethod,
11
+ } = require('./helpers/pool-acquire')
5
12
 
6
13
  addHook({ name: 'mysql', file: 'lib/Connection.js', versions: ['>=2'] }, Connection => {
7
14
  const startCh = channel('apm:mysql:query:start')
@@ -17,6 +24,11 @@ addHook({ name: 'mysql', file: 'lib/Connection.js', versions: ['>=2'] }, Connect
17
24
  const conf = this.config
18
25
  const ctx = { sql, conf }
19
26
 
27
+ const waitTime = takePoolWaitTime(this)
28
+ if (waitTime !== undefined) {
29
+ ctx.poolWaitTime = waitTime
30
+ }
31
+
20
32
  return startCh.runStores(ctx, () => {
21
33
  if (args[0].sql) {
22
34
  args[0].sql = ctx.sql
@@ -61,20 +73,24 @@ addHook({ name: 'mysql', file: 'lib/Pool.js', versions: ['>=2'] }, Pool => {
61
73
  const connectionFinishCh = channel('apm:mysql:connection:finish')
62
74
  const startPoolQueryCh = channel('datadog:mysql:pool:query:start')
63
75
  const finishPoolQueryCh = channel('datadog:mysql:pool:query:finish')
64
-
65
- shimmer.wrap(Pool.prototype, 'getConnection', getConnection => function (cb) {
66
- arguments[0] = function (...args) {
67
- return connectionFinishCh.runStores(ctx, cb, this, ...args)
68
- }
69
-
70
- const ctx = {}
71
-
72
- connectionStartCh.publish(ctx)
73
-
74
- return getConnection.apply(this, arguments)
75
- })
76
-
77
- shimmer.wrap(Pool.prototype, 'query', query => function (...args) {
76
+ const acquireStartCh = channel('apm:mysql:pool:acquire:start')
77
+ const acquireFinishCh = channel('apm:mysql:pool:acquire:finish')
78
+ const deferredPoolAcquire = !dispatchesAcquireSynchronously(
79
+ Pool.prototype.query,
80
+ Object.assign(Object.create(Pool.prototype), { config: { connectionConfig: {} } }),
81
+ 'getConnection',
82
+ ['SELECT 1', () => {}]
83
+ )
84
+
85
+ shimmer.wrap(Pool.prototype, 'getConnection', getConnection => wrapPoolGetConnection(getConnection, {
86
+ connectionStartCh,
87
+ connectionFinishCh,
88
+ acquireStartCh,
89
+ acquireFinishCh,
90
+ reentersQueuedCallbacks: true,
91
+ }, deferredPoolAcquire))
92
+
93
+ shimmer.wrap(Pool.prototype, 'query', query => wrapPoolQueryMethod(function (...args) {
78
94
  if (!startPoolQueryCh.hasSubscribers) {
79
95
  return query.apply(this, args)
80
96
  }
@@ -99,7 +115,21 @@ addHook({ name: 'mysql', file: 'lib/Pool.js', versions: ['>=2'] }, Pool => {
99
115
 
100
116
  return retval
101
117
  })
102
- })
118
+ }, connectionStartCh, deferredPoolAcquire))
103
119
 
104
120
  return Pool
105
121
  })
122
+
123
+ // A `canRetry` failover re-invokes `query` with the same Query object. Key the acquire state by that
124
+ // object so a retry remains part of the query even if a future mysql version defers it.
125
+ addHook({ name: 'mysql', file: 'lib/PoolNamespace.js', versions: ['>=2'] }, PoolNamespace => {
126
+ const channels = {
127
+ connectionStartCh: channel('apm:mysql:connection:start'),
128
+ connectionFinishCh: channel('apm:mysql:connection:finish'),
129
+ acquireStartCh: channel('apm:mysql:pool:acquire:start'),
130
+ acquireFinishCh: channel('apm:mysql:pool:acquire:finish'),
131
+ }
132
+ shimmer.wrap(PoolNamespace.prototype, 'query', query => wrapPoolClusterQueryMethod(query, channels))
133
+
134
+ return PoolNamespace
135
+ })
@@ -5,10 +5,55 @@ const { errorMonitor } = require('node:events')
5
5
  const shimmer = require('../../datadog-shimmer')
6
6
  const satisfies = require('../../../vendor/dist/semifies')
7
7
  const { channel, addHook } = require('./helpers/instrument')
8
+ const {
9
+ dispatchesAcquireSynchronously,
10
+ takePoolWaitTime,
11
+ wrapPoolClusterGetConnection,
12
+ wrapPoolClusterMethod,
13
+ wrapPoolGetConnection,
14
+ wrapPoolQueryMethod,
15
+ } = require('./helpers/pool-acquire')
8
16
 
9
17
  /** @type {WeakMap<object, Function>} */
10
18
  const wrappedOnResult = new WeakMap()
11
19
 
20
+ /** @type {WeakSet<object>} */
21
+ const flagWrappedNamespaces = new WeakSet()
22
+
23
+ const connectionStartCh = channel('apm:mysql2:connection:start')
24
+ const connectionFinishCh = channel('apm:mysql2:connection:finish')
25
+ const acquireStartCh = channel('apm:mysql2:pool:acquire:start')
26
+ const acquireFinishCh = channel('apm:mysql2:pool:acquire:finish')
27
+ const poolAcquireChannels = {
28
+ connectionStartCh,
29
+ connectionFinishCh,
30
+ acquireStartCh,
31
+ acquireFinishCh,
32
+ }
33
+
34
+ /**
35
+ * Bracket a pool-cluster namespace's internal acquires and key retry state by its stable callback.
36
+ * @param {{ query?: Function, execute?: Function, getConnection?: Function }} poolNamespace
37
+ */
38
+ function flagWrapNamespace (poolNamespace) {
39
+ if (poolNamespace == null || flagWrappedNamespaces.has(poolNamespace)) return
40
+ flagWrappedNamespaces.add(poolNamespace)
41
+
42
+ if (typeof poolNamespace.query === 'function') {
43
+ shimmer.wrap(poolNamespace, 'query', query => wrapPoolClusterMethod(query, connectionStartCh))
44
+ }
45
+ if (typeof poolNamespace.execute === 'function') {
46
+ shimmer.wrap(poolNamespace, 'execute', execute => wrapPoolClusterMethod(execute, connectionStartCh))
47
+ }
48
+ if (typeof poolNamespace.getConnection === 'function') {
49
+ shimmer.wrap(
50
+ poolNamespace,
51
+ 'getConnection',
52
+ getConnection => wrapPoolClusterGetConnection(getConnection, poolAcquireChannels)
53
+ )
54
+ }
55
+ }
56
+
12
57
  /**
13
58
  * @param {unknown} sql
14
59
  * @returns {string|undefined}
@@ -43,6 +88,11 @@ function wrapConnection (Connection, version) {
43
88
  const ctx = {}
44
89
 
45
90
  if (isQuery) {
91
+ const waitTime = takePoolWaitTime(this)
92
+ if (waitTime !== undefined) {
93
+ ctx.poolWaitTime = waitTime
94
+ }
95
+
46
96
  command.execute = wrapExecute(command, command.execute, ctx, this.config)
47
97
 
48
98
  return commandAddCh.runStores(ctx, addCommand, this, ...arguments)
@@ -212,22 +262,41 @@ function wrapConnection (Connection, version) {
212
262
  * by the queued query attach to the caller rather than the previous query that freed the connection.
213
263
  *
214
264
  * @param {Function} Pool
265
+ * @param {boolean} deferredPoolAcquire
215
266
  * @returns {Function}
216
267
  */
217
- function wrapGetConnection (Pool) {
218
- const connectionStartCh = channel('apm:mysql2:connection:start')
219
- const connectionFinishCh = channel('apm:mysql2:connection:finish')
268
+ function wrapGetConnection (Pool, deferredPoolAcquire) {
269
+ shimmer.wrap(
270
+ Pool.prototype,
271
+ 'getConnection',
272
+ getConnection => wrapPoolGetConnection(getConnection, poolAcquireChannels, deferredPoolAcquire)
273
+ )
220
274
 
221
- shimmer.wrap(Pool.prototype, 'getConnection', getConnection => function (cb) {
222
- const ctx = {}
223
- arguments[0] = function (...args) {
224
- return connectionFinishCh.runStores(ctx, cb, this, ...args)
225
- }
226
-
227
- connectionStartCh.publish(ctx)
275
+ return Pool
276
+ }
228
277
 
229
- return getConnection.apply(this, arguments)
230
- })
278
+ /**
279
+ * mysql2 >=3.11.5 keeps the outer-query abort at the connection level, so the pool only needs the
280
+ * acquire wrap plus the pool-query flag around `query` / `execute`.
281
+ *
282
+ * @param {Function} Pool
283
+ * @returns {Function}
284
+ */
285
+ function wrapBasePool (Pool) {
286
+ const deferredQueryAcquire = defersPoolAcquire(Pool, 'query')
287
+ const deferredExecuteAcquire = defersPoolAcquire(Pool, 'execute')
288
+ wrapGetConnection(Pool, deferredQueryAcquire || deferredExecuteAcquire)
289
+
290
+ shimmer.wrap(
291
+ Pool.prototype,
292
+ 'query',
293
+ query => wrapPoolQueryMethod(query, connectionStartCh, deferredQueryAcquire)
294
+ )
295
+ shimmer.wrap(
296
+ Pool.prototype,
297
+ 'execute',
298
+ execute => wrapPoolQueryMethod(execute, connectionStartCh, deferredExecuteAcquire)
299
+ )
231
300
 
232
301
  return Pool
233
302
  }
@@ -240,10 +309,12 @@ function wrapGetConnection (Pool) {
240
309
  function wrapPool (Pool, version) {
241
310
  const startOuterQueryCh = channel('datadog:mysql2:outerquery:start')
242
311
  const shouldEmitEndAfterQueryAbort = satisfies(version, '>=1.3.3')
312
+ const deferredQueryAcquire = defersPoolAcquire(Pool, 'query')
313
+ const deferredExecuteAcquire = defersPoolAcquire(Pool, 'execute')
243
314
 
244
- wrapGetConnection(Pool)
315
+ wrapGetConnection(Pool, deferredQueryAcquire || deferredExecuteAcquire)
245
316
 
246
- shimmer.wrap(Pool.prototype, 'query', query => function (sql, values, cb) {
317
+ shimmer.wrap(Pool.prototype, 'query', query => wrapPoolQueryMethod(function (sql, values, cb) {
247
318
  if (!startOuterQueryCh.hasSubscribers) return query.apply(this, arguments)
248
319
 
249
320
  const resolvedSql = resolveSqlString(sql)
@@ -279,9 +350,9 @@ function wrapPool (Pool, version) {
279
350
  }
280
351
 
281
352
  return query.apply(this, arguments)
282
- })
353
+ }, connectionStartCh, deferredQueryAcquire))
283
354
 
284
- shimmer.wrap(Pool.prototype, 'execute', execute => function (sql, values, cb) {
355
+ shimmer.wrap(Pool.prototype, 'execute', execute => wrapPoolQueryMethod(function (sql, values, cb) {
285
356
  if (!startOuterQueryCh.hasSubscribers) return execute.apply(this, arguments)
286
357
 
287
358
  const resolvedSql = resolveSqlString(sql)
@@ -304,11 +375,25 @@ function wrapPool (Pool, version) {
304
375
  }
305
376
 
306
377
  return execute.apply(this, arguments)
307
- })
378
+ }, connectionStartCh, deferredExecuteAcquire))
308
379
 
309
380
  return Pool
310
381
  }
311
382
 
383
+ /**
384
+ * @param {Function} Pool
385
+ * @param {'query'|'execute'} method
386
+ * @returns {boolean}
387
+ */
388
+ function defersPoolAcquire (Pool, method) {
389
+ return !dispatchesAcquireSynchronously(
390
+ Pool.prototype[method],
391
+ Object.assign(Object.create(Pool.prototype), { config: { connectionConfig: {} } }),
392
+ 'getConnection',
393
+ ['SELECT 1', () => {}]
394
+ )
395
+ }
396
+
312
397
  /**
313
398
  * @param {Function} PoolCluster
314
399
  * @returns {Function}
@@ -319,6 +404,7 @@ function wrapPoolCluster (PoolCluster) {
319
404
 
320
405
  shimmer.wrap(PoolCluster.prototype, 'of', of => function (...args) {
321
406
  const poolNamespace = of.apply(this, args)
407
+ flagWrapNamespace(poolNamespace)
322
408
 
323
409
  if (startOuterQueryCh.hasSubscribers && !wrappedPoolNamespaces.has(poolNamespace)) {
324
410
  shimmer.wrap(poolNamespace, 'query', query => function (sql, values, cb) {
@@ -404,7 +490,7 @@ addHook(
404
490
  // mysql2 >=3.11.5 moved the pool onto BasePool in lib/base/pool.js.
405
491
  addHook(
406
492
  { name: 'mysql2', file: 'lib/base/pool.js', versions: ['>=3.11.5'] },
407
- /** @type {(moduleExports: unknown, version: string) => unknown} */ (wrapGetConnection)
493
+ /** @type {(moduleExports: unknown, version: string) => unknown} */ (wrapBasePool)
408
494
  )
409
495
 
410
496
  // PoolNamespace.prototype.query does not exist in mysql2<2.3.0
@@ -412,3 +498,18 @@ addHook(
412
498
  { name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['2.3.0 - 3.11.4'] },
413
499
  /** @type {(moduleExports: unknown, version: string) => unknown} */ (wrapPoolCluster)
414
500
  )
501
+
502
+ // mysql2 >=3.11.5 keeps the outer-query abort at the connection level, so a cluster namespace only
503
+ // needs the pool-query flag so its internal query/execute acquire does not open an acquire span.
504
+ addHook(
505
+ { name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['>=3.11.5'] },
506
+ /** @type {(moduleExports: unknown, version: string) => unknown} */ (PoolCluster => {
507
+ shimmer.wrap(PoolCluster.prototype, 'of', of => function () {
508
+ const poolNamespace = of.apply(this, arguments)
509
+ flagWrapNamespace(poolNamespace)
510
+ return poolNamespace
511
+ })
512
+
513
+ return PoolCluster
514
+ })
515
+ )
@@ -5,6 +5,12 @@ const { addHook } = require('./helpers/instrument')
5
5
  /** @type {((pattern: string | RegExp) => RegExp | undefined) | undefined} */
6
6
  let compileToRegexp
7
7
 
8
+ /** @type {((pattern: string) => { tokens: object[] } | undefined) | undefined} */
9
+ let parseTokens
10
+
11
+ /** @type {((route: string) => ((url: string) => object | undefined) | undefined) | undefined} */
12
+ let makeMatcher
13
+
8
14
  addHook({ name: 'path-to-regexp', versions: ['*'] }, moduleExports => {
9
15
  // 0.1.x and 6.x: `module.exports = (path, ...) => RegExp`.
10
16
  // 7.x: `module.exports = { pathToRegexp(path, ...) => RegExp }`.
@@ -29,6 +35,47 @@ addHook({ name: 'path-to-regexp', versions: ['*'] }, moduleExports => {
29
35
  return moduleExports
30
36
  })
31
37
 
38
+ // 8.x only: earlier majors also expose `parse`/`match`, but with incompatible shapes — 7.x returns
39
+ // `{ tokens }` too, yet its tokens are bare strings rather than typed nodes. Gating on the version
40
+ // keeps those from installing adapters the consumers would misread.
41
+ addHook({ name: 'path-to-regexp', versions: ['>=8'] }, moduleExports => {
42
+ const { parse, match } = moduleExports
43
+
44
+ if (typeof parse === 'function' && typeof match === 'function') {
45
+ parseTokens = pattern => {
46
+ let result
47
+ try {
48
+ result = parse(pattern)
49
+ } catch {
50
+ return
51
+ }
52
+ if (Array.isArray(result?.tokens)) return result
53
+ }
54
+
55
+ makeMatcher = route => {
56
+ let matcher
57
+ try {
58
+ // Callers read which params matched, never their values: skipping decode is faster and
59
+ // stops a malformed escape ('%ZZ') from throwing URIError, which would read as "no match".
60
+ matcher = match(route, { decode: false })
61
+ } catch {
62
+ return
63
+ }
64
+ return url => {
65
+ let result
66
+ try {
67
+ result = matcher(url)
68
+ } catch {
69
+ return
70
+ }
71
+ return result ? result.params : undefined
72
+ }
73
+ }
74
+ }
75
+
76
+ return moduleExports
77
+ })
78
+
32
79
  /**
33
80
  * Returns whatever path-to-regexp compile adapter the host most recently
34
81
  * loaded. Capture this once at addHook fire time so each express/router
@@ -41,4 +88,23 @@ function getCompileToRegexp () {
41
88
  return compileToRegexp
42
89
  }
43
90
 
44
- module.exports = { getCompileToRegexp }
91
+ /**
92
+ * Returns the host's path-to-regexp `parse` adapter (8.x token tree), or `undefined` when the
93
+ * host has not loaded an 8.x path-to-regexp (e.g. Express 4 ships 0.1.x, which has no `parse`).
94
+ * @returns {((pattern: string) => { tokens: object[] } | undefined) | undefined}
95
+ */
96
+ function getParse () {
97
+ return parseTokens
98
+ }
99
+
100
+ /**
101
+ * Returns the host's path-to-regexp 8.x `match` adapter: a factory `route => (url => params)` that
102
+ * compiles a route once and returns a matcher yielding the captured params (or undefined on no
103
+ * match / unusable route). `undefined` when the host has not loaded an 8.x path-to-regexp.
104
+ * @returns {((route: string) => ((url: string) => object | undefined) | undefined) | undefined}
105
+ */
106
+ function getMatch () {
107
+ return makeMatcher
108
+ }
109
+
110
+ module.exports = { getCompileToRegexp, getParse, getMatch }