dd-trace 5.133.0 → 5.134.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 (55) hide show
  1. package/ci/vitest-no-worker-init-runner-setup.mjs +27 -0
  2. package/index.d.ts +16 -5
  3. package/package.json +17 -16
  4. package/packages/datadog-instrumentations/index.js +2 -1
  5. package/packages/datadog-instrumentations/src/cucumber.js +17 -8
  6. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +27 -8
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  8. package/packages/datadog-instrumentations/src/helpers/register.js +9 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/jest.js +12 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/vitest.js +8 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +5 -0
  13. package/packages/datadog-instrumentations/src/jest.js +69 -57
  14. package/packages/datadog-instrumentations/src/mocha/utils.js +35 -2
  15. package/packages/datadog-instrumentations/src/nova-sonic/session.js +25 -16
  16. package/packages/datadog-instrumentations/src/playwright.js +2 -0
  17. package/packages/datadog-instrumentations/src/stripe.js +23 -12
  18. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +7 -3
  20. package/packages/datadog-instrumentations/src/vitest-util.js +5 -2
  21. package/packages/datadog-instrumentations/src/vitest.js +8 -0
  22. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +3 -0
  23. package/packages/datadog-plugin-cypress/src/support.js +2 -0
  24. package/packages/dd-trace/src/aiguard/evaluation.js +143 -1
  25. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -1
  26. package/packages/dd-trace/src/appsec/stack_trace.js +3 -2
  27. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +3 -2
  28. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  29. package/packages/dd-trace/src/config/index.js +45 -0
  30. package/packages/dd-trace/src/config/supported-configurations.json +9 -0
  31. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +6 -5
  32. package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -5
  33. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +4 -6
  34. package/packages/dd-trace/src/debugger/unsupported-runtime.js +16 -0
  35. package/packages/dd-trace/src/llmobs/agent-manifest.js +19 -2
  36. package/packages/dd-trace/src/llmobs/constants/tags.js +3 -0
  37. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +5 -0
  38. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +9 -1
  39. package/packages/dd-trace/src/llmobs/sdk.js +15 -4
  40. package/packages/dd-trace/src/llmobs/span_processor.js +12 -0
  41. package/packages/dd-trace/src/llmobs/tagger.js +18 -8
  42. package/packages/dd-trace/src/llmobs/util.js +10 -4
  43. package/packages/dd-trace/src/openfeature/index.js +6 -0
  44. package/packages/dd-trace/src/openfeature/writers/util.js +4 -4
  45. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +18 -1
  46. package/packages/dd-trace/src/plugin_manager.js +1 -2
  47. package/packages/dd-trace/src/plugins/tracing.js +0 -1
  48. package/packages/dd-trace/src/plugins/util/git.js +91 -35
  49. package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -1
  50. package/packages/dd-trace/src/plugins/util/test.js +2 -0
  51. package/packages/dd-trace/src/plugins/util/web.js +90 -20
  52. package/packages/dd-trace/src/span_stats.js +4 -1
  53. package/packages/dd-trace/src/telemetry/logs/log-collector.js +1 -1
  54. package/packages/dd-trace/src/util.js +8 -4
  55. package/vendor/dist/@apm-js-collab/code-transformer/index.js +8 -8
@@ -64,7 +64,7 @@ const {
64
64
  getChannelPromise,
65
65
  publishWithCompletion,
66
66
  } = require('./helpers/channel')
67
- const { addHook, channel } = require('./helpers/instrument')
67
+ const { addHook, channel, getHooks, tracingChannel } = require('./helpers/instrument')
68
68
 
69
69
  const testSessionStartCh = channel('ci:jest:session:start')
70
70
  const testSessionFinishCh = channel('ci:jest:session:finish')
@@ -125,6 +125,7 @@ const jestSessionState = (globalThis[JEST_SESSION_STATE] ||= {})
125
125
 
126
126
  // https://github.com/jestjs/jest/blob/41f842a46bb2691f828c3a5f27fc1d6290495b82/packages/jest-circus/src/types.ts#L9C8-L9C54
127
127
  const RETRY_TIMES = Symbol.for('RETRY_TIMES')
128
+ const RETRY_TIMES_SETTER = Symbol.for('RETRY_TIMES_SETTER')
128
129
 
129
130
  let skippableSuites = []
130
131
  let skippableSuitesCoverage
@@ -195,6 +196,7 @@ const dynamicAtrResultHandlerRegistrations = new WeakMap()
195
196
  // Tests that are genuinely new (not in known tests list).
196
197
  const newTests = new Set()
197
198
  const testSuiteJestObjects = new Map()
199
+ const deferredTestSuiteFinishes = new Map()
198
200
  const testSuiteDatadogEnvironments = new Map()
199
201
  const wrappedJestGlobals = new WeakSet()
200
202
  const wrappedJestEsmLoaders = new WeakSet()
