dd-trace 6.17.0 → 6.19.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 (110) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/vitest-no-worker-init-setup.mjs +22 -12
  3. package/index.d.ts +55 -11
  4. package/package.json +6 -6
  5. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  6. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  7. package/packages/datadog-instrumentations/src/cucumber.js +50 -9
  8. package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
  9. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
  10. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
  12. package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +51 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
  17. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
  18. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +6 -0
  19. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  20. package/packages/datadog-instrumentations/src/jest.js +30 -13
  21. package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
  23. package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
  24. package/packages/datadog-instrumentations/src/playwright.js +73 -6
  25. package/packages/datadog-instrumentations/src/undici.js +7 -3
  26. package/packages/datadog-instrumentations/src/vitest-main.js +95 -21
  27. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  28. package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
  29. package/packages/datadog-instrumentations/src/ws.js +41 -1
  30. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  31. package/packages/datadog-plugin-cucumber/src/index.js +22 -8
  32. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -14
  33. package/packages/datadog-plugin-cypress/src/index.js +3 -1
  34. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  35. package/packages/datadog-plugin-jest/src/index.js +3 -4
  36. package/packages/datadog-plugin-mocha/src/index.js +3 -4
  37. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  38. package/packages/datadog-plugin-playwright/src/index.js +11 -6
  39. package/packages/datadog-plugin-undici/src/index.js +5 -0
  40. package/packages/datadog-plugin-vitest/src/index.js +3 -4
  41. package/packages/datadog-plugin-ws/src/producer.js +1 -1
  42. package/packages/datadog-shimmer/src/shimmer.js +14 -10
  43. package/packages/datadog-turbopack/index.js +4 -1
  44. package/packages/datadog-turbopack/src/loader.js +6 -2
  45. package/packages/dd-trace/src/agent/info.js +5 -4
  46. package/packages/dd-trace/src/aiguard/client.js +38 -19
  47. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  48. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  49. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  50. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  51. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  52. package/packages/dd-trace/src/config/generated-config-types.d.ts +19 -19
  53. package/packages/dd-trace/src/config/index.js +7 -6
  54. package/packages/dd-trace/src/config/supported-configurations.json +16 -5
  55. package/packages/dd-trace/src/constants.js +2 -0
  56. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
  57. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
  58. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
  59. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  60. package/packages/dd-trace/src/debugger/index.js +1 -1
  61. package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
  62. package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
  63. package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
  64. package/packages/dd-trace/src/evp_proxy/path.js +21 -7
  65. package/packages/dd-trace/src/exporters/common/url.js +33 -2
  66. package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
  67. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  68. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  69. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  70. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  71. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  72. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  73. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  74. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  75. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  76. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  77. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  78. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  79. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
  80. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  81. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  82. package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
  83. package/packages/dd-trace/src/llmobs/prompts/prompt.js +79 -12
  84. package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
  85. package/packages/dd-trace/src/llmobs/tagger.js +11 -40
  86. package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
  87. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
  88. package/packages/dd-trace/src/openfeature/index.js +6 -2
  89. package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
  90. package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
  91. package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
  92. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  93. package/packages/dd-trace/src/opentracing/tracer.js +3 -3
  94. package/packages/dd-trace/src/plugin_manager.js +27 -8
  95. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
  96. package/packages/dd-trace/src/plugins/util/test.js +13 -4
  97. package/packages/dd-trace/src/profiler.js +51 -5
  98. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  99. package/packages/dd-trace/src/proxy.js +2 -2
  100. package/packages/dd-trace/src/ritm.js +12 -9
  101. package/packages/dd-trace/src/span_processor.js +6 -1
  102. package/packages/dd-trace/src/telemetry/send-data.js +2 -1
  103. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  104. package/packages/dd-trace/src/tracer.js +5 -4
  105. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  106. package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
  107. package/packages/datadog-instrumentations/src/bullmq.js +0 -11
  108. package/packages/datadog-instrumentations/src/langchain.js +0 -7
  109. package/packages/datadog-instrumentations/src/langgraph.js +0 -7
  110. package/packages/datadog-instrumentations/src/mercurius.js +0 -11
@@ -58,6 +58,7 @@ const {
58
58
  addCoverageBackfillUntestedFiles,
59
59
  getCoverageBackfillFiles,
60
60
  } = require('./jest/coverage-backfill')
