dd-trace 5.104.0 → 5.105.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 (151) hide show
  1. package/LICENSE-3rdparty.csv +90 -102
  2. package/index.d.ts +82 -3
  3. package/package.json +15 -15
  4. package/packages/datadog-core/src/storage.js +1 -1
  5. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  6. package/packages/datadog-instrumentations/src/ai.js +8 -7
  7. package/packages/datadog-instrumentations/src/aws-sdk.js +13 -0
  8. package/packages/datadog-instrumentations/src/azure-cosmos.js +7 -0
  9. package/packages/datadog-instrumentations/src/azure-functions.js +3 -0
  10. package/packages/datadog-instrumentations/src/cucumber.js +78 -5
  11. package/packages/datadog-instrumentations/src/dns.js +54 -18
  12. package/packages/datadog-instrumentations/src/fastify.js +142 -82
  13. package/packages/datadog-instrumentations/src/graphql.js +188 -62
  14. package/packages/datadog-instrumentations/src/helpers/ai-messages.js +322 -14
  15. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  16. package/packages/datadog-instrumentations/src/helpers/instrument.js +2 -1
  17. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +269 -0
  18. package/packages/datadog-instrumentations/src/helpers/promise-instrumentor.js +42 -0
  19. package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
  20. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -3
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-cosmos.js +50 -0
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langgraph.js +4 -2
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +85 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +37 -236
  26. package/packages/datadog-instrumentations/src/hono.js +54 -3
  27. package/packages/datadog-instrumentations/src/http/server.js +9 -4
  28. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +163 -0
  29. package/packages/datadog-instrumentations/src/jest.js +360 -150
  30. package/packages/datadog-instrumentations/src/kafkajs.js +120 -16
  31. package/packages/datadog-instrumentations/src/mocha/main.js +128 -17
  32. package/packages/datadog-instrumentations/src/nats.js +182 -0
  33. package/packages/datadog-instrumentations/src/nyc.js +38 -1
  34. package/packages/datadog-instrumentations/src/openai.js +33 -18
  35. package/packages/datadog-instrumentations/src/oracledb.js +6 -1
  36. package/packages/datadog-instrumentations/src/pino.js +17 -5
  37. package/packages/datadog-instrumentations/src/playwright.js +515 -292
  38. package/packages/datadog-instrumentations/src/router.js +76 -32
  39. package/packages/datadog-instrumentations/src/stripe.js +1 -1
  40. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -1
  41. package/packages/datadog-plugin-azure-cosmos/src/index.js +144 -0
  42. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -1
  43. package/packages/datadog-plugin-azure-functions/src/index.js +5 -2
  44. package/packages/datadog-plugin-azure-service-bus/src/producer.js +1 -1
  45. package/packages/datadog-plugin-bunyan/src/index.js +28 -0
  46. package/packages/datadog-plugin-cucumber/src/index.js +17 -3
  47. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +199 -28
  48. package/packages/datadog-plugin-cypress/src/support.js +69 -1
  49. package/packages/datadog-plugin-dns/src/lookup.js +8 -6
  50. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +1 -1
  51. package/packages/datadog-plugin-graphql/src/execute.js +2 -0
  52. package/packages/datadog-plugin-graphql/src/resolve.js +64 -67
  53. package/packages/datadog-plugin-http/src/server.js +40 -15
  54. package/packages/datadog-plugin-jest/src/index.js +11 -3
  55. package/packages/datadog-plugin-jest/src/util.js +15 -8
  56. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +1 -1
  57. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -0
  58. package/packages/datadog-plugin-langgraph/src/stream.js +1 -1
  59. package/packages/datadog-plugin-mocha/src/index.js +19 -4
  60. package/packages/datadog-plugin-mongodb-core/src/index.js +281 -40
  61. package/packages/datadog-plugin-nats/src/consumer.js +43 -0
  62. package/packages/datadog-plugin-nats/src/index.js +20 -0
  63. package/packages/datadog-plugin-nats/src/producer.js +62 -0
  64. package/packages/datadog-plugin-nats/src/util.js +33 -0
  65. package/packages/datadog-plugin-next/src/index.js +5 -3
  66. package/packages/datadog-plugin-openai/src/tracing.js +15 -2
  67. package/packages/datadog-plugin-oracledb/src/index.js +13 -2
  68. package/packages/datadog-plugin-pino/src/index.js +42 -0
  69. package/packages/datadog-plugin-playwright/src/index.js +4 -4
  70. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -1
  71. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  72. package/packages/datadog-plugin-router/src/index.js +33 -44
  73. package/packages/datadog-plugin-selenium/src/index.js +1 -1
  74. package/packages/datadog-plugin-vitest/src/index.js +5 -13
  75. package/packages/datadog-plugin-winston/src/index.js +30 -0
  76. package/packages/datadog-shimmer/src/shimmer.js +33 -40
  77. package/packages/dd-trace/src/aiguard/index.js +1 -1
  78. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  79. package/packages/dd-trace/src/appsec/api_security_sampler.js +1 -1
  80. package/packages/dd-trace/src/appsec/index.js +1 -1
  81. package/packages/dd-trace/src/appsec/reporter.js +5 -6
  82. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -1
  83. package/packages/dd-trace/src/appsec/sdk/utils.js +1 -1
  84. package/packages/dd-trace/src/appsec/user_tracking.js +5 -4
  85. package/packages/dd-trace/src/baggage.js +7 -1
  86. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +0 -1
  87. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +25 -13
  88. package/packages/dd-trace/src/ci-visibility/test-optimization-cache.js +70 -6
  89. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -2
  90. package/packages/dd-trace/src/config/supported-configurations.json +27 -8
  91. package/packages/dd-trace/src/datastreams/writer.js +2 -4
  92. package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -8
  93. package/packages/dd-trace/src/encode/0.4.js +124 -108
  94. package/packages/dd-trace/src/encode/0.5.js +114 -26
  95. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +31 -23
  96. package/packages/dd-trace/src/encode/agentless-json.js +4 -2
  97. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +32 -13
  98. package/packages/dd-trace/src/encode/span-stats.js +16 -16
  99. package/packages/dd-trace/src/encode/tags-processors.js +16 -0
  100. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  101. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  102. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  103. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +9 -7
  104. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  105. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +1 -1
  106. package/packages/dd-trace/src/llmobs/sdk.js +0 -16
  107. package/packages/dd-trace/src/llmobs/span_processor.js +3 -3
  108. package/packages/dd-trace/src/llmobs/tagger.js +9 -1
  109. package/packages/dd-trace/src/llmobs/telemetry.js +1 -1
  110. package/packages/dd-trace/src/llmobs/util.js +66 -3
  111. package/packages/dd-trace/src/log/index.js +1 -1
  112. package/packages/dd-trace/src/msgpack/chunk.js +394 -10
  113. package/packages/dd-trace/src/msgpack/index.js +96 -2
  114. package/packages/dd-trace/src/openfeature/encoding.js +70 -0
  115. package/packages/dd-trace/src/openfeature/flagging_provider.js +20 -0
  116. package/packages/dd-trace/src/openfeature/span-enrichment-hook.js +143 -0
  117. package/packages/dd-trace/src/openfeature/span-enrichment.js +149 -0
  118. package/packages/dd-trace/src/opentelemetry/span-helpers.js +4 -3
  119. package/packages/dd-trace/src/opentelemetry/span.js +1 -1
  120. package/packages/dd-trace/src/opentracing/propagation/log.js +18 -7
  121. package/packages/dd-trace/src/opentracing/propagation/text_map.js +62 -67
  122. package/packages/dd-trace/src/opentracing/span.js +59 -19
  123. package/packages/dd-trace/src/opentracing/span_context.js +49 -0
  124. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -20
  125. package/packages/dd-trace/src/plugins/database.js +7 -6
  126. package/packages/dd-trace/src/plugins/index.js +4 -0
  127. package/packages/dd-trace/src/plugins/log_injection.js +56 -0
  128. package/packages/dd-trace/src/plugins/log_plugin.js +3 -48
  129. package/packages/dd-trace/src/plugins/outbound.js +1 -1
  130. package/packages/dd-trace/src/plugins/plugin.js +15 -17
  131. package/packages/dd-trace/src/plugins/tracing.js +43 -5
  132. package/packages/dd-trace/src/plugins/util/test.js +236 -13
  133. package/packages/dd-trace/src/plugins/util/web.js +79 -65
  134. package/packages/dd-trace/src/priority_sampler.js +2 -2
  135. package/packages/dd-trace/src/profiling/profiler.js +2 -2
  136. package/packages/dd-trace/src/profiling/profilers/wall.js +10 -4
  137. package/packages/dd-trace/src/sampling_rule.js +7 -7
  138. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +10 -0
  139. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +8 -0
  140. package/packages/dd-trace/src/service-naming/source-resolver.js +46 -0
  141. package/packages/dd-trace/src/span_format.js +190 -58
  142. package/packages/dd-trace/src/spanleak.js +1 -1
  143. package/packages/dd-trace/src/standalone/index.js +3 -3
  144. package/packages/dd-trace/src/tagger.js +0 -2
  145. package/vendor/dist/@apm-js-collab/code-transformer/index.js +70 -39
  146. package/vendor/dist/@datadog/sketches-js/LICENSE +10 -36
  147. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  148. package/vendor/dist/protobufjs/index.js +1 -1
  149. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  150. package/packages/dd-trace/src/msgpack/encoder.js +0 -308
  151. package/packages/dd-trace/src/plugins/structured_log_plugin.js +0 -9