@@ -741,9 +743,11 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
741
743
  }
742
744
  }
743
745
 
746
+ this.hasAutomaticRetries = false
744
747
  if (this.isFlakyTestRetriesEnabled) {
745
748
  const currentNumRetries = this.global[RETRY_TIMES]
746
749
  if (!currentNumRetries) {
750
+ this.hasAutomaticRetries = true
747
751
  // When dynamic ATR is enabled, use the max bucket value as the initial count.
748
752
  // The actual duration-based count is computed per test after the first attempt.
749
753
  if (this.isDynamicAtrEnabled) {
@@ -2445,7 +2449,16 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
2445
2449
  }
2446
2450
 
2447
2451
  let isAtrRetry = false
2448
- if (this.isFlakyTestRetriesEnabled && event.test?.invocations > 1 && !isAttemptToFix && !isEfdRetry) {
2452
+ let hasAutomaticRetries = this.hasAutomaticRetries || dynamicAtrRetryCountByTest.has(event.test)
2453
+ // Describe retries suppress per-test retries throughout the block, including nested describes.
2454
+ for (let block = event.test.parent; block; block = block.parent) {
2455
+ if (state.describeRetryOptions?.has(block)) {
2456
+ hasAutomaticRetries = false
2457
+ break
2458
+ }
2459
+ }
2460
+ if (this.isFlakyTestRetriesEnabled && hasAutomaticRetries &&
2461
+ event.test?.invocations > 1 && !isAttemptToFix && !isEfdRetry) {
2449
2462
  isAtrRetry = true
2450
2463
  }
2451
2464
 
@@ -2652,19 +2665,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
2652
2665
  }
2653
2666
  }
2654
2667
  }
2655
- const clearActiveTestSuite = () => {
2656
- realSetTimeout(() => {
2657
- if (activeTestSuiteAbsolutePath === this.testSuiteAbsolutePath) {
2658
- activeTestSuiteAbsolutePath = undefined
2659
- }
2660
- }, 0)
2661
- }
2662
- const result = super.teardown()
2663
- if (result?.then) {
2664
- return result.finally(clearActiveTestSuite)
2665
- }
2666
- clearActiveTestSuite()
2667
- return result
2668
+ return super.teardown()
2668
2669
  }
2669
2670
  }
2670
2671
  }
@@ -3492,22 +3493,48 @@ function shouldWaitForTestSuiteFinish (environment) {
3492
3493
  return isJestWorker && environment.globalConfig?.workerIdleMemoryLimit !== undefined
3493
3494
  }
3494
3495
 
3496
+ /**
3497
+ * @param {string} testSuiteAbsolutePath
3498
+ */
3499
+ function finishTestSuiteAfterRun (testSuiteAbsolutePath) {
3500
+ const clearActiveTestSuite = () => {
3501
+ realSetTimeout(() => {
3502
+ if (activeTestSuiteAbsolutePath === testSuiteAbsolutePath) {
3503
+ activeTestSuiteAbsolutePath = undefined
3504
+ }
3505
+ }, 0)
3506
+ }
3507
+ const payload = deferredTestSuiteFinishes.get(testSuiteAbsolutePath)
3508
+ deferredTestSuiteFinishes.delete(testSuiteAbsolutePath)
3509
+ if (payload) {
3510
+ return getChannelPromise(testSuiteFinishCh, { ...payload, waitForFinish: true })
3511
+ .finally(clearActiveTestSuite)
3512
+ }
3513
+ return clearActiveTestSuite()
3514
+ }
3515
+
3516
+ tracingChannel('orchestrion:jest-runner:runTestInternal').end.subscribe((message) => {
3517
+ const ctx = /** @type {{ arguments: [string, ...unknown[]], result: Promise<unknown> }} */ (message)
3518
+ const [testSuiteAbsolutePath] = ctx.arguments
3519
+ // Jest's runner awaits the complete custom teardown before this promise settles.
3520
+ ctx.result = ctx.result.finally(() => finishTestSuiteAfterRun(testSuiteAbsolutePath))
3521
+ })
3522
+
3523
+ for (const hook of getHooks('jest-runner').values()) addHook(hook, moduleExports => moduleExports)
3524
+
3495
3525
  /**
3496
3526
  * @param {Record<string, unknown>} payload
3497
- * @param {boolean} waitForFinish
3527
+ * @param {InstanceType<ReturnType<typeof getWrappedEnvironment>>} environment
3498
3528
  */