61
+ const { getSessionError } = require('./jest/session-error')
61
62
  const {
62
63
  getChannelPromise,
63
64
  publishWithCompletion,
@@ -109,6 +110,10 @@ const CHILD_MESSAGE_CALL = 1
109
110
  const FLUSH_TIMEOUT = FINAL_FLUSH_TIMEOUT + FINAL_FLUSH_FALLBACK_DELAY + 5000
110
111
  const JEST_SESSION_STATE = Symbol.for('dd-trace:jest:session')
111
112
  const JEST_BAIL_REPORTER_PATH = require.resolve('./jest/bail-reporter')
113
+ const JEST_CIRCUS_TEST_RUNNER_PATH = /(?:^|[\\/])jest-circus(?:[\\/]|$)/
114
+ const UNSUPPORTED_JEST_TEST_RUNNER_WARNING =
115
+ 'dd-trace Test Optimization supports jest-circus; another test runner was detected; ' +
116
+ 'suite and test events may be incomplete.'
112
117
  const DD_JEST_HANDLE_TEST_EVENT_WRAPPED = Symbol('dd-trace:jest:handle-test-event-wrapped')
113
118
  const DD_JEST_HANDLE_TEST_EVENT_DATADOG = Symbol('dd-trace:jest:handle-test-event-datadog')
114
119
  const DD_JEST_CONCURRENT_TEST_ORIGINAL = Symbol('dd-trace:jest:concurrent-test-original')
@@ -248,6 +253,7 @@ const MINIMUM_JEST_TEST_SCHEDULER_VERSION = DD_MAJOR >= 6 ? '>=28.0.0' : '>=27.0
248
253
  const MINIMUM_JEST_COVERAGE_BACKFILL_VERSION = '>=28.0.0'
249
254
  const atrSuppressedErrors = new Map()
250
255
  let hasWarnedDeprecatedJestVersion = false
256
+ let hasWarnedUnsupportedJestTestRunner = false
251
257
  let isJestCoverageBackfillSupported = false
252
258
  let hasFinishedTestSession = false
253
259
  let jestEachBind
@@ -322,6 +328,20 @@ function warnDeprecatedJestVersion (frameworkVersion) {
322
328
  )
323
329
  }
324
330
 