@@ -2,6 +2,7 @@
2
2
 
3
3
  // Capture real timers at module load time, before any test can install fake timers.
4
4
  const realSetTimeout = setTimeout
5
+ const realClearTimeout = clearTimeout
5
6
 
6
7
  const { performance } = require('node:perf_hooks')
7
8
  const satisfies = require('../../../vendor/dist/semifies')
@@ -25,7 +26,7 @@ const {
25
26
  getValueFromEnvSources,
26
27
  } = require('../../dd-trace/src/config/helper')
27
28
  const { DD_MAJOR } = require('../../../version')
28
- const { addHook, channel } = require('./helpers/instrument')
29
+ const { addHook, channel, tracingChannel } = require('./helpers/instrument')
29
30
 
30
31
  const testStartCh = channel('ci:playwright:test:start')
31
32
  const testFinishCh = channel('ci:playwright:test:finish')
@@ -46,6 +47,12 @@ const testSuiteFinishCh = channel('ci:playwright:test-suite:finish')
46
47
  const workerReportCh = channel('ci:playwright:worker:report')
47
48
  const testPageGotoCh = channel('ci:playwright:test:page-goto')
48
49
 
50
+ const dispatcherRunCh = tracingChannel('orchestrion:playwright:Dispatcher_run')
51
+ const dispatcherCreateWorkerCh = tracingChannel('orchestrion:playwright:Dispatcher_createWorker')
52
+ const processHostStartRunnerCh = tracingChannel('orchestrion:playwright:ProcessHost_startRunner')
53
+ const createRootSuiteCh = tracingChannel('orchestrion:playwright:createRootSuite')
54
+ const pageGotoCh = tracingChannel('orchestrion:playwright-core:Page_goto')
55
+
49
56
  const testToCtx = new WeakMap()
50
57
  const testSuiteToCtx = new Map()
51
58
  const testSuiteToTestStatuses = new Map()
@@ -53,6 +60,7 @@ const testSuiteToErrors = new Map()
53
60
  const testsToTestStatuses = new Map()
54
61
 
55
62
  const RUM_FLUSH_WAIT_TIME = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')) || 500
63
+ const DD_PROPERTIES_TIMEOUT = 5000
56
64
 
57
65
  let applyRepeatEachIndex = null
58
66
 
@@ -95,12 +103,17 @@ const efdRetryTestsById = new Map()
95
103
  const efdScheduledOriginalTestKeys = new Set()
96
104
  const efdStartedOriginalTestKeys = new Set()
97
105
  const efdSlowAbortedTests = new Set()
106
+ const ddPropertiesByTestId = new Map()
107
+ const ddPropertiesRequestsByTestId = new Map()
98
108
  let rootDir = ''
99
109
  let sessionProjects = []
100
110
 
101
111
  const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0' // TODO: remove this once we drop support for v5
102
112
  const EFD_RETRY_COUNT_REQUEST = 'ddEfdRetryCountRequest'
103
113
  const EFD_RETRY_COUNT_RESPONSE = 'ddEfdRetryCountResponse'
114
+ const DD_PROPERTIES_REQUEST = 'ddPropertiesRequest'
115
+ const DD_PROPERTIES_RESPONSE = 'ddProperties'
116
+ const kDdPlaywrightWorkerInstrumented = Symbol('ddPlaywrightWorkerInstrumented')
104
117
 
105
118
  function isValidKnownTests (receivedKnownTests) {
106
119
  return !!receivedKnownTests.playwright
@@ -281,6 +294,43 @@ function sendEfdRetryCountToWorkerWhenAvailable (workerProcess, testId) {
281
294
  })
282
295
  }
283
296
 
