dd-trace 6.17.0 → 6.18.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 (70) hide show
  1. package/index.d.ts +52 -9
  2. package/package.json +1 -1
  3. package/packages/datadog-instrumentations/src/cucumber.js +10 -5
  4. package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
  5. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
  6. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
  8. package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +43 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +4 -0
  15. package/packages/datadog-instrumentations/src/jest.js +27 -3
  16. package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
  17. package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
  18. package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
  19. package/packages/datadog-instrumentations/src/playwright.js +47 -6
  20. package/packages/datadog-instrumentations/src/undici.js +7 -3
  21. package/packages/datadog-instrumentations/src/vitest-main.js +91 -20
  22. package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
  23. package/packages/datadog-instrumentations/src/ws.js +41 -1
  24. package/packages/datadog-plugin-cucumber/src/index.js +3 -4
  25. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +44 -14
  26. package/packages/datadog-plugin-cypress/src/index.js +3 -1
  27. package/packages/datadog-plugin-jest/src/index.js +3 -4
  28. package/packages/datadog-plugin-mocha/src/index.js +3 -4
  29. package/packages/datadog-plugin-playwright/src/index.js +11 -6
  30. package/packages/datadog-plugin-vitest/src/index.js +3 -4
  31. package/packages/datadog-plugin-ws/src/producer.js +1 -1
  32. package/packages/datadog-shimmer/src/shimmer.js +14 -10
  33. package/packages/datadog-turbopack/index.js +4 -1
  34. package/packages/datadog-turbopack/src/loader.js +6 -2
  35. package/packages/dd-trace/src/agent/info.js +5 -4
  36. package/packages/dd-trace/src/config/generated-config-types.d.ts +15 -15
  37. package/packages/dd-trace/src/config/index.js +7 -6
  38. package/packages/dd-trace/src/config/supported-configurations.json +11 -0
  39. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
  41. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
  42. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  43. package/packages/dd-trace/src/debugger/index.js +1 -1
  44. package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
  45. package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
  46. package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
  47. package/packages/dd-trace/src/evp_proxy/path.js +21 -7
  48. package/packages/dd-trace/src/exporters/common/url.js +33 -2
  49. package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
  50. package/packages/dd-trace/src/llmobs/prompts/prompt.js +77 -11
  51. package/packages/dd-trace/src/llmobs/tagger.js +9 -4
  52. package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
  53. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
  54. package/packages/dd-trace/src/openfeature/index.js +6 -2
  55. package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
  56. package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
  57. package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
  58. package/packages/dd-trace/src/opentracing/tracer.js +3 -3
  59. package/packages/dd-trace/src/plugin_manager.js +27 -8
  60. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
  61. package/packages/dd-trace/src/plugins/util/test.js +9 -4
  62. package/packages/dd-trace/src/proxy.js +2 -2
  63. package/packages/dd-trace/src/telemetry/send-data.js +2 -1
  64. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  65. package/packages/dd-trace/src/tracer.js +5 -4
  66. package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
  67. package/packages/datadog-instrumentations/src/bullmq.js +0 -11
  68. package/packages/datadog-instrumentations/src/langchain.js +0 -7
  69. package/packages/datadog-instrumentations/src/langgraph.js +0 -7
  70. package/packages/datadog-instrumentations/src/mercurius.js +0 -11
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
- // Generated by scripts/generate-rewriter-targets.js from ./instrumentations. Do not edit by hand.
3
+ // targets.json is generated by scripts/generate-rewriter-targets.js from ./instrumentation-registry.
4
4
  const targets = require('./targets.json')
5
+ const rewriteTargetNames = new Set(Object.values(targets))
5
6
 
6
7
  const NODE_MODULES = '/node_modules/'
7
8
 
@@ -9,6 +10,7 @@ const NODE_MODULES = '/node_modules/'
9
10
  * @typedef {object} RewriteTarget
10
11
  * @property {string} moduleName
11
12
  * @property {string} filePath
13
+ * @property {boolean} [activate]
12
14
  */
13
15
 