331
+ /** @param {object[]} configs resolved Jest project configurations */
332
+ function warnIfUnsupportedJestTestRunner (configs) {
333
+ if (hasWarnedUnsupportedJestTestRunner || !testSessionConfigurationCh.hasSubscribers) return
334
+
335
+ const usesUnsupportedTestRunner = getEnvironmentVariable('JEST_JASMINE') === '1' ||
336
+ configs.some(config => !JEST_CIRCUS_TEST_RUNNER_PATH.test(config.testRunner))
337
+
338
+ if (usesUnsupportedTestRunner) {
339
+ hasWarnedUnsupportedJestTestRunner = true
340
+ // eslint-disable-next-line no-console
341
+ console.warn(UNSUPPORTED_JEST_TEST_RUNNER_WARNING)
342
+ }
343
+ }
344
+
325
345
  function getTestEnvironmentOptions (config) {
326
346
  if (config.projectConfig && config.projectConfig.testEnvironmentOptions) { // newer versions
327
347
  return config.projectConfig.testEnvironmentOptions
@@ -2720,13 +2740,6 @@ function applySkippedCoverageToJestCoverageMap (coverageMap, rootDir) {
2720
2740
  )
2721
2741
  }
2722
2742
 
2723
- function getSessionFinishError (results) {
2724
- const numFailedTestSuites = results?.numFailedTestSuites || 0
2725
- const numFailedTests = results?.numFailedTests || 0
2726
-
2727
- return new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
2728
- }
2729
-
2730
2743
  function getTestSessionCoveragePayload (results, fallbackRootDir) {
2731
2744
  const payload = {}
2732
2745
  if (!shouldReportCodeCoverageLinesPct()) return payload
@@ -2828,7 +2841,7 @@ function getTestSessionFinishPayload (status, error, extra = {}) {
2828
2841
  async function finishBailTestSession (results, fallbackRootDir) {
2829
2842
  await waitForTestSessionFinish(getTestSessionFinishPayload(
2830
2843
  'fail',
2831
- getSessionFinishError(results),
2844
+ getSessionError(results),
2832
2845
  getTestSessionCoveragePayload(results, fallbackRootDir)
2833
2846
  ))
2834
2847
  }
@@ -3151,8 +3164,8 @@ function getCliWrapper (isNewJestVersion) {
3151
3164
 
3152
3165
  const {
3153
3166
  results: {
3154
- numFailedTestSuites,
3155
3167
  numFailedTests,
3168
+ numPassedTests,
3156
3169
  numRuntimeErrorTestSuites = 0,
3157
3170
  numTotalTests,
3158
3171
  numTotalTestSuites,
@@ -3323,16 +3336,19 @@ function getCliWrapper (isNewJestVersion) {
3323
3336
 
3324
3337
  // Determine session status after EFD and quarantine checks have potentially modified success
3325
3338
  let status, error
3326
- const isExpectedEmptySession = numTotalTests === 0 && numTotalTestSuites === 0
3339
+ const hasExecutedTests = numPassedTests > 0 || numFailedTests > 0 || numSuppressedQuarantinedTests > 0
3340
+ const testSessionEmptyReason = result.results.success && !hasExecutedTests
3341
+ ? (numTotalTests > 0 || isSuitesSkipped ? 'all_tests_skipped' : 'zero_tests')
3342
+ : undefined
3327
3343
  if (result.results.success) {
3328
- status = isExpectedEmptySession ? 'skip' : 'pass'
3344
+ status = testSessionEmptyReason ? 'skip' : 'pass'
3329
3345
  } else {
3330
3346
  status = 'fail'
3331
- error = new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
3347
+ error = getSessionError(result.results)
3332
3348
  }
3333
3349
 
3334
3350
  await waitForTestSessionFinish(getTestSessionFinishPayload(status, error, {
3335
- isExpectedEmptySession: result.results.success && isExpectedEmptySession,
3351
+ testSessionEmptyReason,
3336
3352
  ...getTestSessionCoveragePayload(result.results, result.globalConfig?.rootDir),
3337
3353
  }))
3338
3354
 
@@ -3625,6 +3641,7 @@ addHook({
3625
3641
  }, jestAdapterWrapper)
3626
3642
 
3627
3643
  function configureTestEnvironment (readConfigsResult) {
3644
+ warnIfUnsupportedJestTestRunner(readConfigsResult.configs)
3628
3645
  repositoryRoot = getJestRepositoryRoot(readConfigsResult)
3629
3646
  isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
3630
3647
  const isCodeCoverageEnabledBecauseOfUs = shouldCollectJestCoverageForTia() && !isUserCodeCoverageEnabled
@@ -316,8 +316,8 @@ function getOnEndHandler (isParallel, onDone) {
316
316
  let status = 'pass'
317
317
  let error = frameworkError
318
318
  if (this.stats) {
319
- status = this.stats.failures === 0 ? 'pass' : 'fail'
320
- if (this.stats.tests === 0 && this.stats.failures === 0) {
319
+ status = this.stats.failures === 0 && this.failures === 0 ? 'pass' : 'fail'
320
+ if (this.stats.tests === this.stats.pending && status === 'pass') {
321
321
  status = 'skip'
322
322
  }
323
323
  } else if (this.failures !== 0) {
@@ -329,7 +329,9 @@ function getOnEndHandler (isParallel, onDone) {
329
329
  } else if (status === 'fail') {
330
330
  error = new Error(`Failed tests: ${this.failures}.`)
331
331
  }
332
- const isExpectedEmptySession = arguments.length === 0 && status === 'skip'
332
+ const testSessionEmptyReason = status === 'skip'
333
+ ? (this.stats.pending > 0 || skippedSuites.length > 0 ? 'all_tests_skipped' : 'zero_tests')
334
+ : undefined
333
335
 
334
336
  testFileToSuiteCtx.clear()
335
337
 
@@ -373,7 +375,7 @@ function getOnEndHandler (isParallel, onDone) {
373
375
  isTestManagementEnabled: config.isTestManagementTestsEnabled,
374
376
  isParallel,
375
377
  isFrameworkError: arguments.length > 0,
376
- isExpectedEmptySession,
378
+ testSessionEmptyReason,
377
379
  }, () => {
378
380
  try {
379
381
  onDone()
@@ -1022,7 +1024,7 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
1022
1024
  config.isSuitesSkippingEnabled = config.isItrEnabled && libraryConfig.isSuitesSkippingEnabled
1023
1025
  config.isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
1024
1026
  config.flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
1025
- config.isDynamicAtrEnabled = libraryConfig.isDynamicAtrEnabled
1027
+ config.isDynamicAtrEnabled = libraryConfig.isDynamicAtrEnabled && satisfies(frameworkVersion, '>=8.0.0')
1026
1028
  config.dynamicAtrBuckets = libraryConfig.dynamicAtrBuckets
1027
1029
  config.isDiEnabled = libraryConfig.isDiEnabled
1028
1030
  config.isTestDynamicInstrumentationEnabled = isTestDynamicInstrumentationEnabled
@@ -54,7 +54,7 @@ const testsQuarantined = new Set()
54
54
  const testsStatuses = new Map()
55
55
  const efdRetryCountByTestFullName = new Map()
56
56
  const efdSlowAbortedTests = new Set()
57
- const dynamicAtrRetryCountByTestFullName = new Map()
57
+ let dynamicAtrRetryCountByTest = new WeakMap()
58
58
  const attemptToFixExecutions = new Map()
59
59
  const isMochaWorker = !!getEnvironmentVariable('MOCHA_WORKER_ID')
60
60
 
@@ -343,7 +343,7 @@ function resetRunState (rootSuite) {
343
343
  testsStatuses.clear()
344
344
  efdRetryCountByTestFullName.clear()
345
345
  efdSlowAbortedTests.clear()
346
- dynamicAtrRetryCountByTestFullName.clear()
346
+ dynamicAtrRetryCountByTest = new WeakMap()
347
347
  attemptToFixExecutions.clear()
348
348
  loggedAttemptToFixTests.clear()
349
349
 
@@ -538,8 +538,7 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
538
538
  if (libraryConfig.isDynamicAtrEnabled) {
539
539
  // Dynamic ATR: set the max possible retries initially.
540
540
  // The actual duration-based count is computed after the first attempt.
541
- const testName = getTestFullName(test)
542
- const dynamicCount = dynamicAtrRetryCountByTestFullName.get(testName)
541
+ const dynamicCount = dynamicAtrRetryCountByTest.get(test._retriedTest || test)
543
542
  if (dynamicCount === undefined) {
544
543
  const maxRetries = libraryConfig.dynamicAtrBuckets
545
544
  ? Math.max(...libraryConfig.dynamicAtrBuckets)
@@ -723,12 +722,14 @@ function getTestFinishInfo (test, status, config, error) {
723
722
  }
724
723
 
725
724
  // Dynamic ATR: after the first attempt, compute the duration-based retry budget.
725
+ // Mocha's native retry clones point to the original test, independently of duplicate titles.
726
+ const originalTest = test._retriedTest || test
726
727
  if (
727
728
  config.isDynamicAtrEnabled &&
728
729
  config.isFlakyTestRetriesEnabled &&
729
730
  !test._ddIsAttemptToFix &&
730
731
  !test._ddIsEfdRetry &&
731
- !dynamicAtrRetryCountByTestFullName.has(testName) &&
732
+ !dynamicAtrRetryCountByTest.has(originalTest) &&
732
733
  test._currentRetry === 0
733
734
  ) {
734
735
  const duration = test.duration > 0 ? test.duration : performance.now() - test._ddStartTime
@@ -737,7 +738,7 @@ function getTestFinishInfo (test, status, config, error) {
737
738
  config.earlyFlakeDetectionRetryPolicy,
738
739
  config.dynamicAtrBuckets
739
740
  )
740
- dynamicAtrRetryCountByTestFullName.set(testName, dynamicCount)
741
+ dynamicAtrRetryCountByTest.set(originalTest, dynamicCount)
741
742
  }
742
743
 
743
744
  if (testsStatuses.get(testName)) {
@@ -1094,13 +1095,12 @@ function getOnTestRetryHandler (config) {
1094
1095
  !test._ddIsAttemptToFix &&
1095
1096
  !isEarlyFlakeDetectionTest(test, config)
1096
1097
  if (isDynamicAtrTest && isFirstAttempt) {
1097
- const testName = getTestFullName(test)
1098
1098
  const dynamicCount = getDynamicAtrRetryCount(
1099
1099
  test.duration > 0 ? test.duration : performance.now() - test._ddStartTime,
1100
1100
  config.earlyFlakeDetectionRetryPolicy,
1101
1101
  config.dynamicAtrBuckets
1102
1102
  )
1103
- dynamicAtrRetryCountByTestFullName.set(testName, dynamicCount)
1103
+ dynamicAtrRetryCountByTest.set(test._retriedTest || test, dynamicCount)
1104
1104
  // Mocha emits retry before its next attempt starts; narrow its ceiling here.
1105
1105
  test._retries = dynamicCount
1106
1106
  }
@@ -206,16 +206,15 @@ class DatadogPlaywrightReporter {
206
206
  onExit () {}
207
207
 
208
208
  /**
209
- * Reports errors emitted by Playwright while later reporters are finalizing.
209
+ * Reports global run errors as well as errors from later reporters during finalization.
210
210
  *
211
211
  * @param {unknown} error
212
212
  */
213
213
  onError (error) {
214
- if (this.isFinalizing) {
215
- reporterErrorCh.publish(error)
216
- } else {
214
+ if (!this.isFinalizing) {
217
215
  this.fatalErrorCount += 1
218
216
  }
217
+ reporterErrorCh.publish(error)
219
218
  }
220
219
 
221
220
  /**
@@ -69,13 +69,16 @@ const dispatcherCreateWorkerCh = tracingChannel('orchestrion:playwright:Dispatch
69
69
  const filterForShardCh = tracingChannel('orchestrion:playwright:filterForShard')
70
70
  const processHostStartRunnerCh = tracingChannel('orchestrion:playwright:ProcessHost_startRunner')
71
71
  const createRootSuiteCh = tracingChannel('orchestrion:playwright:createRootSuite')
72
+ const lastRunTestIdCh = tracingChannel('orchestrion:playwright:lastRunTestId')
72
73
  const artifactsRecorderScreenshotPathCh =
73
74
  tracingChannel('orchestrion:playwright:ArtifactsRecorder_createScreenshotAttachmentPath')
74
75
  const snapshotRecorderScreenshotPathCh = tracingChannel('orchestrion:playwright:SnapshotRecorder_createAttachmentPath')
75
76
  const saveAutomaticVideoCh = tracingChannel('orchestrion:playwright:saveAutomaticVideo')
76
77
  const pageGotoCh = tracingChannel('orchestrion:playwright-core:Page_goto')
78
+ const fullProjectInternalCh = tracingChannel('orchestrion:playwright:FullProjectInternal')
77
79
 
78
80
  const testToCtx = new WeakMap()
81
+ const originalTestIdsByRetry = new WeakMap()
79
82
  const testSuiteToCtx = new Map()
80
83
  const testSuiteToTestStatuses = new Map()
81
84
  const testSuiteToErrors = new Map()
@@ -89,6 +92,8 @@ const isFailureScreenshotUploadEnabled =
89
92
  getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED') === true
90
93
  const isFailureVideoUploadEnabled =
91
94
  getValueFromEnvSources('DD_TEST_FAILURE_VIDEOS_ENABLED') === true
95
+ const shouldEnableFailureScreenshots = getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED', true) === true
96
+ const shouldEnableFailureVideos = getValueFromEnvSources('DD_TEST_FAILURE_VIDEOS_ENABLED', true) === true
92
97
 
93
98
  let applyRepeatEachIndex = null
94
99
  let reporterError
@@ -475,6 +480,7 @@ function deepCloneSuite (suite, filterTest, tags = [], configureCopiedTest) {
475
480
  } else {
476
481
  if (filterTest(entry)) {
477
482
  const copiedTest = entry._clone()
483
+ originalTestIdsByRetry.set(copiedTest, originalTestIdsByRetry.get(entry) ?? entry.id)
478
484
  if (configureCopiedTest) {
479
485
  configureCopiedTest(copiedTest, entry)
480
486
  }
@@ -915,6 +921,7 @@ function testEndHandler ({
915
921
  testDuration,
916
922
  testResultStatus,
917
923
  expectedStatus = test.expectedStatus,
924
+ hasNonRetriableError,
918
925
  }) {
919
926
  const {
920
927
  _requireFile: testSuiteAbsolutePath,
@@ -1053,6 +1060,8 @@ function testEndHandler ({
1053
1060
  if (isFlakyTestRetriesEnabled && !testProperties.attemptToFix && !test._ddIsEfdRetry &&
1054
1061
  !(test._ddIsNew || test._ddIsModified) &&
1055
1062
  atrRetryCount != null && atrRetryCount > 0 &&
1063
+ // Serial suites can add skipped results before a test's first execution.
1064
+ results.some((result, index) => index < results.length - 1 && result.status !== 'skipped') &&
1056
1065
  !willRetry && testResultStatus !== expectedStatus &&
1057
1066
  testStatuses.every(status => status === 'fail')) {
1058
1067
  test._ddHasFailedAllRetries = true
@@ -1087,6 +1096,8 @@ function testEndHandler ({
1087
1096
  if (testCtx) {
1088
1097
  testFinishCh.publish({
1089
1098
  testStatus,
1099
+ isExpectedFailure: testResultStatus === 'failed' && expectedStatus === 'failed',
1100
+ hasNonRetriableError,
1090
1101
  steps: testResult?.steps || [],
1091
1102
  isRetry: testResult?.retry > 0,
1092
1103
  error,
@@ -1278,7 +1289,7 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1278
1289
  videos.push(attachment)
1279
1290
  }
1280
1291
  })
1281
- worker.on('testEnd', ({ testId, status, errors, annotations, duration, expectedStatus }) => {
1292
+ worker.on('testEnd', ({ testId, status, errors, annotations, duration, expectedStatus, hasNonRetriableError }) => {
1282
1293
  const test = getTestByTestId(dispatcher, testId)
1283
1294
  if (!test) return
1284
1295
 
@@ -1300,6 +1311,7 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1300
1311
  testDuration: duration,
1301
1312
  testResultStatus: status,
1302
1313
  expectedStatus,
1314
+ hasNonRetriableError,
1303
1315
  }
1304
1316
  )
1305
1317
  const testResult = test.results.at(-1)
@@ -1406,6 +1418,8 @@ function dispatcherHook (dispatcherExport) {
1406
1418
  annotations: params.annotations,
1407
1419
  testStatus: STATUS_TO_TEST_STATUS[testResult.status],
1408
1420
  testResultStatus: testResult.status,
1421
+ expectedStatus: params.expectedStatus,
1422
+ hasNonRetriableError: params.hasNonRetriableError,
1409
1423
  error: testResult.error,
1410
1424
  isTimeout,
1411
1425
  shouldCreateTestSpan: true,
@@ -1431,7 +1445,7 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
1431
1445
 
1432
1446
  function runAllTestsWrapper (runAllTests, playwrightVersion) {
1433
1447
  // Config parameter is only available from >=1.55.0
1434
- return async function (config) {
1448
+ return async function (config, options) {
1435
1449
  // A later run must not inherit ATR settings when configuration fails or the plugin is disabled.
1436
1450
  isFlakyTestRetriesEnabled = false
1437
1451
  flakyTestRetriesCount = 0
@@ -1682,14 +1696,30 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1682
1696
  const finalStatus = hasReporterError
1683
1697
  ? 'fail'
1684
1698
  : (preventedToFail ? 'pass' : STATUS_TO_TEST_STATUS[sessionStatus])
1685
- const isExpectedEmptyShard = finalStatus === 'pass' &&
1686
- Boolean(playwrightConfig.shard) &&
1699
+ const isTestDiscovery = finalStatus === 'pass' &&
1700
+ Boolean(
1701
+ config?.listOnly ||
1702
+ config?.cliListOnly ||
1703
+ runnerConfig.cliListOnly ||
1704
+ runnerConfig._internal?.listOnly ||
1705
+ options?.listMode
1706
+ )
1707
+ const isEmptyShard = Boolean(playwrightConfig.shard) &&
1687
1708
  hasTestsBeforeSharding &&
1688
1709
  !hasTestsAssignedToShard &&
1689
1710
  testsReportedInGenerateSummary.size === 0
1711
+ const hasExecutedTests = [...testsToTestStatuses.values()].some(statuses =>
1712
+ statuses.some(status => status !== 'skip')
1713
+ )
1714
+ const hasSkippedTests = testsToTestStatuses.size > 0 || testsReportedInGenerateSummary.size > 0
1715
+ const testSessionEmptyReason = finalStatus === 'pass' && !hasExecutedTests
1716
+ ? (isTestDiscovery
1717
+ ? 'test_discovery'
1718
+ : isEmptyShard ? 'zero_test_shard' : hasSkippedTests ? 'all_tests_skipped' : 'zero_tests')
1719
+ : undefined
1690
1720
  await getChannelPromise(testSessionFinishCh, {
1691
- status: isExpectedEmptyShard ? 'skip' : finalStatus,
1692
- isExpectedEmptyShard,
1721
+ status: testSessionEmptyReason ? 'skip' : finalStatus,
1722
+ testSessionEmptyReason,
1693
1723
  error: finalizationError,
1694
1724
  isEarlyFlakeDetectionEnabled,
1695
1725
  isEarlyFlakeDetectionFaulty,
@@ -1857,6 +1887,27 @@ dispatcherRunCh.subscribe({
1857
1887
  },
1858
1888
  })
1859
1889
 
1890
+ fullProjectInternalCh.subscribe({
1891
+ end ({ self, error }) {
1892
+ if (error || !libraryConfigurationCh.hasSubscribers) return
1893
+
1894
+ // Workers reload the config, so apply capture settings as each project is resolved in either process.
1895
+ const { project } = self
1896
+ if (shouldEnableFailureScreenshots && !isFailureScreenshotCaptureEnabled([project])) {
1897
+ const screenshot = project.use.screenshot
1898
+ project.use.screenshot = typeof screenshot === 'object' && screenshot !== null
1899
+ ? { ...screenshot, mode: 'only-on-failure' }
1900
+ : 'only-on-failure'
1901
+ }
1902
+ if (shouldEnableFailureVideos && !isFailureVideoCaptureEnabled([project])) {
1903
+ const video = project.use.video
1904
+ project.use.video = typeof video === 'object' && video !== null
1905
+ ? { ...video, mode: 'retain-on-failure' }
1906
+ : 'retain-on-failure'
1907
+ }
1908
+ },
1909
+ })
1910
+
1860
1911
  dispatcherCreateWorkerCh.subscribe({
1861
1912
  end (ctx) {
1862
1913
  onDispatcherCreateWorker(ctx.self, ctx.result)
@@ -1898,6 +1949,14 @@ pageGotoCh.subscribe({
1898
1949
  },
1899
1950
  })
1900
1951
 
1952
+ // Retry clones exist only after discovery, so persist their original IDs for --last-failed.
1953
+ lastRunTestIdCh.subscribe({
1954
+ end (ctx) {
1955
+ const originalId = originalTestIdsByRetry.get(ctx.arguments[0])
1956
+ if (originalId !== undefined) ctx.result = originalId
1957
+ },
1958
+ })
1959
+
1901
1960
  reporterErrorCh.subscribe((error) => {
1902
1961
  recordReporterError(error)
1903
1962
  })
@@ -1968,6 +2027,12 @@ snapshotRecorderScreenshotPathCh.subscribe({ end: recordAutomaticFailureScreensh
1968
2027
  saveAutomaticVideoCh.subscribe({ start: recordAutomaticFailureVideoPath })
1969
2028
 
1970
2029
  if (DD_MAJOR < 6) { // <1.38.0 is only supported up to version 5
2030
+ tracingChannel('orchestrion:@playwright/test:Multiplexer_onError').subscribe({
2031
+ start (ctx) {
2032
+ recordReporterError(ctx.arguments[0])
2033
+ },
2034
+ })
2035
+
1971
2036
  addHook({
1972
2037
  name: '@playwright/test',
1973
2038
  file: 'lib/runner.js',
@@ -2656,6 +2721,8 @@ function instrumentWorkerMainMethods (workerMain) {
2656
2721
 
2657
2722
  await getChannelPromise(testFinishCh, {
2658
2723
  testStatus: STATUS_TO_TEST_STATUS[status],
2724
+ isExpectedFailure: status === 'failed' && testInfo.expectedStatus === 'failed',
2725
+ hasNonRetriableError: testInfo._hasNonRetriableError,
2659
2726
  retryTestId,
2660
2727
  deferFinalStatus: test._ddDeferFinalStatus,
2661
2728
  steps: steps.filter(step => step.testId === testId),
@@ -45,9 +45,13 @@ function wrapRequestUpgrade (Request) {
45
45
  const method = Request.prototype[methodName]
46
46
 
47
47
  // Orchestrion cannot leave fixed source byte-for-byte untouched after a conditional AST match.
48
- if (typeof method !== 'function' ||
49
- !requestSource.includes('channels.trailers.publish') ||
50
- Function.prototype.toString.call(method).includes('channels.trailers.publish')) {
48
+ if (typeof method !== 'function' || !requestSource.includes('channels.trailers.publish')) {
49
+ return Request
50
+ }
51
+
52
+ const methodSource = Function.prototype.toString.call(method)
53
+ // Undici 6.29 delegates native upgrade completion to a private helper.
54
+ if (methodSource.includes('channels.trailers.publish') || methodSource.includes('#publishUpgradeTrailers')) {
51
55
  return Request
52
56
  }
53
57
 
@@ -65,6 +65,9 @@ const workerProcesses = new WeakSet()
65
65
  const mainProcessSetupStates = new WeakMap()
66
66
  const coverageWrappedProviders = new WeakSet()
67
67
  const finishWrappedContexts = new WeakSet()
68
+ const finishedSessionContexts = new WeakSet()
69
+ const sessionWrappedPrototypes = new WeakSet()
70
+ const emptyShardContexts = new WeakSet()
68
71
  const runFilesWrappedPrototypes = new WeakSet()
69
72
  const activeRunFilesContexts = new WeakSet()
70
73
  const runErrorsByContext = new WeakMap()
@@ -163,16 +166,6 @@ function getThreadsPoolWorkerExport (vitestPackage) {
163
166
  return findExportByName(vitestPackage, 'ThreadsPoolWorker')
164
167
  }
165
168
 
166
- function getSessionStatus (state) {
167
- if (state.getCountOfFailedTests() > 0) {
168
- return 'fail'
169
- }
170
- if (state.pathsSet.size === 0) {
171
- return 'skip'
172
- }
173
- return 'pass'
174
- }
175
-
176
169
  function getTestFilepathsFromSpecifications (testSpecifications) {
177
170
  if (!Array.isArray(testSpecifications) || !testSpecifications.length) {
178
171
  return
@@ -1194,6 +1187,9 @@ function safeWorkspaceProject (ctx) {
1194
1187
 
1195
1188
  function getSortWrapper (sort, frameworkVersion) {
1196
1189
  return async function () {
1190
+ if (this.ctx.config.shard && this.ctx.state.pathsSet.size > 0 && arguments[0].length === 0) {
1191
+ emptyShardContexts.add(this.ctx)
1192
+ }
1197
1193
  if (!activeRunFilesContexts.has(this.ctx)) {
1198
1194
  const testSpecifications = arguments[0]
1199
1195
  await ensureMainProcessSetup(this.ctx, frameworkVersion, testSpecifications)
@@ -1204,18 +1200,18 @@ function getSortWrapper (sort, frameworkVersion) {
1204
1200
  }
1205
1201
 
1206
1202
  function getFinishWrapper (exitOrClose) {
1207
- let isClosed = false
1208
1203
  return async function () {
1209
- if (isClosed) { // needed because exit calls close
1204
+ if (finishedSessionContexts.has(this)) {
1210
1205
  return exitOrClose.apply(this, arguments)
1211
1206
  }
1212
- isClosed = true
1213
1207
 
1214
1208
  if (!testSessionFinishCh.hasSubscribers) {
1215
1209
  return exitOrClose.apply(this, arguments)
1216
1210
  }
1211
+ finishedSessionContexts.add(this)
1217
1212
 
1218
1213
  const failedSuites = this.state.getFailedFilepaths()
1214
+ const hasRunError = runErrorsByContext.has(this)
1219
1215
  const runError = runErrorsByContext.get(this)
1220
1216
  runErrorsByContext.delete(this)
1221
1217
  let error = runError
@@ -1223,19 +1219,30 @@ function getFinishWrapper (exitOrClose) {
1223
1219
  error = new Error(`Test suites failed: ${failedSuites.length}.`)
1224
1220
  }
1225
1221
 
1222
+ // pathsSet contains candidates before sharding, including files that never run.
1226
1223
  const hasNoTestFiles = this.state.pathsSet.size === 0
1227
- const hasUnexpectedEmptySession = hasNoTestFiles && !areAllSuitesSkipped && !this.config.passWithNoTests
1224
+ const isEmptyShard = emptyShardContexts.has(this)
1225
+ const hasUnexpectedEmptySession = hasNoTestFiles && !isEmptyShard &&
1226
+ !areAllSuitesSkipped && !this.config.passWithNoTests
1228
1227
  if (!error && hasUnexpectedEmptySession) {
1229
1228
  error = new Error('No test files were found.')
1230
1229
  }
1231
- const status = runError || hasUnexpectedEmptySession
1232
- ? 'fail'
1233
- : (areAllSuitesSkipped ? 'skip' : getSessionStatus(this.state))
1234
- const isExpectedEmptySession = !runError && !hasUnexpectedEmptySession &&
1235
- (areAllSuitesSkipped || hasNoTestFiles)
1230
+ const tests = getTypeTasks(this.state.getFiles())
1231
+ const hasExecutedTests = tests.some(test =>
1232
+ test.mode !== 'skip' && test.mode !== 'todo' &&
1233
+ test.result?.state !== 'skip' && test.result?.state !== 'todo'
1234
+ )
1235
+ const hasErrors = hasRunError || error ||
1236
+ this.state.getCountOfFailedTests() > 0 || this.state.getUnhandledErrors().length > 0
1237
+ const testSessionEmptyReason = !hasErrors && !hasExecutedTests
1238
+ ? (isEmptyShard
1239
+ ? 'zero_test_shard'
1240
+ : tests.length > 0 || skippedSuites.length > 0 ? 'all_tests_skipped' : 'zero_tests')
1241
+ : undefined
1242
+ const status = hasErrors ? 'fail' : (testSessionEmptyReason ? 'skip' : 'pass')
1236
1243
  const flushPromise = getChannelPromise(testSessionFinishCh, {
1237
1244
  status,
1238
- isExpectedEmptySession,
1245
+ testSessionEmptyReason,
1239
1246
  testCodeCoverageLinesTotal,
1240
1247
  error,
1241
1248
  isEarlyFlakeDetectionEnabled,
@@ -1391,12 +1398,46 @@ function markVitestWorkerEnv (ctx, testSpecifications, shouldSkipWorkerInit = fa
1391
1398
  config.env = getVitestWorkerEnv(config.env, shouldSkipWorkerInit)
1392
1399
  }
1393
1400
 
1401
+ /**
1402
+ * Installs completion hooks before discovery, which can bypass runFiles entirely.
1403
+ * @param {Function} Vitest
1404
+ */
1405
+ function wrapVitestSession (Vitest) {
1406
+ if (!Vitest?.prototype?.start || sessionWrappedPrototypes.has(Vitest.prototype)) return
1407
+ sessionWrappedPrototypes.add(Vitest.prototype)
1408
+
1409
+ // Empty discovery bypasses runFiles. Vitest 1 exits directly after reporting coverage,
1410
+ // so its finalization must be awaited before that process.exit rather than only in close.
1411
+ // Hashed bundle names prevent Orchestrion's exact-file matching.
1412
+ shimmer.wrap(Vitest.prototype, 'start', start => function () {
1413
+ if (!testSessionFinishCh.hasSubscribers) return start.apply(this, arguments)
1414
+ wrapSessionFinish(this)
1415
+ return start.apply(this, arguments).then(undefined, error => {
1416
+ if (error?.code !== 'VITEST_FILES_NOT_FOUND') runErrorsByContext.set(this, error)
1417
+ throw error
1418
+ })
1419
+ })
1420
+ shimmer.wrap(Vitest.prototype, 'reportCoverage', reportCoverage => function () {
1421
+ const result = reportCoverage.apply(this, arguments)
1422
+ if (!testSessionFinishCh.hasSubscribers || !isSessionStarted || this.config.watch ||
1423
+ mainProcessSetupStates.has(this) || this.state.pathsSet.size > 0) {
1424
+ return result
1425
+ }
1426
+ return result.then(value => {
1427
+ const finish = getFinishWrapper(() => value)
1428
+ return finish.call(this)
1429
+ })
1430
+ })
1431
+ }
1432
+
1394
1433
  function wrapVitestRunFiles (Vitest, frameworkVersion) {
1395
1434
  if (!Vitest?.prototype?.runFiles || runFilesWrappedPrototypes.has(Vitest.prototype)) {
1396
1435
  return
1397
1436
  }
1398
1437
  runFilesWrappedPrototypes.add(Vitest.prototype)
1399
1438
 
1439
+ wrapVitestSession(Vitest)
1440
+
1400
1441
  shimmer.wrap(Vitest.prototype, 'runFiles', runFiles => async function (testSpecifications) {
1401
1442
  if (activeRunFilesContexts.has(this)) {
1402
1443
  return runFiles.apply(this, arguments)
@@ -1940,6 +1981,9 @@ function getWrappedOn (on) {
1940
1981
  if (event !== 'message') {
1941
1982
  return on.apply(this, arguments)
1942
1983
  }
1984
+ // Reply through Vitest's pool worker, which owns its version-specific serialization.
1985
+ // The listener's `this` is the underlying ChildProcess or Worker instead.
1986
+ const worker = this
1943
1987
  // `arguments[1]` is the callback function, which
1944
1988
  // we modify to intercept our messages to not interfere
1945
1989
  // with vitest's own messages
@@ -1947,7 +1991,7 @@ function getWrappedOn (on) {
1947
1991
  if (message.type !== 'Buffer' && Array.isArray(message)) {
1948
1992
  const [interprocessCode, data] = message
1949
1993
  if (
1950
- handleEfdAdmissionMessage(this, interprocessCode, data) ||
1994
+ handleEfdAdmissionMessage(worker, interprocessCode, data) ||
1951
1995
  handleWorkerReport(interprocessCode, data)
1952
1996
  ) {
1953
1997
  // If we execute the callback vitest crashes, as the message is not supported
@@ -2080,6 +2124,36 @@ addHook({
2080
2124
  return wrapTinyPool(TinyPool)
2081
2125
  })
2082
2126
 
2127
+ /**
2128
+ * Older Vitest versions expose their core instance through the logger constructor.
2129
+ * @param {Record<string, Function>} vitestPackage
2130
+ */
2131
+ function wrapVitestLogger (vitestPackage) {
2132
+ const logger = findExportByName(vitestPackage, 'Logger')
2133
+ if (logger) {
2134
+ // Hashed bundle names prevent Orchestrion's exact-file matching.
2135
+ shimmer.wrap(vitestPackage, logger.key, Logger => class extends Logger {
2136
+ constructor (...args) {
2137
+ super(...args)
2138
+ if (testSessionFinishCh.hasSubscribers) wrapVitestSession(args[0]?.constructor)
2139
+ }
2140
+ })
2141
+ }
2142
+ return vitestPackage
2143
+ }
2144
+
2145
+ addHook({
2146
+ name: 'vitest',
2147
+ versions: ['>=1.6.0 <2.0.5'],
2148
+ filePattern: 'dist/vendor/index.*',
2149
+ }, wrapVitestLogger)
2150
+
2151
+ addHook({
2152
+ name: 'vitest',
2153
+ versions: ['>=2.0.5 <3.0.0'],
2154
+ filePattern: 'dist/chunks/index.*',
2155
+ }, wrapVitestLogger)
2156
+
2083
2157
  // There are multiple index* files across different versions of vitest,
2084
2158
  // so we check for the existence of BaseSequencer to determine if we are in the right file
2085
2159
  addHook({