3499
- function publishTestSuiteFinish (payload, waitForFinish) {
3529
+ function publishTestSuiteFinish (payload, environment) {
3500
3530
  if (!testSuiteFinishCh.hasSubscribers) return
3501
3531
 
3502
- if (!waitForFinish) {
3503
- testSuiteFinishCh.publish(payload)
3532
+ if (shouldWaitForTestSuiteFinish(environment)) {
3533
+ deferredTestSuiteFinishes.set(environment.testSuiteAbsolutePath, payload)
3504
3534
  return
3505
3535
  }
3506
3536
 
3507
- return getChannelPromise(testSuiteFinishCh, {
3508
- ...payload,
3509
- waitForFinish,
3510
- })
3537
+ testSuiteFinishCh.publish(payload)
3511
3538
  }
3512
3539
 
3513
3540
  function cleanupTestSuiteState (testSuiteAbsolutePath) {
@@ -3684,48 +3711,24 @@ function jestAdapterWrapper (jestAdapter, jestVersion, isIitm, hookMeta) {
3684
3711
  testSuiteAbsolutePath: environment.testSuiteAbsolutePath,
3685
3712
  })
3686
3713
  }
3687
- const waitForFinish = shouldWaitForTestSuiteFinish(environment)
3688
3714
  const finishPayload = {
3689
3715
  status,
3690
3716
  errorMessage,
3691
3717
  testSuiteAbsolutePath: environment.testSuiteAbsolutePath,
3692
3718
  }
3693
- if (waitForFinish) {
3694
- const finishPromise = publishTestSuiteFinish(finishPayload, waitForFinish)
3695
- if (finishPromise) {
3696
- return finishPromise.then(() => {
3697
- // Cleanup per-suite state to avoid memory leaks
3698
- cleanupTestSuiteState(environment.testSuiteAbsolutePath)
3699
-
3700
- return suiteResults
3701
- })
3702
- }
3703
- }
3704
- publishTestSuiteFinish(finishPayload, waitForFinish)
3719
+ publishTestSuiteFinish(finishPayload, environment)
3705
3720
 
3706
3721
  // Cleanup per-suite state to avoid memory leaks
3707
3722
  cleanupTestSuiteState(environment.testSuiteAbsolutePath)
3708
3723
 
3709
3724
  return suiteResults
3710
3725
  }).catch(error => {
3711
- const waitForFinish = shouldWaitForTestSuiteFinish(environment)
3712
3726
  const finishPayload = {
3713
3727
  status: 'fail',
3714
3728
  error,
3715
3729
  testSuiteAbsolutePath: environment.testSuiteAbsolutePath,
3716
3730
  }
3717
- if (waitForFinish) {
3718
- const finishPromise = publishTestSuiteFinish(finishPayload, waitForFinish)
3719
- if (finishPromise) {
3720
- return finishPromise.then(() => {
3721
- // Cleanup per-suite state to avoid memory leaks
3722
- cleanupTestSuiteState(environment.testSuiteAbsolutePath)
3723
-
3724
- throw error
3725
- })
3726
- }
3727
- }
3728
- publishTestSuiteFinish(finishPayload, waitForFinish)
3731
+ publishTestSuiteFinish(finishPayload, environment)
3729
3732
 
3730
3733
  // Cleanup per-suite state to avoid memory leaks
3731
3734
  cleanupTestSuiteState(environment.testSuiteAbsolutePath)
@@ -3978,12 +3981,11 @@ const LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE = new Set([
3978
3981
  'selenium-webdriver/chromium',
3979
3982
  ])
3980
3983
 
3981
- function recordMockedFile (suiteFilePath, moduleName) {
3984
+ function recordMockedFile (suiteFilePath, moduleName, from = suiteFilePath) {
3982
3985
  if (!suiteFilePath || typeof moduleName !== 'string') return
3983
3986
 
3984
3987
  const existingMockedFiles = testSuiteMockedFiles.get(suiteFilePath) || []
3985
- const suiteDir = path.dirname(suiteFilePath)
3986
- const mockPath = path.resolve(suiteDir, moduleName)
3988
+ const mockPath = path.resolve(path.dirname(from), moduleName)
3987
3989
  existingMockedFiles.push(mockPath)
3988
3990
  testSuiteMockedFiles.set(suiteFilePath, existingMockedFiles)
3989
3991
  }
@@ -4018,14 +4020,24 @@ function getMockedFiles (suiteFilePath) {
4018
4020
  return staticMockedFiles
4019
4021
  }
4020
4022
 
4021
- function wrapJestObject (jestObject, suiteFilePath) {
4023
+ function wrapJestObject (jestObject, suiteFilePath, from = suiteFilePath) {
4022
4024
  if (!jestObject || !suiteFilePath || wrappedJestObjects.has(jestObject)) return
4023
4025
 
4024
4026
  testSuiteJestObjects.set(suiteFilePath, jestObject)
4025
4027
  wrappedJestObjects.add(jestObject)
4026
4028
 
4029
+ // retryTimes is created per runtime, so wrap the returned public API object.
4030
+ shimmer.wrap(jestObject, 'retryTimes', retryTimes => function () {
4031
+ const environment = testSuiteDatadogEnvironments.get(suiteFilePath)
4032
+ // Older Jest versions ignore entireDescribe and still replace the global budget.
4033
+ if (environment && !(arguments[1]?.entireDescribe && environment.global[RETRY_TIMES_SETTER])) {
4034
+ environment.hasAutomaticRetries = false
4035
+ }
4036
+ return retryTimes.apply(this, arguments)
4037
+ })
4038
+
4027
4039
  shimmer.wrap(jestObject, 'mock', mock => function (moduleName) {
4028
- recordMockedFile(suiteFilePath, moduleName)
4040
+ recordMockedFile(suiteFilePath, moduleName, from)
4029
4041
  return mock.apply(this, arguments)
4030
4042
  })
4031
4043
  }
@@ -4039,7 +4051,7 @@ function wrapJestGlobalsForRuntime (runtime) {
4039
4051
  wrappedJestGlobals.add(jestGlobals)
4040
4052
  shimmer.wrap(jestGlobals, 'jestObjectFor', jestObjectFor => function (from) {
4041
4053
  const jestObject = jestObjectFor.apply(this, arguments)
4042
- wrapJestObject(jestObject, from)
4054
+ wrapJestObject(jestObject, runtime._testPath || from, from)
4043
4055
  return jestObject
4044
4056
  })
4045
4057
  }
@@ -4334,9 +4346,9 @@ addHook({
4334
4346
  if (typeof Runtime.prototype._createJestObjectFor === 'function') {
4335
4347
  shimmer.wrap(Runtime.prototype, '_createJestObjectFor', _createJestObjectFor => function (from) {
4336
4348
  const result = _createJestObjectFor.apply(this, arguments)
4337
- const suiteFilePath = this._testPath || from
4349
+ const suiteFilePath = this._testPath || this._environment?.testSuiteAbsolutePath || from
4338
4350
 
4339
- wrapJestObject(result, suiteFilePath)
4351
+ wrapJestObject(result, suiteFilePath, from)
4340
4352
  return result
4341
4353
  })
4342
4354
  }
@@ -43,6 +43,9 @@ const originalEfdFns = new WeakMap()
43
43
  const originalFns = new WeakMap()
44
44
  const originalPendingByTest = new WeakMap()
45
45
  const originalRetriesByTest = new WeakMap()
46
+ let automaticRetryTests = new WeakSet()
47
+ let automaticRetryAttempts = new WeakMap()
48
+ let nativeRetryTests = new WeakSet()
46
49
  const testToStartLine = new WeakMap()
47
50
  const testFileToSuiteCtx = new Map()
48
51
  const datadogRetryOriginals = new WeakMap()
@@ -346,6 +349,9 @@ function resetRunState (rootSuite) {
346
349
  efdRetryCountByTestFullName.clear()
347
350
  efdSlowAbortedTests.clear()
348
351
  suiteHookFailureSuites.clear()
352
+ automaticRetryTests = new WeakSet()
353
+ automaticRetryAttempts = new WeakMap()
354
+ nativeRetryTests = new WeakSet()
349
355
  dynamicAtrRetryCountByTest = new WeakMap()
350
356
  attemptToFixExecutions.clear()
351
357
  loggedAttemptToFixTests.clear()
@@ -423,7 +429,7 @@ function isMochaRetry (test) {
423
429
  }
424
430
 
425
431
  function getIsLastRetry (test) {
426
- return test._currentRetry === test._retries
432
+ return test._currentRetry >= test._retries
427
433
  }
428
434
 
429
435
  function getTestFullName (test) {
@@ -534,6 +540,18 @@ function inheritDatadogPropertiesFromRetriedTest (test) {
534
540
 
535
541
  function runnableWrapper (RunnablePackage, libraryConfig) {
536
542
  const Runnable = RunnablePackage.Runnable ?? RunnablePackage
543
+ // Mocha 12 can load its ESM class through require() without the ESM rewriter.
544
+ // Intercept the exported prototype to observe setters in both injection modes.
545
+ shimmer.wrap(Runnable.prototype, 'retries', retries => function () {
546
+ if (arguments.length && automaticRetryTests.delete(this)) {
547
+ const originalTest = this._retriedTest || this
548
+ // afterEach can change this runnable after Mocha has already copied its budget into a queued clone.
549
+ if (!automaticRetryAttempts.get(originalTest)?.has(this._currentRetry + 1)) {
550
+ nativeRetryTests.add(originalTest)
551
+ }
552
+ }
553
+ return retries.apply(this, arguments)
554
+ })
537
555
  shimmer.wrap(Runnable.prototype, 'run', run => function (...args) {
538
556
  if (!testFinishCh.hasSubscribers) {
539
557
  return run.apply(this, args)
@@ -559,11 +577,12 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
559
577
  return onRunnableFinished.apply(this, arguments)
560
578
  }
561
579
  }
562
- } else if (libraryConfig?.isFlakyTestRetriesEnabled) {
580
+ } else if (libraryConfig?.isFlakyTestRetriesEnabled && !nativeRetryTests.has(test._retriedTest || test)) {
563
581
  if (!originalRetriesByTest.has(test)) {
564
582
  const originalRetries = originalRetriesByTest.get(test._retriedTest) ?? test.retries()
565
583
  originalRetriesByTest.set(test, originalRetries)
566
584
  }
585
+ automaticRetryTests.delete(test)
567
586
  if (libraryConfig.isDynamicAtrEnabled) {
568
587
  // Dynamic ATR: set the max possible retries initially.
569
588
  // The actual duration-based count is computed after the first attempt.
@@ -581,6 +600,7 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
581
600
  } else {
582
601
  test.retries(libraryConfig.flakyTestRetriesCount)
583
602
  }
603
+ automaticRetryTests.add(test)
584
604
  }
585
605
 
586
606
  if (isTestHook || this.type === 'test') {
@@ -833,6 +853,7 @@ function getTestFinishInfo (test, status, config, error) {
833
853
 
834
854
  const isAttemptToFixRetry = test._ddIsAttemptToFix && testStatuses.length > 1
835
855
  const isAtrRetry = config.isFlakyTestRetriesEnabled &&
856
+ automaticRetryAttempts.get(test._retriedTest || test)?.has(test._currentRetry) &&
836
857
  !test._ddIsAttemptToFix &&
837
858
  !test._ddIsEfdRetry
838
859
  const isFinalAttempt = isTerminalHookFailure || status !== 'fail' || test._currentRetry >= test._retries
@@ -1178,9 +1199,20 @@ function getOnFailHandler (isMain, config) {
1178
1199
 
1179
1200
  function getOnTestRetryHandler (config) {
1180
1201
  return function (test, err) {
1202
+ if (automaticRetryTests.has(test)) {
1203
+ // Mocha has already queued the clone; later budget changes cannot change its scheduling owner.
1204
+ const originalTest = test._retriedTest || test
1205
+ let attempts = automaticRetryAttempts.get(originalTest)
1206
+ if (!attempts) {
1207
+ attempts = new Set()
1208
+ automaticRetryAttempts.set(originalTest, attempts)
1209
+ }
1210
+ attempts.add(test._currentRetry + 1)
1211
+ }
1181
1212
  const isFirstAttempt = test._currentRetry === 0
1182
1213
  const isDynamicAtrTest = config.isDynamicAtrEnabled &&
1183
1214
  config.isFlakyTestRetriesEnabled &&
1215
+ automaticRetryTests.has(test) &&
1184
1216
  !test._ddIsAttemptToFix &&
1185
1217
  !isEarlyFlakeDetectionTest(test, config)
1186
1218
  if (isDynamicAtrTest && isFirstAttempt) {
@@ -1217,6 +1249,7 @@ function publishTestRetry (test, err, config) {
1217
1249
  const willBeRetried = test._currentRetry < test._retries
1218
1250
  const isAtrRetry = !isFirstAttempt &&
1219
1251
  config.isFlakyTestRetriesEnabled &&
1252
+ automaticRetryAttempts.get(test._retriedTest || test)?.has(test._currentRetry) &&
1220
1253
  !test._ddIsAttemptToFix &&
1221
1254
  !test._ddIsEfdRetry
1222
1255
  const promises = {}
@@ -33,7 +33,7 @@ class Turn {
33
33
  textEnd
34
34
  completionId
35
35
  metrics = { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
36
- missingAudioMetrics = new Set()
36
+ missingModalityMetrics = new Set()
37
37
  emitted = false
38
38
  }
39
39
 
@@ -86,7 +86,9 @@ class SonicSession {
86
86
  #sessionId = randomUUID()
87
87
  #providerSessionId
88
88
  #totals = { input_tokens: 0, output_tokens: 0 }
89
- #audioTotals = { input: 0, output: 0 }
89
+ #modalityTotals = {
90
+ input_audio_tokens: 0, output_audio_tokens: 0, input_text_tokens: 0, output_text_tokens: 0,
91
+ }
90
92
 
91
93
  /** @param {(descriptor: object) => void} emit */
92
94
  constructor (emit) {
@@ -389,21 +391,28 @@ class SonicSession {
389
391
  target.metrics[key] += delta
390
392
  target.metrics.total_tokens += delta
391
393
 
392
- // Speech usage is a subset of these totals, never another charge to total_tokens.
393
- const audioKey = `${direction}_audio_tokens`
394
- const audioTotal = data.details?.total?.[direction]?.speechTokens
395
- const previous = this.#audioTotals[direction]
396
- const valid = Number.isSafeInteger(audioTotal) && audioTotal >= 0 && audioTotal <= total &&
397
- (previous === undefined || audioTotal >= previous)
398
- const audioDelta = valid && previous !== undefined ? audioTotal - previous : undefined
399
- this.#audioTotals[direction] = valid ? audioTotal : delta ? undefined : previous
400
- if (audioDelta === undefined || audioDelta > delta) {
401
- if (delta || audioDelta > delta) {
402
- target.missingAudioMetrics.add(audioKey)
403
- delete target.metrics[audioKey]
394
+ // Each modality is a subset of the aggregate and has its own cumulative baseline.
395
+ // Missing text must not discard valid speech or charge a previous turn's gap.
396
+ let remaining = delta
397
+ for (const [modality, field] of [['audio', 'speechTokens'], ['text', 'textTokens']]) {
398
+ const modalityKey = `${direction}_${modality}_tokens`
399
+ const modalityTotal = data.details?.total?.[direction]?.[field]
400
+ const previous = this.#modalityTotals[modalityKey]
401
+ const valid = Number.isSafeInteger(modalityTotal) && modalityTotal >= 0 && modalityTotal <= total &&
402
+ (previous === undefined || modalityTotal >= previous)
403
+ const modalityDelta = valid && previous !== undefined ? modalityTotal - previous : undefined
404
+ this.#modalityTotals[modalityKey] = valid ? modalityTotal : delta ? undefined : previous
405
+ if (modalityDelta === undefined || modalityDelta > remaining) {
406
+ if (delta || modalityDelta > remaining) {
407
+ target.missingModalityMetrics.add(modalityKey)
408
+ delete target.metrics[modalityKey]
409
+ }
410
+ } else {
411
+ remaining -= modalityDelta
412
+ if (!target.missingModalityMetrics.has(modalityKey)) {
413
+ target.metrics[modalityKey] = (target.metrics[modalityKey] ?? 0) + modalityDelta
414
+ }
404
415
  }
405
- } else if (!target.missingAudioMetrics.has(audioKey)) {
406
- target.metrics[audioKey] = (target.metrics[audioKey] ?? 0) + audioDelta
407
416
  }
408
417
  }
409
418
  }
@@ -1077,6 +1077,7 @@ function testEndHandler ({
1077
1077
  : undefined
1078
1078
  const isAtrRetry = testResult?.retry > 0 &&
1079
1079
  isFlakyTestRetriesEnabled &&
1080
+ hasAutomaticRetries(test, projects) &&
1080
1081
  !test._ddIsAttemptToFix &&
1081
1082
  !test._ddIsEfdRetry
1082
1083
 
@@ -1340,6 +1341,7 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1340
1341
  }
1341
1342
  const isAtrRetry = testResult?.retry > 0 &&
1342
1343
  isFlakyTestRetriesEnabled &&
1344
+ hasAutomaticRetries(test, projects) &&
1343
1345
  !test._ddIsAttemptToFix &&
1344
1346
  !test._ddIsEfdRetry
1345
1347
 
@@ -58,19 +58,30 @@ function wrapConstructEventAsync (constructEventAsync) {
58
58
  }
59
59
  }
60
60
 
61
+ const wrappedMethods = new WeakSet()
62
+
63
+ // Stripe >=9 <11.10 and >=18 share `webhooks` across clients. Stripe >=11.10 <18
64
+ // creates it per client. checkout.sessions and paymentIntents are separate for each client.
65
+ // Rewrapping shared webhook methods adds one layer per client, causing duplicate
66
+ // publications and eventually a stack overflow. Track installed functions so replacements
67
+ // still get wrapped and restored wrappers are reused.
68
+ /**
69
+ * @param {object|undefined} target
70
+ * @param {string} name
71
+ * @param {Function} wrapper
72
+ */
73
+ function wrapOnce (target, name, wrapper) {
74
+ if (typeof target?.[name] !== 'function' || wrappedMethods.has(target[name])) return
75
+
76
+ shimmer.wrap(target, name, wrapper)
77
+ wrappedMethods.add(target[name])
78
+ }
79
+
61
80
  function instrumentStripeInstance (stripe) {
62
- if (typeof stripe.checkout?.sessions?.create === 'function') {
63
- shimmer.wrap(stripe.checkout.sessions, 'create', wrapSessionCreate)
64
- }
65
- if (typeof stripe.paymentIntents?.create === 'function') {
66
- shimmer.wrap(stripe.paymentIntents, 'create', wrapPaymentIntentCreate)
67
- }
68
- if (typeof stripe.webhooks?.constructEvent === 'function') {
69
- shimmer.wrap(stripe.webhooks, 'constructEvent', wrapConstructEvent)
70
- }
71
- if (typeof stripe.webhooks?.constructEventAsync === 'function') {
72
- shimmer.wrap(stripe.webhooks, 'constructEventAsync', wrapConstructEventAsync)
73
- }
81
+ wrapOnce(stripe.checkout?.sessions, 'create', wrapSessionCreate)
82
+ wrapOnce(stripe.paymentIntents, 'create', wrapPaymentIntentCreate)
83
+ wrapOnce(stripe.webhooks, 'constructEvent', wrapConstructEvent)
84
+ wrapOnce(stripe.webhooks, 'constructEventAsync', wrapConstructEventAsync)
74
85
  }
75
86
 
76
87
  // stripe <22: the constructor mutates this (when invoked with 'new') and
@@ -934,6 +934,7 @@ function createMainProcessReporter (reporterState) {
934
934
  isDynamicAtrEnabled: state.isDynamicAtrEnabled,
935
935
  flakyTestRetriesIncludesUnnamedProject: flakyTestRetriesConfiguration.includesUnnamedProject,
936
936
  flakyTestRetriesProjectNames: flakyTestRetriesConfiguration.projectNames,
937
+ flakyTestRetriesUseRetryMarker: flakyTestRetriesConfiguration.usesRetryMarker,
937
938
  }, task)
938
939
 
939
940
  return {
@@ -680,6 +680,7 @@ function resetMainProcessProvidedContext (ctx) {
680
680
  _ddIsEfdSuiteAdmissionEnabled: false,
681
681
  _ddIsFlakyTestRetriesEnabled: false,
682
682
  _ddFlakyTestRetriesCount: 0,
683
+ _ddFlakyTestRetriesUseRetryMarker: false,
683
684
  _ddIsDynamicAtrEnabled: false,
684
685
  _ddDynamicAtrBuckets: undefined,
685
686
  _ddFlakyTestRetriesIncludesUnnamedProject: false,
@@ -823,7 +824,7 @@ async function runMainProcessSetup (
823
824
  }
824
825
  }
825
826
 
826
- const flakyTestRetriesConfiguration = configureFlakyTestRetries(ctx, testSpecifications)
827
+ const flakyTestRetriesConfiguration = configureFlakyTestRetries(ctx, testSpecifications, frameworkVersion)
827
828
  if (flakyTestRetriesConfiguration) {
828
829
  setProvidedContext(ctx, {
829
830
  _ddIsFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled,
@@ -832,6 +833,7 @@ async function runMainProcessSetup (
832
833
  _ddDynamicAtrBuckets: dynamicAtrBuckets,
833
834
  _ddFlakyTestRetriesIncludesUnnamedProject: flakyTestRetriesConfiguration.includesUnnamedProject,
834
835
  _ddFlakyTestRetriesProjectNames: flakyTestRetriesConfiguration.projectNames,
836
+ _ddFlakyTestRetriesUseRetryMarker: flakyTestRetriesConfiguration.usesRetryMarker,
835
837
  }, 'Could not send library configuration to workers.')
836
838
  }
837
839
 
@@ -1038,7 +1040,7 @@ function shouldUseBrowserReporter (frameworkVersion, testSpecifications) {
1038
1040
  testSpecifications.some(isBrowserTestSpecification)
1039
1041
  }
1040
1042
 
1041
- function configureFlakyTestRetries (ctx, testSpecifications) {
1043
+ function configureFlakyTestRetries (ctx, testSpecifications, frameworkVersion) {
1042
1044
  if (!isFlakyTestRetriesEnabled || (!isDynamicAtrEnabled && flakyTestRetriesCount <= 0)) return
1043
1045
 
1044
1046
  const maximumDynamicAtrRetries = dynamicAtrBuckets
@@ -1047,13 +1049,14 @@ function configureFlakyTestRetries (ctx, testSpecifications) {
1047
1049
  const retryCount = isDynamicAtrEnabled
1048
1050
  ? Math.max(1, maximumDynamicAtrRetries)
1049
1051
  : flakyTestRetriesCount
1052
+ const usesRetryMarker = isDynamicAtrEnabled || satisfies(frameworkVersion, '>=4.1.0')
1050
1053
  let configured = false
1051
1054
  let includesUnnamedProject = false
1052
1055
  const projectNames = []
1053
1056
  for (const { config, projectName } of getVitestProjectConfigs(ctx, testSpecifications)) {
1054
1057
  if (!config.retry || config.retry.__ddTestOptAtr) {
1055
1058
  // The serializable marker survives task inheritance and setup refreshes, unlike numeric retry counts.
1056
- config.retry = isDynamicAtrEnabled ? { count: retryCount, __ddTestOptAtr: true } : retryCount
1059
+ config.retry = usesRetryMarker ? { count: retryCount, __ddTestOptAtr: true } : retryCount
1057
1060
  configured = true
1058
1061
  if (projectName) {
1059
1062
  projectNames.push(projectName)
@@ -1068,6 +1071,7 @@ function configureFlakyTestRetries (ctx, testSpecifications) {
1068
1071
  return {
1069
1072
  includesUnnamedProject,
1070
1073
  projectNames,
1074
+ usesRetryMarker,
1071
1075
  }
1072
1076
  }
1073
1077
 
@@ -182,6 +182,7 @@ function getProvidedContext () {
182
182
  _ddDynamicAtrBuckets: dynamicAtrBuckets,
183
183
  _ddFlakyTestRetriesIncludesUnnamedProject: flakyTestRetriesIncludesUnnamedProject,
184
184
  _ddFlakyTestRetriesProjectNames: flakyTestRetriesProjectNames,
185
+ _ddFlakyTestRetriesUseRetryMarker: flakyTestRetriesUseRetryMarker,
185
186
  _ddIsImpactedTestsEnabled: isImpactedTestsEnabled,
186
187
  _ddTestSessionId: testSessionId,
187
188
  _ddTestModuleId: testModuleId,
@@ -211,6 +212,7 @@ function getProvidedContext () {
211
212
  dynamicAtrBuckets,
212
213
  flakyTestRetriesIncludesUnnamedProject,
213
214
  flakyTestRetriesProjectNames,
215
+ flakyTestRetriesUseRetryMarker,
214
216
  isImpactedTestsEnabled,
215
217
  testSessionId,
216
218
  testModuleId,
@@ -254,8 +256,9 @@ function getProvidedContext () {
254
256
  }
255
257
 
256
258
  function isFlakyTestRetriesEnabledForTask (providedContext, task) {
257
- if (!providedContext.isFlakyTestRetriesEnabled) return false
258
- if (providedContext.isDynamicAtrEnabled && !task.retry?.__ddTestOptAtr) return false
259
+ if (!providedContext.isFlakyTestRetriesEnabled || task.meta?.__ddTestOptNativeRetry) return false
260
+ if ((providedContext.isDynamicAtrEnabled || providedContext.flakyTestRetriesUseRetryMarker) &&
261
+ !task.retry?.__ddTestOptAtr) return false
259
262
 
260
263
  const { flakyTestRetriesProjectNames } = providedContext
261
264
  if (!Array.isArray(flakyTestRetriesProjectNames)) return true
@@ -2,6 +2,14 @@
2
2
 
3
3
  const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
4
4
  const { isTrue } = require('../../dd-trace/src/util')
5
+ const { channel } = require('./helpers/instrument')
6
+
7
+ channel('tracing:orchestrion:@vitest/runner:createTestTask:end').subscribe(({ arguments: args, result: task }) => {
8
+ // Suite options have already been merged here, including overrides equal to ATR's count.
9
+ if (args[1]?.retry !== undefined && task?.meta) {
10
+ task.meta = { ...task.meta, __ddTestOptNativeRetry: true }
11
+ }
12
+ })
5
13
 
6
14
  if (getEnvironmentVariable('DD_VITEST_WORKER') === '1') {
7
15
  const VITEST_NO_WORKER_INIT_ACTIVE_ENV = 'DD_TEST_OPT_VITEST_NO_WORKER_INIT_ACTIVE'
@@ -1664,6 +1664,7 @@ class CypressPlugin {
1664
1664
  cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.attempts[attemptIndex].state]
1665
1665
  const isAtrRetry = attemptIndex > 0 &&
1666
1666
  this.isFlakyTestRetriesEnabled &&
1667
+ (!finishedTest.hasNativeRetries || this.isDynamicAtrEnabled) &&
1667
1668
  !finishedTest.isAttemptToFix &&
1668
1669
  !finishedTest.isEfdRetry
1669
1670
  if (attemptIndex > 0) {
@@ -2154,6 +2155,7 @@ class CypressPlugin {
2154
2155
  isEfdRetry,
2155
2156
  isAttemptToFix,
2156
2157
  isModified,
2158
+ hasNativeRetries,
2157
2159
  duration,
2158
2160
  isQuarantined: isQuarantinedFromSupport,
2159
2161
  isDisabled: isDisabledFromSupport,
@@ -2354,6 +2356,7 @@ class CypressPlugin {
2354
2356
  isEfdRetry,
2355
2357
  isEfdManagedTest,
2356
2358
  isAttemptToFix,
2359
+ hasNativeRetries,
2357
2360
  }
2358
2361
  if (this.finishedTestsByFile[testSuite]) {
2359
2362
  this.finishedTestsByFile[testSuite].push(finishedTest)
@@ -523,6 +523,8 @@ afterEach(function () {
523
523
  isEfdRetry: currentTest._ddIsEfdRetry,
524
524
  isAttemptToFix: currentTest._ddIsAttemptToFix,
525
525
  isModified: currentTest._ddIsModified,
526
+ hasNativeRetries: !!(currentTest._testConfig && currentTest._testConfig.unverifiedTestConfig &&
527
+ currentTest._testConfig.unverifiedTestConfig.retries !== undefined),
526
528
  duration: currentTest.duration,
527
529
  // Mark suppressed tests so the plugin can tag them with the correct test management reason.
528
530
  isQuarantined: isTestManagementTestThatFailed && suppressedTestFailure.isQuarantined,