14
16
  /**
@@ -20,13 +22,36 @@ function getRewriteTarget (filename) {
20
22
  if (nodeModulesIndex === -1) return
21
23
 
22
24
  const modulePath = filename.slice(nodeModulesIndex + NODE_MODULES.length)
25
+ if (!Object.hasOwn(targets, modulePath)) return
23
26
  const moduleName = targets[modulePath]
24
27
  if (typeof moduleName !== 'string') return
28
+ const activate = isRewriteActivationEnabled(moduleName)
25
29
 
26
- return {
30
+ const rewriteTarget = {
27
31
  moduleName,
28
32
  filePath: modulePath.slice(moduleName.length + 1),
29
33
  }
34
+ if (activate) rewriteTarget.activate = true
35
+ return rewriteTarget
30
36
  }
31
37
 
32
- module.exports = { getRewriteTarget }
38
+ function getRewriteTargetNames () {
39
+ return rewriteTargetNames.values()
40
+ }
41
+
42
+ /**
43
+ * @param {string} name
44
+ */
45
+ function isRewriteTargetName (name) {
46
+ return rewriteTargetNames.has(name)
47
+ }
48
+
49
+ /**
50
+ * @param {string} moduleName
51
+ */
52
+ function isRewriteActivationEnabled (moduleName) {
53
+ // Keep target-name-only consumers from loading every instrumentation descriptor.
54
+ return require('./instrumentation-registry').isRewriteActivationEnabled(moduleName)
55
+ }
56
+
57
+ module.exports = { getRewriteTarget, getRewriteTargetNames, isRewriteActivationEnabled, isRewriteTargetName }
@@ -30,6 +30,7 @@
30
30
  "@modelcontextprotocol/sdk/dist/esm/server/mcp.js": "@modelcontextprotocol/sdk",
31
31
  "@openai/agents-openai/dist/openaiChatCompletionsModel.js": "@openai/agents-openai",
32
32
  "@openai/agents-openai/dist/openaiChatCompletionsModel.mjs": "@openai/agents-openai",
33
+ "@playwright/test/lib/reporters/multiplexer.js": "@playwright/test",
33
34
  "@supabase/auth-js/dist/main/GoTrueClient.js": "@supabase/auth-js",
34
35
  "@supabase/auth-js/dist/module/GoTrueClient.js": "@supabase/auth-js",
35
36
  "@supabase/functions-js/dist/main/FunctionsClient.js": "@supabase/functions-js",
@@ -41,6 +42,7 @@
41
42
  "@supabase/storage-js/dist/index.cjs": "@supabase/storage-js",
42
43
  "@supabase/storage-js/dist/index.mjs": "@supabase/storage-js",
43
44
  "@wdio/cli/build/index.js": "@wdio/cli",
45
+ "@wdio/config/build/node/index.js": "@wdio/config",
44
46
  "@wdio/jasmine-framework/build/index.js": "@wdio/jasmine-framework",
45
47
  "@wdio/local-runner/build/index.js": "@wdio/local-runner",
46
48
  "@wdio/runner/build/index.js": "@wdio/runner",
@@ -77,6 +79,8 @@
77
79
  "playwright-core/lib/coreBundle.js": "playwright-core",
78
80
  "playwright/lib/index.js": "playwright",
79
81
  "playwright/lib/runner/index.js": "playwright",
82
+ "playwright/lib/runner/lastRun.js": "playwright",
83
+ "playwright/lib/runner/runner.js": "playwright",
80
84
  "postgres/cjs/src/connection.js": "postgres",
81
85
  "postgres/cjs/src/index.js": "postgres",
82
86
  "postgres/cjs/src/query.js": "postgres",
@@ -109,6 +109,10 @@ const CHILD_MESSAGE_CALL = 1
109
109
  const FLUSH_TIMEOUT = FINAL_FLUSH_TIMEOUT + FINAL_FLUSH_FALLBACK_DELAY + 5000
110
110
  const JEST_SESSION_STATE = Symbol.for('dd-trace:jest:session')
111
111
  const JEST_BAIL_REPORTER_PATH = require.resolve('./jest/bail-reporter')
112
+ const JEST_CIRCUS_TEST_RUNNER_PATH = /(?:^|[\\/])jest-circus(?:[\\/]|$)/
113
+ const UNSUPPORTED_JEST_TEST_RUNNER_WARNING =
114
+ 'dd-trace Test Optimization supports jest-circus; another test runner was detected; ' +
115
+ 'suite and test events may be incomplete.'
112
116
  const DD_JEST_HANDLE_TEST_EVENT_WRAPPED = Symbol('dd-trace:jest:handle-test-event-wrapped')
113
117
  const DD_JEST_HANDLE_TEST_EVENT_DATADOG = Symbol('dd-trace:jest:handle-test-event-datadog')
114
118
  const DD_JEST_CONCURRENT_TEST_ORIGINAL = Symbol('dd-trace:jest:concurrent-test-original')