297
+ function sendDdPropertiesToWorker (workerProcess, testId, properties) {
298
+ workerProcess.send({
299
+ type: DD_PROPERTIES_RESPONSE,
300
+ testId,
301
+ properties,
302
+ })
303
+ }
304
+
305
+ function setDdPropertiesForTest (workerProcess, testId, properties) {
306
+ ddPropertiesByTestId.set(testId, properties)
307
+
308
+ const requests = ddPropertiesRequestsByTestId.get(testId)
309
+ if (requests) {
310
+ ddPropertiesRequestsByTestId.delete(testId)
311
+ for (const resolveRequest of requests) {
312
+ resolveRequest(properties)
313
+ }
314
+ }
315
+
316
+ sendDdPropertiesToWorker(workerProcess, testId, properties)
317
+ }
318
+
319
+ function sendDdPropertiesToWorkerWhenAvailable (workerProcess, testId) {
320
+ const properties = ddPropertiesByTestId.get(testId)
321
+ if (properties) {
322
+ sendDdPropertiesToWorker(workerProcess, testId, properties)
323
+ return
324
+ }
325
+
326
+ if (!ddPropertiesRequestsByTestId.has(testId)) {
327
+ ddPropertiesRequestsByTestId.set(testId, [])
328
+ }
329
+ ddPropertiesRequestsByTestId.get(testId).push((properties) => {
330
+ sendDdPropertiesToWorker(workerProcess, testId, properties)
331
+ })
332
+ }
333
+
284
334
  /**
285
335
  * @param {object} test
286
336
  * @returns {boolean}
@@ -354,11 +404,15 @@ function getSuiteType (test, type) {
354
404
  return suite
355
405
  }
356
406
 
407
+ function isSuiteEntry (entry) {
408
+ return entry.constructor.name === 'Suite' || entry.constructor.name === '_Suite'
409
+ }
410
+
357
411
  // Copy of Suite#_deepClone but with a function to filter tests
358
412
  function deepCloneSuite (suite, filterTest, tags = [], configureCopiedTest) {
359
413
  const copy = suite._clone()
360
414
  for (const entry of suite._entries) {
361
- if (entry.constructor.name === 'Suite') {
415
+ if (isSuiteEntry(entry)) {
362
416
  copy._addSuite(deepCloneSuite(entry, filterTest, tags, configureCopiedTest))
363
417
  } else {
364
418
  if (filterTest(entry)) {
@@ -445,6 +499,10 @@ function getProjectsFromRunner (runner, configArg) {
445
499
  }
446
500
 
447
501
  function getProjectsFromDispatcher (dispatcher) {
502
+ const bundledConfig = dispatcher._testRun?.config?.config?.projects
503
+ if (bundledConfig) {
504
+ return bundledConfig
505
+ }
448
506
  const newConfig = dispatcher._config?.config?.projects
449
507
  if (newConfig) {
450
508
  return newConfig
@@ -888,31 +946,118 @@ function deferEfdRetryGroups (testGroups) {
888
946
  return [...groupsWithOriginalTests, ...efdRetryOnlyGroups]
889
947
  }
890
948
 
949
+ function prepareDispatcherRun (dispatcher, args) {
950
+ let testGroups = args[0]
951
+
952
+ // Filter out disabled tests from testGroups before they get scheduled,
953
+ // unless they have attemptToFix (in which case they should still run and be retried)
954
+ if (isTestManagementTestsEnabled) {
955
+ for (const group of testGroups) {
956
+ group.tests = group.tests.filter(test => !test._ddIsDisabled || test._ddIsAttemptToFix)
957
+ }
958
+ // Remove empty groups
959
+ testGroups = testGroups.filter(group => group.tests.length > 0)
960
+ }
961
+
962
+ if (isEarlyFlakeDetectionEnabled) {
963
+ testGroups = deferEfdRetryGroups(testGroups)
964
+ }
965
+
966
+ if (!dispatcher._allTests) {
967
+ // Removed in https://github.com/microsoft/playwright/commit/1e52c37b254a441cccf332520f60225a5acc14c7
968
+ // Not available from >=1.44.0
969
+ dispatcher._ddAllTests = testGroups.flatMap(g => g.tests)
970
+ }
971
+ remainingTestsByFile = getTestsBySuiteFromTestGroups(testGroups)
972
+ args[0] = testGroups
973
+ }
974
+
891
975
  function dispatcherRunWrapperNew (run) {
892
- return function (testGroups) {
893
- // Filter out disabled tests from testGroups before they get scheduled,
894
- // unless they have attemptToFix (in which case they should still run and be retried)
895
- if (isTestManagementTestsEnabled) {
896
- for (const group of testGroups) {
897
- group.tests = group.tests.filter(test => !test._ddIsDisabled || test._ddIsAttemptToFix)
898
- }
899
- // Remove empty groups
900
- testGroups = testGroups.filter(group => group.tests.length > 0)
976
+ return function () {
977
+ prepareDispatcherRun(this, arguments)
978
+ return run.apply(this, arguments)
979
+ }
980
+ }
981
+
982
+ function onDispatcherCreateWorker (dispatcher, worker) {
983
+ if (!worker) {
984
+ return worker
985
+ }
986
+
987
+ const projects = getProjectsFromDispatcher(dispatcher)
988
+ sessionProjects = projects
989
+
990
+ worker.on('testBegin', ({ testId }) => {
991
+ const test = getTestByTestId(dispatcher, testId)
992
+ const browser = getBrowserNameFromProjects(projects, test)
993
+ const shouldCreateTestSpan = test.expectedStatus === 'skipped'
994
+ testBeginHandler(test, browser, shouldCreateTestSpan)
995
+ })
996
+ worker.on('testEnd', ({ testId, status, errors, annotations }) => {
997
+ const test = getTestByTestId(dispatcher, testId)
998
+
999
+ const isTimeout = status === 'timedOut'
1000
+ const testStatus = STATUS_TO_TEST_STATUS[status]
1001
+ const shouldCreateTestSpan = test.expectedStatus === 'skipped'
1002
+ if (shouldCreateTestSpan && !testToCtx.has(test)) {
1003
+ testBeginHandler(test, getBrowserNameFromProjects(projects, test), true)
901
1004
  }
1005
+ testEndHandler(
1006
+ {
1007
+ test,
1008
+ annotations,
1009
+ testStatus,
1010
+ error: errors && errors[0],
1011
+ isTimeout,
1012
+ shouldCreateTestSpan,
1013
+ projects,
1014
+ }
1015
+ )
1016
+ const testResult = test.results.at(-1)
1017
+ const isAtrRetry = testResult?.retry > 0 &&
1018
+ isFlakyTestRetriesEnabled &&
1019
+ !test._ddIsAttemptToFix &&
1020
+ !test._ddIsEfdRetry
902
1021
 
903
- if (isEarlyFlakeDetectionEnabled) {
904
- testGroups = deferEfdRetryGroups(testGroups)
1022
+ // EFD retries (new or modified tests) are implemented as clones with retries=0,
1023
+ // so testWillRetry always returns false for them. Instead, we track how many
1024
+ // executions have been reported via testsToTestStatuses (updated by testEndHandler
1025
+ // above) and mark the execution final once the count reaches the expected total.
1026
+ // This mirrors how ATF finality is detected and centralizes the decision in the
1027
+ // main process, so workers only need to act on the _ddIsFinalExecution flag.
1028
+ const isEfdManagedTest = isTestEfdManaged(test)
1029
+ let isFinalExecution
1030
+ if (isEfdManagedTest) {
1031
+ const efdTestStatuses = testsToTestStatuses.get(getTestEfdKey(test)) || []
1032
+ isFinalExecution = efdTestStatuses.length === getEfdRetryCountForTest(test) + 1
1033
+ } else if (test._ddIsAttemptToFix) {
1034
+ isFinalExecution = !!(test._ddHasPassedAttemptToFixRetries || test._ddHasFailedAttemptToFixRetries)
1035
+ } else {
1036
+ isFinalExecution = !testWillRetry(test, testStatus)
905
1037
  }
906
1038
 
907
- if (!this._allTests) {
908
- // Removed in https://github.com/microsoft/playwright/commit/1e52c37b254a441cccf332520f60225a5acc14c7
909
- // Not available from >=1.44.0
910
- this._ddAllTests = testGroups.flatMap(g => g.tests)
1039
+ const ddProperties = {
1040
+ _ddIsDisabled: test._ddIsDisabled,
1041
+ _ddIsQuarantined: test._ddIsQuarantined,
1042
+ _ddIsAttemptToFix: test._ddIsAttemptToFix,
1043
+ _ddIsAttemptToFixRetry: test._ddIsAttemptToFixRetry,
1044
+ _ddIsNew: test._ddIsNew,
1045
+ _ddIsEfdRetry: test._ddIsEfdRetry,
1046
+ _ddHasFailedAllRetries: test._ddHasFailedAllRetries,
1047
+ _ddHasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
1048
+ _ddHasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
1049
+ _ddIsAtrRetry: isAtrRetry,
1050
+ _ddIsModified: test._ddIsModified,
1051
+ _ddIsFinalExecution: isFinalExecution,
1052
+ _ddIsEfdManagedTest: isEfdManagedTest,
1053
+ _ddEarlyFlakeAbortReason: efdSlowAbortedTests.has(getTestEfdKey(test)) ? 'slow' : undefined,
1054
+ _ddHasPassedAnyEfdAttempt: (testsToTestStatuses.get(getTestEfdKey(test)) || []).includes('pass'),
911
1055
  }
912
- remainingTestsByFile = getTestsBySuiteFromTestGroups(testGroups)
913
- arguments[0] = testGroups
914
- return run.apply(this, arguments)
915
- }
1056
+
1057
+ setDdPropertiesForTest(worker.process, test.id, ddProperties)
1058
+ })
1059
+
1060
+ return worker
916
1061
  }
917
1062
 
918
1063
  function dispatcherHook (dispatcherExport) {
@@ -960,82 +1105,7 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
960
1105
  shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function (...args) {
961
1106
  const dispatcher = this
962
1107
  const worker = createWorker.apply(this, args)
963
- const projects = getProjectsFromDispatcher(dispatcher)
964
- sessionProjects = projects
965
-
966
- worker.on('testBegin', ({ testId }) => {
967
- const test = getTestByTestId(dispatcher, testId)
968
- const browser = getBrowserNameFromProjects(projects, test)
969
- const shouldCreateTestSpan = test.expectedStatus === 'skipped'
970
- testBeginHandler(test, browser, shouldCreateTestSpan)
971
- })
972
- worker.on('testEnd', ({ testId, status, errors, annotations }) => {
973
- const test = getTestByTestId(dispatcher, testId)
974
-
975
- const isTimeout = status === 'timedOut'
976
- const testStatus = STATUS_TO_TEST_STATUS[status]
977
- const shouldCreateTestSpan = test.expectedStatus === 'skipped'
978
- if (shouldCreateTestSpan && !testToCtx.has(test)) {
979
- testBeginHandler(test, getBrowserNameFromProjects(projects, test), true)
980
- }
981
- testEndHandler(
982
- {
983
- test,
984
- annotations,
985
- testStatus,
986
- error: errors && errors[0],
987
- isTimeout,
988
- shouldCreateTestSpan,
989
- projects,
990
- }
991
- )
992
- const testResult = test.results.at(-1)
993
- const isAtrRetry = testResult?.retry > 0 &&
994
- isFlakyTestRetriesEnabled &&
995
- !test._ddIsAttemptToFix &&
996
- !test._ddIsEfdRetry
997
-
998
- // EFD retries (new or modified tests) are implemented as clones with retries=0,
999
- // so testWillRetry always returns false for them. Instead, we track how many
1000
- // executions have been reported via testsToTestStatuses (updated by testEndHandler
1001
- // above) and mark the execution final once the count reaches the expected total.
1002
- // This mirrors how ATF finality is detected and centralizes the decision in the
1003
- // main process, so workers only need to act on the _ddIsFinalExecution flag.
1004
- const isEfdManagedTest = isTestEfdManaged(test)
1005
- let isFinalExecution
1006
- if (isEfdManagedTest) {
1007
- const efdTestStatuses = testsToTestStatuses.get(getTestEfdKey(test)) || []
1008
- isFinalExecution = efdTestStatuses.length === getEfdRetryCountForTest(test) + 1
1009
- } else if (test._ddIsAttemptToFix) {
1010
- isFinalExecution = !!(test._ddHasPassedAttemptToFixRetries || test._ddHasFailedAttemptToFixRetries)
1011
- } else {
1012
- isFinalExecution = !testWillRetry(test, testStatus)
1013
- }
1014
-
1015
- // We want to send the ddProperties to the worker
1016
- worker.process.send({
1017
- type: 'ddProperties',
1018
- testId: test.id,
1019
- properties: {
1020
- _ddIsDisabled: test._ddIsDisabled,
1021
- _ddIsQuarantined: test._ddIsQuarantined,
1022
- _ddIsAttemptToFix: test._ddIsAttemptToFix,
1023
- _ddIsAttemptToFixRetry: test._ddIsAttemptToFixRetry,
1024
- _ddIsNew: test._ddIsNew,
1025
- _ddIsEfdRetry: test._ddIsEfdRetry,
1026
- _ddHasFailedAllRetries: test._ddHasFailedAllRetries,
1027
- _ddHasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
1028
- _ddHasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
1029
- _ddIsAtrRetry: isAtrRetry,
1030
- _ddIsModified: test._ddIsModified,
1031
- _ddIsFinalExecution: isFinalExecution,
1032
- _ddIsEfdManagedTest: isEfdManagedTest,
1033
- _ddEarlyFlakeAbortReason: efdSlowAbortedTests.has(getTestEfdKey(test)) ? 'slow' : undefined,
1034
- _ddHasPassedAnyEfdAttempt: (testsToTestStatuses.get(getTestEfdKey(test)) || []).includes('pass'),
1035
- },
1036
- })
1037
- })
1038
- return worker
1108
+ return onDispatcherCreateWorker(dispatcher, worker)
1039
1109
  })
1040
1110
  return dispatcherExport
1041
1111
  }
@@ -1046,7 +1116,6 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1046
1116
  let onDone
1047
1117
 
1048
1118
  rootDir = getRootDir(this, config)
1049
-
1050
1119
  const processArgv = process.argv.slice(2).join(' ')
1051
1120
  const command = `playwright ${processArgv}`
1052
1121
  testSessionStartCh.publish({ command, frameworkVersion: playwrightVersion, rootDir })
@@ -1233,6 +1302,8 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1233
1302
  efdScheduledOriginalTestKeys.clear()
1234
1303
  efdStartedOriginalTestKeys.clear()
1235
1304
  efdSlowAbortedTests.clear()
1305
+ ddPropertiesByTestId.clear()
1306
+ ddPropertiesRequestsByTestId.clear()
1236
1307
 
1237
1308
  // TODO: we can trick playwright into thinking the session passed by returning
1238
1309
  // 'passed' here. We might be able to use this for both EFD and Test Management tests.
@@ -1259,6 +1330,85 @@ function runnerHookNew (runnerExport, playwrightVersion) {
1259
1330
  return runnerExport
1260
1331
  }
1261
1332
 
1333
+ function runnerIndexHook (runnerExport, playwrightVersion) {
1334
+ let wrappedTestRunner
1335
+ runnerExport = shimmer.wrap(runnerExport, 'testRunner', function (originalGetter) {
1336
+ return function () {
1337
+ if (!wrappedTestRunner) {
1338
+ wrappedTestRunner = runnerHookNew(originalGetter.call(this), playwrightVersion)
1339
+ }
1340
+ return wrappedTestRunner
1341
+ }
1342
+ })
1343
+
1344
+ const baseReporter = runnerExport.base?.TerminalReporter
1345
+ if (baseReporter) {
1346
+ shimmer.wrap(baseReporter.prototype, 'generateSummary', generateSummaryWrapper)
1347
+ }
1348
+
1349
+ return runnerExport
1350
+ }
1351
+
1352
+ function commonIndexHook (commonExport) {
1353
+ applyRepeatEachIndex = commonExport.suiteUtils?.applyRepeatEachIndex
1354
+
1355
+ let wrappedStartProcessRunner
1356
+ commonExport = shimmer.wrap(commonExport, 'startProcessRunner', function (originalGetter) {
1357
+ return function () {
1358
+ if (!wrappedStartProcessRunner) {
1359
+ const startProcessRunner = originalGetter.call(this)
1360
+ wrappedStartProcessRunner = function (create) {
1361
+ return startProcessRunner.call(this, function () {
1362
+ const processRunner = create.apply(this, arguments)
1363
+ instrumentWorkerMainMethods(processRunner)
1364
+ return processRunner
1365
+ })
1366
+ }
1367
+ }
1368
+ return wrappedStartProcessRunner
1369
+ }
1370
+ })
1371
+
1372
+ return commonExport
1373
+ }
1374
+
1375
+ dispatcherRunCh.subscribe({
1376
+ start (ctx) {
1377
+ prepareDispatcherRun(ctx.self, ctx.arguments)
1378
+ },
1379
+ })
1380
+
1381
+ dispatcherCreateWorkerCh.subscribe({
1382
+ end (ctx) {
1383
+ onDispatcherCreateWorker(ctx.self, ctx.result)
1384
+ },
1385
+ })
1386
+
1387
+ processHostStartRunnerCh.subscribe({
1388
+ start (ctx) {
1389
+ prepareProcessHostStartRunner(ctx.self)
1390
+ },
1391
+ asyncEnd (ctx) {
1392
+ finishProcessHostStartRunner(ctx.self)
1393
+ },
1394
+ })
1395
+
1396
+ createRootSuiteCh.subscribe({
1397
+ asyncEnd (ctx) {
1398
+ if (ctx.error) {
1399
+ return
1400
+ }
1401
+ processRootSuite(ctx.result || ctx.arguments?.[0])
1402
+ },
1403
+ })
1404
+
1405
+ pageGotoCh.subscribe({
1406
+ asyncEnd (ctx) {
1407
+ // The Page.goto rewriter waits for this so tests closing immediately after navigation still get RUM tags.
1408
+ ctx.asyncEndPromise = handlePageGoto(ctx.self)
1409
+ },
1410
+ })
1411
+
1262
1412
  if (DD_MAJOR < 6) { // <1.38.0 is only supported up to version 5
1263
1413
  addHook({
1264
1414
  name: '@playwright/test',
@@ -1291,28 +1441,40 @@ if (DD_MAJOR < 6) { // <1.38.0 is only supported up to version 5
1291
1441
  }, runnerHook)
1292
1442
  }
1293
1443
 
1444
+ addHook({
1445
+ name: 'playwright',
1446
+ file: 'lib/runner/index.js',
1447
+ versions: ['>=1.60.0'],
1448
+ }, runnerIndexHook)
1449
+
1450
+ addHook({
1451
+ name: 'playwright',
1452
+ file: 'lib/common/index.js',
1453
+ versions: ['>=1.60.0'],
1454
+ }, commonIndexHook)
1455
+
1294
1456
  addHook({
1295
1457
  name: 'playwright',
1296
1458
  file: 'lib/runner/runner.js',
1297
- versions: ['>=1.38.0'],
1459
+ versions: ['>=1.38.0 <1.60.0'],
1298
1460
  }, runnerHook)
1299
1461
 
1300
1462
  addHook({
1301
1463
  name: 'playwright',
1302
1464
  file: 'lib/runner/testRunner.js',
1303
- versions: ['>=1.55.0'],
1465
+ versions: ['>=1.55.0 <1.60.0'],
1304
1466
  }, runnerHookNew)
1305
1467
 
1306
1468
  addHook({
1307
1469
  name: 'playwright',
1308
1470
  file: 'lib/runner/dispatcher.js',
1309
- versions: ['>=1.38.0'],
1471
+ versions: ['>=1.38.0 <1.60.0'],
1310
1472
  }, (dispatcher) => dispatcherHookNew(dispatcher, dispatcherRunWrapperNew))
1311
1473
 
1312
1474
  addHook({
1313
1475
  name: 'playwright',
1314
1476
  file: 'lib/common/suiteUtils.js',
1315
- versions: ['>=1.38.0'],
1477
+ versions: ['>=1.38.0 <1.60.0'],
1316
1478
  }, suiteUtilsPackage => {
1317
1479
  // We grab `applyRepeatEachIndex` to use it later
1318
1480
  // `applyRepeatEachIndex` needs to be applied to a cloned suite
@@ -1361,102 +1523,142 @@ function applyRetriesToTests (
1361
1523
  }
1362
1524
  }
1363
1525
 
1364
- addHook({
1365
- name: 'playwright',
1366
- file: 'lib/runner/loadUtils.js',
1367
- versions: ['>=1.38.0'],
1368
- }, (loadUtilsPackage) => {
1369
- const oldCreateRootSuite = loadUtilsPackage.createRootSuite
1526
+ function processRootSuite (createRootSuiteReturnValue) {
1527
+ if (!isKnownTestsEnabled && !isTestManagementTestsEnabled && !isImpactedTestsEnabled) {
1528
+ return createRootSuiteReturnValue
1529
+ }
1370
1530
 
1371
- async function newCreateRootSuite () {
1372
- if (!isKnownTestsEnabled && !isTestManagementTestsEnabled && !isImpactedTestsEnabled) {
1373
- return oldCreateRootSuite.apply(this, arguments)
1374
- }
1531
+ if (!createRootSuiteReturnValue) {
1532
+ return createRootSuiteReturnValue
1533
+ }
1375
1534
 
1376
- const createRootSuiteReturnValue = await oldCreateRootSuite.apply(this, arguments)
1377
- // From v1.56.0 on, createRootSuite returns `{ rootSuite, topLevelProjects }`
1378
- const rootSuite = createRootSuiteReturnValue.rootSuite || createRootSuiteReturnValue
1379
-
1380
- const allTests = rootSuite.allTests()
1381
-
1382
- if (isTestManagementTestsEnabled) {
1383
- const fileSuitesWithManagedTestsToProjects = new Map()
1384
- for (const test of allTests) {
1385
- const testProperties = getTestProperties(test)
1386
- // Disabled tests are skipped unless they have attemptToFix
1387
- if (testProperties.disabled) {
1388
- test._ddIsDisabled = true
1389
- if (!testProperties.attemptToFix) {
1390
- test.expectedStatus = 'skipped'
1391
- // setting test.expectedStatus to 'skipped' does not work for every case,
1392
- // so we need to filter out disabled tests in dispatcherRunWrapperNew,
1393
- // so they don't get to the workers
1394
- continue
1395
- }
1535
+ // From v1.56.0 on, createRootSuite returns `{ rootSuite, topLevelProjects }`
1536
+ const rootSuite = createRootSuiteReturnValue.rootSuite || createRootSuiteReturnValue
1537
+ if (typeof rootSuite?.allTests !== 'function') {
1538
+ return createRootSuiteReturnValue
1539
+ }
1540
+
1541
+ const allTests = rootSuite.allTests()
1542
+
1543
+ if (isTestManagementTestsEnabled) {
1544
+ const fileSuitesWithManagedTestsToProjects = new Map()
1545
+ for (const test of allTests) {
1546
+ const testProperties = getTestProperties(test)
1547
+ // Disabled tests are skipped unless they have attemptToFix
1548
+ if (testProperties.disabled) {
1549
+ test._ddIsDisabled = true
1550
+ if (!testProperties.attemptToFix) {
1551
+ test.expectedStatus = 'skipped'
1552
+ // setting test.expectedStatus to 'skipped' does not work for every case,
1553
+ // so we need to filter out disabled tests in dispatcherRunWrapperNew,
1554
+ // so they don't get to the workers
1555
+ continue
1396
1556
  }
1397
- if (testProperties.quarantined) {
1398
- test._ddIsQuarantined = true
1399
- if (!testProperties.attemptToFix) {
1400
- // Do not skip quarantined tests, let them run and overwrite results post-run if they fail
1401
- const testFqn = getTestFullyQualifiedName(test)
1402
- quarantinedButNotAttemptToFixFqns.add(testFqn)
1403
- }
1557
+ }
1558
+ if (testProperties.quarantined) {
1559
+ test._ddIsQuarantined = true
1560
+ if (!testProperties.attemptToFix) {
1561
+ // Do not skip quarantined tests, let them run and overwrite results post-run if they fail
1562
+ const testFqn = getTestFullyQualifiedName(test)
1563
+ quarantinedButNotAttemptToFixFqns.add(testFqn)
1404
1564
  }
1405
- if (testProperties.attemptToFix) {
1406
- test._ddIsAttemptToFix = true
1407
- // Prevent ATR or `--retries` from retrying attemptToFix tests
1408
- test.retries = 0
1409
- const fileSuite = getSuiteType(test, 'file')
1410
-
1411
- if (!fileSuitesWithManagedTestsToProjects.has(fileSuite)) {
1412
- fileSuitesWithManagedTestsToProjects.set(fileSuite, getSuiteType(test, 'project'))
1413
- }
1565
+ }
1566
+ if (testProperties.attemptToFix) {
1567
+ test._ddIsAttemptToFix = true
1568
+ // Prevent ATR or `--retries` from retrying attemptToFix tests
1569
+ test.retries = 0
1570
+ const fileSuite = getSuiteType(test, 'file')
1571
+
1572
+ if (!fileSuitesWithManagedTestsToProjects.has(fileSuite)) {
1573
+ fileSuitesWithManagedTestsToProjects.set(fileSuite, getSuiteType(test, 'project'))
1414
1574
  }
1415
1575
  }
1416
- applyRetriesToTests(
1417
- fileSuitesWithManagedTestsToProjects,
1418
- (test) => test._ddIsAttemptToFix,
1419
- [
1420
- (test) => test._ddIsQuarantined && '_ddIsQuarantined',
1421
- (test) => test._ddIsDisabled && '_ddIsDisabled',
1422
- '_ddIsAttemptToFix',
1423
- '_ddIsAttemptToFixRetry',
1424
- ],
1425
- testManagementAttemptToFixRetries
1426
- )
1427
1576
  }
1577
+ applyRetriesToTests(
1578
+ fileSuitesWithManagedTestsToProjects,
1579
+ (test) => test._ddIsAttemptToFix,
1580
+ [
1581
+ (test) => test._ddIsQuarantined && '_ddIsQuarantined',
1582
+ (test) => test._ddIsDisabled && '_ddIsDisabled',
1583
+ '_ddIsAttemptToFix',
1584
+ '_ddIsAttemptToFixRetry',
1585
+ ],
1586
+ testManagementAttemptToFixRetries
1587
+ )
1588
+ }
1428
1589
 
1429
- if (isImpactedTestsEnabled) {
1430
- const impactedTests = allTests.filter(test => {
1431
- let isImpacted = false
1432
- isModifiedCh.publish({
1433
- filePath: test._requireFile,
1434
- modifiedFiles,
1435
- onDone: (isModified) => { isImpacted = isModified },
1436
- })
1437
- return isImpacted
1590
+ if (isImpactedTestsEnabled) {
1591
+ const impactedTests = allTests.filter(test => {
1592
+ let isImpacted = false
1593
+ isModifiedCh.publish({
1594
+ filePath: test._requireFile,
1595
+ modifiedFiles,
1596
+ onDone: (isModified) => { isImpacted = isModified },
1438
1597
  })
1598
+ return isImpacted
1599
+ })
1439
1600
 
1440
- const fileSuitesWithImpactedTestsToProjects = new Map()
1441
- for (const impactedTest of impactedTests) {
1442
- impactedTest._ddIsModified = true
1443
- if (isEarlyFlakeDetectionEnabled && impactedTest.expectedStatus !== 'skipped') {
1444
- markEfdManagedTest(impactedTest)
1445
- const fileSuite = getSuiteType(impactedTest, 'file')
1446
- if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) {
1447
- fileSuitesWithImpactedTestsToProjects.set(fileSuite, getSuiteType(impactedTest, 'project'))
1601
+ const fileSuitesWithImpactedTestsToProjects = new Map()
1602
+ for (const impactedTest of impactedTests) {
1603
+ impactedTest._ddIsModified = true
1604
+ if (isEarlyFlakeDetectionEnabled && impactedTest.expectedStatus !== 'skipped') {
1605
+ markEfdManagedTest(impactedTest)
1606
+ const fileSuite = getSuiteType(impactedTest, 'file')
1607
+ if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) {
1608
+ fileSuitesWithImpactedTestsToProjects.set(fileSuite, getSuiteType(impactedTest, 'project'))
1609
+ }
1610
+ }
1611
+ }
1612
+ // If something change in the file, all tests in the file are impacted, hence the () => true filter
1613
+ applyRetriesToTests(
1614
+ fileSuitesWithImpactedTestsToProjects,
1615
+ () => true,
1616
+ [
1617
+ '_ddIsModified',
1618
+ '_ddIsEfdRetry',
1619
+ (test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null),
1620
+ ],
1621
+ getConfiguredEfdRetryCount(),
1622
+ (copiedTest, originalTest, retryIndex) => {
1623
+ markEfdRetryTest(copiedTest, retryIndex, originalTest)
1624
+ markEfdManagedTest(copiedTest)
1625
+ },
1626
+ getEfdRetryRepeatEachIndex
1627
+ )
1628
+ }
1629
+
1630
+ if (isKnownTestsEnabled) {
1631
+ const newTests = allTests.filter(isNewTest)
1632
+
1633
+ const isFaulty = getIsFaultyEarlyFlakeDetection(
1634
+ allTests.map(test => getTestSuitePath(test._requireFile, rootDir)),
1635
+ knownTests.playwright,
1636
+ earlyFlakeDetectionFaultyThreshold
1637
+ )
1638
+
1639
+ if (isFaulty) {
1640
+ isEarlyFlakeDetectionEnabled = false
1641
+ isKnownTestsEnabled = false
1642
+ isEarlyFlakeDetectionFaulty = true
1643
+ } else {
1644
+ const fileSuitesWithNewTestsToProjects = new Map()
1645
+ for (const newTest of newTests) {
1646
+ newTest._ddIsNew = true
1647
+ if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
1648
+ // Prevent ATR or `--retries` from retrying new tests if EFD is enabled
1649
+ newTest.retries = 0
1650
+ markEfdManagedTest(newTest)
1651
+ const fileSuite = getSuiteType(newTest, 'file')
1652
+ if (!fileSuitesWithNewTestsToProjects.has(fileSuite)) {
1653
+ fileSuitesWithNewTestsToProjects.set(fileSuite, getSuiteType(newTest, 'project'))
1448
1654
  }
1449
1655
  }
1450
1656
  }
1451
- // If something change in the file, all tests in the file are impacted, hence the () => true filter
1657
+
1452
1658
  applyRetriesToTests(
1453
- fileSuitesWithImpactedTestsToProjects,
1454
- () => true,
1455
- [
1456
- '_ddIsModified',
1457
- '_ddIsEfdRetry',
1458
- (test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null),
1459
- ],
1659
+ fileSuitesWithNewTestsToProjects,
1660
+ isNewTest,
1661
+ ['_ddIsNew', '_ddIsEfdRetry'],
1460
1662
  getConfiguredEfdRetryCount(),
1461
1663
  (copiedTest, originalTest, retryIndex) => {
1462
1664
  markEfdRetryTest(copiedTest, retryIndex, originalTest)
@@ -1465,50 +1667,20 @@ addHook({
1465
1667
  getEfdRetryRepeatEachIndex
1466
1668
  )
1467
1669
  }
1670
+ }
1468
1671
 
1469
- if (isKnownTestsEnabled) {
1470
- const newTests = allTests.filter(isNewTest)
1471
-
1472
- const isFaulty = getIsFaultyEarlyFlakeDetection(
1473
- allTests.map(test => getTestSuitePath(test._requireFile, rootDir)),
1474
- knownTests.playwright,
1475
- earlyFlakeDetectionFaultyThreshold
1476
- )
1477
-
1478
- if (isFaulty) {
1479
- isEarlyFlakeDetectionEnabled = false
1480
- isKnownTestsEnabled = false
1481
- isEarlyFlakeDetectionFaulty = true
1482
- } else {
1483
- const fileSuitesWithNewTestsToProjects = new Map()
1484
- for (const newTest of newTests) {
1485
- newTest._ddIsNew = true
1486
- if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
1487
- // Prevent ATR or `--retries` from retrying new tests if EFD is enabled
1488
- newTest.retries = 0
1489
- markEfdManagedTest(newTest)
1490
- const fileSuite = getSuiteType(newTest, 'file')
1491
- if (!fileSuitesWithNewTestsToProjects.has(fileSuite)) {
1492
- fileSuitesWithNewTestsToProjects.set(fileSuite, getSuiteType(newTest, 'project'))
1493
- }
1494
- }
1495
- }
1672
+ return createRootSuiteReturnValue
1673
+ }
1496
1674
 
1497
- applyRetriesToTests(
1498
- fileSuitesWithNewTestsToProjects,
1499
- isNewTest,
1500
- ['_ddIsNew', '_ddIsEfdRetry'],
1501
- getConfiguredEfdRetryCount(),
1502
- (copiedTest, originalTest, retryIndex) => {
1503
- markEfdRetryTest(copiedTest, retryIndex, originalTest)
1504
- markEfdManagedTest(copiedTest)
1505
- },
1506
- getEfdRetryRepeatEachIndex
1507
- )
1508
- }
1509
- }
1675
+ addHook({
1676
+ name: 'playwright',
1677
+ file: 'lib/runner/loadUtils.js',
1678
+ versions: ['>=1.38.0 <1.60.0'],
1679
+ }, (loadUtilsPackage) => {
1680
+ const oldCreateRootSuite = loadUtilsPackage.createRootSuite
1510
1681
 
1511
- return createRootSuiteReturnValue
1682
+ async function newCreateRootSuite () {
1683
+ return processRootSuite(await oldCreateRootSuite.apply(this, arguments))
1512
1684
  }
1513
1685
 
1514
1686
  // We need to proxy the createRootSuite function because the function is not configurable
@@ -1522,32 +1694,47 @@ addHook({
1522
1694
  })
1523
1695
  })
1524
1696
 
1697
+ function prepareProcessHostStartRunner (processHost) {
1698
+ processHost._extraEnv = {
1699
+ ...processHost._extraEnv,
1700
+ // Used to detect that we're in a playwright worker
1701
+ DD_PLAYWRIGHT_WORKER: '1',
1702
+ }
1703
+ }
1704
+
1705
+ function finishProcessHostStartRunner (processHost) {
1706
+ if (!processHost.process) {
1707
+ return
1708
+ }
1709
+
1710
+ // We add a new listener to `processHost.process`, which represents the worker
1711
+ processHost.process.on('message', (message) => {
1712
+ if (message?.type === EFD_RETRY_COUNT_REQUEST) {
1713
+ sendEfdRetryCountToWorkerWhenAvailable(processHost.process, message.testId)
1714
+ return
1715
+ }
1716
+ if (message?.type === DD_PROPERTIES_REQUEST) {
1717
+ sendDdPropertiesToWorkerWhenAvailable(processHost.process, message.testId)
1718
+ return
1719
+ }
1720
+ // These messages are [code, payload]. The payload is test data
1721
+ if (Array.isArray(message) && message[0] === PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE) {
1722
+ workerReportCh.publish(message[1])
1723
+ }
1724
+ })
1725
+ }
1726
+
1525
1727
  // main process hook
1526
1728
  addHook({
1527
1729
  name: 'playwright',
1528
1730
  file: 'lib/runner/processHost.js',
1529
- versions: ['>=1.38.0'],
1731
+ versions: ['>=1.38.0 <1.60.0'],
1530
1732
  }, (processHostPackage) => {
1531
1733
  shimmer.wrap(processHostPackage.ProcessHost.prototype, 'startRunner', startRunner => async function () {
1532
- this._extraEnv = {
1533
- ...this._extraEnv,
1534
- // Used to detect that we're in a playwright worker
1535
- DD_PLAYWRIGHT_WORKER: '1',
1536
- }
1734
+ prepareProcessHostStartRunner(this)
1537
1735
 
1538
1736
  const res = await startRunner.apply(this, arguments)
1539
-
1540
- // We add a new listener to `this.process`, which is represents the worker
1541
- this.process.on('message', (message) => {
1542
- if (message?.type === EFD_RETRY_COUNT_REQUEST) {
1543
- sendEfdRetryCountToWorkerWhenAvailable(this.process, message.testId)
1544
- return
1545
- }
1546
- // These messages are [code, payload]. The payload is test data
1547
- if (Array.isArray(message) && message[0] === PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE) {
1548
- workerReportCh.publish(message[1])
1549
- }
1550
- })
1737
+ finishProcessHostStartRunner(this)
1551
1738
 
1552
1739
  return res
1553
1740
  })
@@ -1555,34 +1742,36 @@ addHook({
1555
1742
  return processHostPackage
1556
1743
  })
1557
1744
 
1745
+ async function handlePageGoto (page) {
1746
+ try {
1747
+ if (page && typeof page.evaluate === 'function') {
1748
+ const { isRumInstrumented, isRumActive, rumSamplingRate } = await page.evaluate(detectRum)
1749
+ if (isRumInstrumented && rumSamplingRate < 100 && !isRumActive) {
1750
+ log.debug("RUM was detected on the page, but it isn't active because the sampling rate is below 100%")
1751
+ }
1752
+
1753
+ if (isRumActive) {
1754
+ testPageGotoCh.publish({
1755
+ isRumActive,
1756
+ page,
1757
+ })
1758
+ }
1759
+ }
1760
+ } catch (e) {
1761
+ // ignore errors such as redirects, context destroyed, etc
1762
+ log.error('goto hook error', e)
1763
+ }
1764
+ }
1765
+
1558
1766
  addHook({
1559
1767
  name: 'playwright-core',
1560
1768
  file: 'lib/client/page.js',
1561
- versions: ['>=1.38.0'],
1769
+ versions: ['>=1.38.0 <1.60.0'],
1562
1770
  }, (pagePackage) => {
1563
1771
  shimmer.wrap(pagePackage.Page.prototype, 'goto', goto => async function (url, options) {
1564
1772
  const response = await goto.apply(this, arguments)
1565
1773
 
1566
- const page = this
1567
-
1568
- try {
1569
- if (page) {
1570
- const { isRumInstrumented, isRumActive, rumSamplingRate } = await page.evaluate(detectRum)
1571
- if (isRumInstrumented && rumSamplingRate < 100 && !isRumActive) {
1572
- log.debug("RUM was detected on the page, but it isn't active because the sampling rate is below 100%")
1573
- }
1574
-
1575
- if (isRumActive) {
1576
- testPageGotoCh.publish({
1577
- isRumActive,
1578
- page,
1579
- })
1580
- }
1581
- }
1582
- } catch (e) {
1583
- // ignore errors such as redirects, context destroyed, etc
1584
- log.error('goto hook error', e)
1585
- }
1774
+ await handlePageGoto(this)
1586
1775
 
1587
1776
  return response
1588
1777
  })
@@ -1590,17 +1779,19 @@ addHook({
1590
1779
  return pagePackage
1591
1780
  })
1592
1781
 
1593
- // Only in worker
1594
- addHook({
1595
- name: 'playwright',
1596
- file: 'lib/worker/workerMain.js',
1597
- versions: ['>=1.38.0'],
1598
- }, (workerPackage) => {
1782
+ function instrumentWorkerMainMethods (workerMain) {
1783
+ if (!workerMain || workerMain[kDdPlaywrightWorkerInstrumented] ||
1784
+ typeof workerMain._runTest !== 'function' || typeof workerMain.dispatchEvent !== 'function') {
1785
+ return workerMain
1786
+ }
1787
+
1788
+ Object.defineProperty(workerMain, kDdPlaywrightWorkerInstrumented, { value: true })
1789
+
1599
1790
  // we assume there's only a test running at a time
1600
1791
  let steps = []
1601
1792
  const stepInfoByStepId = {}
1602
1793
 
1603
- shimmer.wrap(workerPackage.WorkerMain.prototype, '_runTest', _runTest => async function (test) {
1794
+ shimmer.wrap(workerMain, '_runTest', _runTest => async function (test) {
1604
1795
  await waitForEfdRetryCount(test)
1605
1796
  if (shouldSkipEfdRetry(test)) {
1606
1797
  test._ddShouldSkipEfdRetry = true
@@ -1636,6 +1827,27 @@ addHook({
1636
1827
  browserName,
1637
1828
  }
1638
1829
  testToCtx.set(test, testCtx)
1830
+
1831
+ // Wait for ddProperties to be received and processed. The main process sends
1832
+ // this during Playwright's testEnd event, which can happen before _runTest
1833
+ // resolves in 1.60 when retry clones run across multiple workers.
1834
+ let hasDdProperties = false
1835
+ const ddPropertiesDeferred = {}
1836
+ const ddPropertiesPromise = new Promise(resolve => {
1837
+ ddPropertiesDeferred.resolve = resolve
1838
+ })
1839
+ const ddPropertiesMessageHandler = ({ type, testId, properties }) => {
1840
+ if (type === DD_PROPERTIES_RESPONSE && testId === test.id) {
1841
+ hasDdProperties = true
1842
+ if (properties) {
1843
+ Object.assign(test, properties)
1844
+ }
1845
+ process.removeListener('message', ddPropertiesMessageHandler)
1846
+ ddPropertiesDeferred.resolve()
1847
+ }
1848
+ }
1849
+ process.on('message', ddPropertiesMessageHandler)
1850
+
1639
1851
  // TODO - In the future we may need to implement a mechanism to send test properties
1640
1852
  // to the worker process before _runTest is called
1641
1853
  testStartCh.runStores(testCtx, () => {
@@ -1708,6 +1920,16 @@ addHook({
1708
1920
  }
1709
1921
  }
1710
1922
 
1923
+ if (!hasDdProperties && process.send) {
1924
+ process.send({
1925
+ type: DD_PROPERTIES_REQUEST,
1926
+ testId: test.id,
1927
+ })
1928
+ } else if (!hasDdProperties) {
1929
+ process.removeListener('message', ddPropertiesMessageHandler)
1930
+ ddPropertiesDeferred.resolve()
1931
+ }
1932
+
1711
1933
  // testInfo.errors could be better than "error",
1712
1934
  // which will only include timeout error (even though the test failed because of a different error)
1713
1935
 
@@ -1722,26 +1944,17 @@ addHook({
1722
1944
  onDone = resolve
1723
1945
  })
1724
1946
 
1725
- // Wait for ddProperties to be received and processed
1726
- // Create a promise that will be resolved when the properties are received
1727
- const ddPropertiesPromise = new Promise(resolve => {
1728
- const messageHandler = ({ type, testId, properties }) => {
1729
- if (type === 'ddProperties' && testId === test.id) {
1730
- // Apply the properties to the test object
1731
- if (properties) {
1732
- Object.assign(test, properties)
1733
- }
1734
- process.removeListener('message', messageHandler)
1735
- resolve()
1736
- }
1737
- }
1738
-
1739
- // Add the listener
1740
- process.on('message', messageHandler)
1947
+ // Wait for the properties to be received, but do not block the worker forever if IPC fails.
1948
+ const ddPropertiesTimeoutPromise = new Promise(resolve => {
1949
+ const ddPropertiesTimeout = realSetTimeout(() => {
1950
+ process.removeListener('message', ddPropertiesMessageHandler)
1951
+ resolve()
1952
+ }, DD_PROPERTIES_TIMEOUT)
1953
+ ddPropertiesPromise.then(() => {
1954
+ realClearTimeout(ddPropertiesTimeout)
1955
+ })
1741
1956
  })
1742
-
1743
- // Wait for the properties to be received
1744
- await ddPropertiesPromise
1957
+ await Promise.race([ddPropertiesPromise, ddPropertiesTimeoutPromise])
1745
1958
 
1746
1959
  const finalStatus = getFinalStatus({
1747
1960
  isFinalExecution: test._ddIsFinalExecution,
@@ -1787,7 +2000,7 @@ addHook({
1787
2000
 
1788
2001
  // We reproduce what happens in `Dispatcher#_onStepBegin` and `Dispatcher#_onStepEnd`,
1789
2002
  // since `startTime` and `duration` are not available directly in the worker process
1790
- shimmer.wrap(workerPackage.WorkerMain.prototype, 'dispatchEvent', dispatchEvent => function (event, payload) {
2003
+ shimmer.wrap(workerMain, 'dispatchEvent', dispatchEvent => function (event, payload) {
1791
2004
  if (event === 'stepBegin') {
1792
2005
  stepInfoByStepId[payload.stepId] = {
1793
2006
  startTime: payload.wallTime,
@@ -1808,6 +2021,16 @@ addHook({
1808
2021
  return dispatchEvent.apply(this, arguments)
1809
2022
  })
1810
2023
 
2024
+ return workerMain
2025
+ }
2026
+
2027
+ // Only in worker
2028
+ addHook({
2029
+ name: 'playwright',
2030
+ file: 'lib/worker/workerMain.js',
2031
+ versions: ['>=1.38.0 <1.60.0'],
2032
+ }, (workerPackage) => {
2033
+ instrumentWorkerMainMethods(workerPackage.WorkerMain.prototype)
1811
2034
  return workerPackage
1812
2035
  })
1813
2036
 
@@ -1856,7 +2079,7 @@ function generateSummaryWrapper (generateSummary) {
1856
2079
  addHook({
1857
2080
  name: 'playwright',
1858
2081
  file: 'lib/reporters/base.js',
1859
- versions: ['>=1.38.0'],
2082
+ versions: ['>=1.38.0 <1.60.0'],
1860
2083
  }, (reportersPackage) => {
1861
2084
  // v1.50.0 changed the name of the base reporter from BaseReporter to TerminalReporter
1862
2085
  if (reportersPackage.TerminalReporter) {