@@ -248,6 +252,7 @@ const MINIMUM_JEST_TEST_SCHEDULER_VERSION = DD_MAJOR >= 6 ? '>=28.0.0' : '>=27.0
248
252
  const MINIMUM_JEST_COVERAGE_BACKFILL_VERSION = '>=28.0.0'
249
253
  const atrSuppressedErrors = new Map()
250
254
  let hasWarnedDeprecatedJestVersion = false
255
+ let hasWarnedUnsupportedJestTestRunner = false
251
256
  let isJestCoverageBackfillSupported = false
252
257
  let hasFinishedTestSession = false
253
258
  let jestEachBind
@@ -322,6 +327,20 @@ function warnDeprecatedJestVersion (frameworkVersion) {
322
327
  )
323
328
  }
324
329
 
330
+ /** @param {object[]} configs resolved Jest project configurations */
331
+ function warnIfUnsupportedJestTestRunner (configs) {
332
+ if (hasWarnedUnsupportedJestTestRunner || !testSessionConfigurationCh.hasSubscribers) return
333
+
334
+ const usesUnsupportedTestRunner = getEnvironmentVariable('JEST_JASMINE') === '1' ||
335
+ configs.some(config => !JEST_CIRCUS_TEST_RUNNER_PATH.test(config.testRunner))
336
+
337
+ if (usesUnsupportedTestRunner) {
338
+ hasWarnedUnsupportedJestTestRunner = true
339
+ // eslint-disable-next-line no-console
340
+ console.warn(UNSUPPORTED_JEST_TEST_RUNNER_WARNING)
341
+ }
342
+ }
343
+
325
344
  function getTestEnvironmentOptions (config) {
326
345
  if (config.projectConfig && config.projectConfig.testEnvironmentOptions) { // newer versions
327
346
  return config.projectConfig.testEnvironmentOptions
@@ -3153,6 +3172,7 @@ function getCliWrapper (isNewJestVersion) {
3153
3172
  results: {
3154
3173
  numFailedTestSuites,
3155
3174
  numFailedTests,
3175
+ numPassedTests,
3156
3176
  numRuntimeErrorTestSuites = 0,
3157
3177
  numTotalTests,
3158
3178
  numTotalTestSuites,
@@ -3323,16 +3343,19 @@ function getCliWrapper (isNewJestVersion) {
3323
3343
 
3324
3344
  // Determine session status after EFD and quarantine checks have potentially modified success
3325
3345
  let status, error
3326
- const isExpectedEmptySession = numTotalTests === 0 && numTotalTestSuites === 0
3346
+ const hasExecutedTests = numPassedTests > 0 || numFailedTests > 0 || numSuppressedQuarantinedTests > 0
3347
+ const testSessionEmptyReason = result.results.success && !hasExecutedTests
3348
+ ? (numTotalTests > 0 || isSuitesSkipped ? 'all_tests_skipped' : 'zero_tests')
3349
+ : undefined
3327
3350
  if (result.results.success) {
3328
- status = isExpectedEmptySession ? 'skip' : 'pass'
3351
+ status = testSessionEmptyReason ? 'skip' : 'pass'
3329
3352
  } else {
3330
3353
  status = 'fail'
3331
3354
  error = new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
3332
3355
  }
3333
3356
 
3334
3357
  await waitForTestSessionFinish(getTestSessionFinishPayload(status, error, {
3335
- isExpectedEmptySession: result.results.success && isExpectedEmptySession,
3358
+ testSessionEmptyReason,
3336
3359
  ...getTestSessionCoveragePayload(result.results, result.globalConfig?.rootDir),
3337
3360
  }))
3338
3361
 
@@ -3625,6 +3648,7 @@ addHook({
3625
3648
  }, jestAdapterWrapper)
3626
3649
 
3627
3650
  function configureTestEnvironment (readConfigsResult) {
3651
+ warnIfUnsupportedJestTestRunner(readConfigsResult.configs)
3628
3652
  repositoryRoot = getJestRepositoryRoot(readConfigsResult)
3629
3653
  isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
3630
3654
  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,6 +69,7 @@ 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')
@@ -76,6 +77,7 @@ const saveAutomaticVideoCh = tracingChannel('orchestrion:playwright:saveAutomati
76
77
  const pageGotoCh = tracingChannel('orchestrion:playwright-core:Page_goto')
77
78
 
78
79
  const testToCtx = new WeakMap()
80
+ const originalTestIdsByRetry = new WeakMap()
79
81
  const testSuiteToCtx = new Map()
80
82
  const testSuiteToTestStatuses = new Map()
81
83
  const testSuiteToErrors = new Map()
@@ -475,6 +477,7 @@ function deepCloneSuite (suite, filterTest, tags = [], configureCopiedTest) {
475
477
  } else {
476
478
  if (filterTest(entry)) {
477
479
  const copiedTest = entry._clone()
480
+ originalTestIdsByRetry.set(copiedTest, originalTestIdsByRetry.get(entry) ?? entry.id)
478
481
  if (configureCopiedTest) {
479
482
  configureCopiedTest(copiedTest, entry)
480
483
  }
@@ -915,6 +918,7 @@ function testEndHandler ({
915
918
  testDuration,
916
919
  testResultStatus,
917
920
  expectedStatus = test.expectedStatus,
921
+ hasNonRetriableError,
918
922
  }) {
919
923
  const {
920
924
  _requireFile: testSuiteAbsolutePath,
@@ -1087,6 +1091,8 @@ function testEndHandler ({
1087
1091
  if (testCtx) {
1088
1092
  testFinishCh.publish({
1089
1093
  testStatus,
1094
+ isExpectedFailure: testResultStatus === 'failed' && expectedStatus === 'failed',
1095
+ hasNonRetriableError,
1090
1096
  steps: testResult?.steps || [],
1091
1097
  isRetry: testResult?.retry > 0,
1092
1098
  error,
@@ -1278,7 +1284,7 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1278
1284
  videos.push(attachment)
1279
1285
  }
1280
1286
  })
1281
- worker.on('testEnd', ({ testId, status, errors, annotations, duration, expectedStatus }) => {
1287
+ worker.on('testEnd', ({ testId, status, errors, annotations, duration, expectedStatus, hasNonRetriableError }) => {
1282
1288
  const test = getTestByTestId(dispatcher, testId)
1283
1289
  if (!test) return
1284
1290
 
@@ -1300,6 +1306,7 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1300
1306
  testDuration: duration,
1301
1307
  testResultStatus: status,
1302
1308
  expectedStatus,
1309
+ hasNonRetriableError,
1303
1310
  }
1304
1311
  )
1305
1312
  const testResult = test.results.at(-1)
@@ -1406,6 +1413,8 @@ function dispatcherHook (dispatcherExport) {
1406
1413
  annotations: params.annotations,
1407
1414
  testStatus: STATUS_TO_TEST_STATUS[testResult.status],
1408
1415
  testResultStatus: testResult.status,
1416
+ expectedStatus: params.expectedStatus,
1417
+ hasNonRetriableError: params.hasNonRetriableError,
1409
1418
  error: testResult.error,
1410
1419
  isTimeout,
1411
1420
  shouldCreateTestSpan: true,
@@ -1431,7 +1440,7 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
1431
1440
 
1432
1441
  function runAllTestsWrapper (runAllTests, playwrightVersion) {
1433
1442
  // Config parameter is only available from >=1.55.0
1434
- return async function (config) {
1443
+ return async function (config, options) {
1435
1444
  // A later run must not inherit ATR settings when configuration fails or the plugin is disabled.
1436
1445
  isFlakyTestRetriesEnabled = false
1437
1446
  flakyTestRetriesCount = 0
@@ -1682,14 +1691,30 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1682
1691
  const finalStatus = hasReporterError
1683
1692
  ? 'fail'
1684
1693
  : (preventedToFail ? 'pass' : STATUS_TO_TEST_STATUS[sessionStatus])
1685
- const isExpectedEmptyShard = finalStatus === 'pass' &&
1686
- Boolean(playwrightConfig.shard) &&
1694
+ const isTestDiscovery = finalStatus === 'pass' &&
1695
+ Boolean(
1696
+ config?.listOnly ||
1697
+ config?.cliListOnly ||
1698
+ runnerConfig.cliListOnly ||
1699
+ runnerConfig._internal?.listOnly ||
1700
+ options?.listMode
1701
+ )
1702
+ const isEmptyShard = Boolean(playwrightConfig.shard) &&
1687
1703
  hasTestsBeforeSharding &&
1688
1704
  !hasTestsAssignedToShard &&
1689
1705
  testsReportedInGenerateSummary.size === 0
1706
+ const hasExecutedTests = [...testsToTestStatuses.values()].some(statuses =>
1707
+ statuses.some(status => status !== 'skip')
1708
+ )
1709
+ const hasSkippedTests = testsToTestStatuses.size > 0 || testsReportedInGenerateSummary.size > 0
1710
+ const testSessionEmptyReason = finalStatus === 'pass' && !hasExecutedTests
1711
+ ? (isTestDiscovery
1712
+ ? 'test_discovery'
1713
+ : isEmptyShard ? 'zero_test_shard' : hasSkippedTests ? 'all_tests_skipped' : 'zero_tests')
1714
+ : undefined
1690
1715
  await getChannelPromise(testSessionFinishCh, {
1691
- status: isExpectedEmptyShard ? 'skip' : finalStatus,
1692
- isExpectedEmptyShard,
1716
+ status: testSessionEmptyReason ? 'skip' : finalStatus,
1717
+ testSessionEmptyReason,
1693
1718
  error: finalizationError,
1694
1719
  isEarlyFlakeDetectionEnabled,
1695
1720
  isEarlyFlakeDetectionFaulty,
@@ -1898,6 +1923,14 @@ pageGotoCh.subscribe({
1898
1923
  },
1899
1924
  })
1900
1925
 
1926
+ // Retry clones exist only after discovery, so persist their original IDs for --last-failed.
1927
+ lastRunTestIdCh.subscribe({
1928
+ end (ctx) {
1929
+ const originalId = originalTestIdsByRetry.get(ctx.arguments[0])
1930
+ if (originalId !== undefined) ctx.result = originalId
1931
+ },
1932
+ })
1933
+
1901
1934
  reporterErrorCh.subscribe((error) => {
1902
1935
  recordReporterError(error)
1903
1936
  })
@@ -1968,6 +2001,12 @@ snapshotRecorderScreenshotPathCh.subscribe({ end: recordAutomaticFailureScreensh
1968
2001
  saveAutomaticVideoCh.subscribe({ start: recordAutomaticFailureVideoPath })
1969
2002
 
1970
2003
  if (DD_MAJOR < 6) { // <1.38.0 is only supported up to version 5
2004
+ tracingChannel('orchestrion:@playwright/test:Multiplexer_onError').subscribe({
2005
+ start (ctx) {
2006
+ recordReporterError(ctx.arguments[0])
2007
+ },
2008
+ })
2009
+
1971
2010
  addHook({
1972
2011
  name: '@playwright/test',
1973
2012
  file: 'lib/runner.js',
@@ -2656,6 +2695,8 @@ function instrumentWorkerMainMethods (workerMain) {
2656
2695
 
2657
2696
  await getChannelPromise(testFinishCh, {
2658
2697
  testStatus: STATUS_TO_TEST_STATUS[status],
2698
+ isExpectedFailure: status === 'failed' && testInfo.expectedStatus === 'failed',
2699
+ hasNonRetriableError: testInfo._hasNonRetriableError,
2659
2700
  retryTestId,
2660
2701
  deferFinalStatus: test._ddDeferFinalStatus,
2661
2702
  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)
@@ -2080,6 +2121,36 @@ addHook({
2080
2121
  return wrapTinyPool(TinyPool)
2081
2122
  })
2082
2123
 
2124
+ /**
2125
+ * Older Vitest versions expose their core instance through the logger constructor.
2126
+ * @param {Record<string, Function>} vitestPackage
2127
+ */
2128
+ function wrapVitestLogger (vitestPackage) {
2129
+ const logger = findExportByName(vitestPackage, 'Logger')
2130
+ if (logger) {
2131
+ // Hashed bundle names prevent Orchestrion's exact-file matching.
2132
+ shimmer.wrap(vitestPackage, logger.key, Logger => class extends Logger {
2133
+ constructor (...args) {
2134
+ super(...args)
2135
+ if (testSessionFinishCh.hasSubscribers) wrapVitestSession(args[0]?.constructor)
2136
+ }
2137
+ })
2138
+ }
2139
+ return vitestPackage
2140
+ }
2141
+
2142
+ addHook({
2143
+ name: 'vitest',
2144
+ versions: ['>=1.6.0 <2.0.5'],
2145
+ filePattern: 'dist/vendor/index.*',
2146
+ }, wrapVitestLogger)
2147
+
2148
+ addHook({
2149
+ name: 'vitest',
2150
+ versions: ['>=2.0.5 <3.0.0'],
2151
+ filePattern: 'dist/chunks/index.*',
2152
+ }, wrapVitestLogger)
2153
+
2083
2154
  // There are multiple index* files across different versions of vitest,
2084
2155
  // so we check for the existence of BaseSequencer to determine if we are in the right file
2085
2156
  addHook({