dd-trace 6.11.0 → 6.13.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 (139) hide show
  1. package/LICENSE-3rdparty.csv +5 -0
  2. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  3. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  4. package/index.d.ts +143 -25
  5. package/package.json +26 -25
  6. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  7. package/packages/datadog-instrumentations/src/cucumber.js +4 -4
  8. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  9. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  12. package/packages/datadog-instrumentations/src/jest.js +140 -18
  13. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  14. package/packages/datadog-instrumentations/src/mariadb.js +7 -0
  15. package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
  16. package/packages/datadog-instrumentations/src/pino.js +15 -4
  17. package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
  18. package/packages/datadog-instrumentations/src/playwright.js +67 -53
  19. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +13 -12
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
  21. package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
  22. package/packages/datadog-instrumentations/src/winston.js +6 -3
  23. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  24. package/packages/datadog-plugin-http/src/client.js +2 -15
  25. package/packages/datadog-plugin-http2/src/client.js +2 -15
  26. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  27. package/packages/datadog-plugin-playwright/src/index.js +13 -1
  28. package/packages/datadog-plugin-undici/src/index.js +2 -15
  29. package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
  30. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  31. package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
  32. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  33. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  34. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  35. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  36. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  37. package/packages/dd-trace/src/appsec/reporter.js +2 -0
  38. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  39. package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
  40. package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
  41. package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
  42. package/packages/dd-trace/src/carrier.js +107 -48
  43. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
  44. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
  46. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
  47. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
  48. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
  49. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
  50. package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
  51. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
  52. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
  53. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  54. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
  55. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
  56. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
  57. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
  58. package/packages/dd-trace/src/config/index.js +2 -1
  59. package/packages/dd-trace/src/config/supported-configurations.json +27 -0
  60. package/packages/dd-trace/src/debugger/constants.js +1 -0
  61. package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
  62. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
  63. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
  64. package/packages/dd-trace/src/debugger/index.js +4 -2
  65. package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
  66. package/packages/dd-trace/src/dogstatsd.js +45 -18
  67. package/packages/dd-trace/src/encode/agentless-json.js +5 -4
  68. package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
  69. package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
  70. package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
  71. package/packages/dd-trace/src/evp_proxy/path.js +22 -0
  72. package/packages/dd-trace/src/exporters/agent/index.js +22 -2
  73. package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
  74. package/packages/dd-trace/src/exporters/common/request.js +5 -2
  75. package/packages/dd-trace/src/exporters/common/writer.js +24 -2
  76. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  77. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  78. package/packages/dd-trace/src/flush.js +102 -0
  79. package/packages/dd-trace/src/llmobs/experiments/client.js +10 -2
  80. package/packages/dd-trace/src/llmobs/experiments/dataset.js +301 -14
  81. package/packages/dd-trace/src/llmobs/experiments/experiment.js +440 -112
  82. package/packages/dd-trace/src/llmobs/experiments/index.js +68 -44
  83. package/packages/dd-trace/src/llmobs/experiments/noop.js +103 -12
  84. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  85. package/packages/dd-trace/src/llmobs/experiments/util.js +81 -4
  86. package/packages/dd-trace/src/llmobs/index.js +80 -17
  87. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  88. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  89. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  90. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  91. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  92. package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
  93. package/packages/dd-trace/src/noop/span.js +5 -0
  94. package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
  95. package/packages/dd-trace/src/openfeature/index.js +7 -4
  96. package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
  97. package/packages/dd-trace/src/openfeature/writers/exposures.js +70 -15
  98. package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
  99. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  100. package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
  101. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  102. package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
  103. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  104. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
  105. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
  106. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  107. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  108. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  109. package/packages/dd-trace/src/opentracing/propagation/text_map.js +429 -352
  110. package/packages/dd-trace/src/opentracing/span.js +28 -0
  111. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  112. package/packages/dd-trace/src/plugin_manager.js +2 -0
  113. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  114. package/packages/dd-trace/src/plugins/util/test.js +74 -21
  115. package/packages/dd-trace/src/plugins/util/web.js +2 -65
  116. package/packages/dd-trace/src/profiling/oom.js +4 -3
  117. package/packages/dd-trace/src/profiling/profiler.js +7 -0
  118. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  119. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  120. package/packages/dd-trace/src/proxy.js +36 -8
  121. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  122. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
  123. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
  124. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
  125. package/packages/dd-trace/src/serverless/vercel.js +122 -0
  126. package/packages/dd-trace/src/serverless.js +37 -20
  127. package/packages/dd-trace/src/span_stats.js +40 -3
  128. package/packages/dd-trace/src/telemetry/metrics.js +62 -8
  129. package/packages/dd-trace/src/telemetry/send-data.js +1 -0
  130. package/packages/dd-trace/src/tracer.js +22 -0
  131. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  132. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  133. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  134. package/vendor/dist/https-proxy-agent/LICENSE +22 -0
  135. package/vendor/dist/https-proxy-agent/index.js +4 -0
  136. package/vendor/dist/protobufjs/index.js +1 -1
  137. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  138. package/vendor/dist/proxy-from-env/LICENSE +20 -0
  139. package/vendor/dist/proxy-from-env/index.js +1 -0
@@ -14,6 +14,7 @@ const {
14
14
  takePoolWaitTime,
15
15
  wrapPoolQueryMethod,
16
16
  } = require('./helpers/pool-acquire')
17
+ const { wrapCallbackBundle, wrapPromiseBundle } = require('./mariadb-bundle')
17
18
 
18
19
  const commandAddCh = channel('apm:mariadb:command:add')
19
20
  const connectionStartCh = channel('apm:mariadb:connection:start')
@@ -443,3 +444,9 @@ addHook({ name, file: 'lib/connection.js', versions: ['>=2.0.4 <=2.5.1'] }, (Con
443
444
  addHook({ name, file: 'lib/pool-base.js', versions: ['>=2.0.4 <3'] }, (PoolBase) => {
444
445
  return shimmer.wrapFunction(PoolBase, wrapPoolBase)
445
446
  })
447
+
448
+ // MariaDB 3.5.3 added single-file CommonJS bundles that do not load the original source modules at runtime.
449
+ // Matching their generated, minified internals would couple instrumentation to unstable bundle output, so wrap
450
+ // the runtime objects returned by the public factories instead.
451
+ addHook({ name, versions: ['>=3.5.3'] }, wrapPromiseBundle)
452
+ addHook({ name, file: 'dist/callback.cjs', versions: ['>=3.5.3'] }, wrapCallbackBundle)
@@ -299,6 +299,9 @@ function retryTest (test, numRetries, tags, retryPolicy) {
299
299
  clonedTest[tag] = true
300
300
  }
301
301
  }
302
+ if (clonedTest._ddIsQuarantined && !clonedTest._ddIsAttemptToFix) {
303
+ testsQuarantined.add(clonedTest)
304
+ }
302
305
  }
303
306
  }
304
307
 
@@ -1146,7 +1149,7 @@ function getRunTestsWrapper (runTests, config) {
1146
1149
  retryTest(
1147
1150
  test,
1148
1151
  config.earlyFlakeDetectionRetryPolicy.schedulingRetryCount,
1149
- ['_ddIsModified', '_ddIsEfdRetry'],
1152
+ ['_ddIsModified', '_ddIsEfdRetry', test._ddIsQuarantined && '_ddIsQuarantined'],
1150
1153
  config.earlyFlakeDetectionRetryPolicy
1151
1154
  )
1152
1155
  }
@@ -1165,7 +1168,7 @@ function getRunTestsWrapper (runTests, config) {
1165
1168
  retryTest(
1166
1169
  test,
1167
1170
  config.earlyFlakeDetectionRetryPolicy.schedulingRetryCount,
1168
- ['_ddIsNew', '_ddIsEfdRetry'],
1171
+ ['_ddIsNew', '_ddIsEfdRetry', test._ddIsQuarantined && '_ddIsQuarantined'],
1169
1172
  config.earlyFlakeDetectionRetryPolicy
1170
1173
  )
1171
1174
  }
@@ -6,6 +6,8 @@ const {
6
6
  addHook,
7
7
  } = require('./helpers/instrument')
8
8
 
9
+ const logSubmissionCh = channel('ci:log-submission:log')
10
+
9
11
  /**
10
12
  * @param {string} symbol
11
13
  * @param {(original: Function) => Function} wrapper
@@ -36,13 +38,22 @@ function wrapAsJson (asJson) {
36
38
  obj = arguments[0] = obj || {}
37
39
 
38
40
  // Caller-provided `dd` wins -- skip the splice so a bespoke `dd` survives.
41
+ let line
39
42
  if (!jsonCh.hasSubscribers || Object.hasOwn(obj, 'dd')) {
40
- return asJson.apply(this, arguments)
43
+ line = asJson.apply(this, arguments)
44
+ } else {
45
+ const payload = { line: asJson.apply(this, arguments) }
46
+ jsonCh.publish(payload)
47
+ line = payload.line
48
+ }
49
+
50
+ // Submit the serialized line for agentless log collection only when trace
51
+ // correlation is active, matching the Bunyan contract.
52
+ if (jsonCh.hasSubscribers && logSubmissionCh.hasSubscribers) {
53
+ logSubmissionCh.publish({ source: 'pino', message: line })
41
54
  }
42
55
 
43
- const payload = { line: asJson.apply(this, arguments) }
44
- jsonCh.publish(payload)
45
- return payload.line
56
+ return line
46
57
  }
47
58
  }
48
59
 
@@ -3,6 +3,9 @@
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
5
  const reporterErrorCh = channel('ci:playwright:reporter:error')
6
+ const reporterRunSummaryCh = channel('ci:playwright:reporter:run-summary')
7
+ const reporterSuiteHookErrorCh = channel('ci:playwright:reporter:suite-hook-error')
8
+ const suitesWithHookErrors = new Set()
6
9
  const PLAYWRIGHT_REPORTER_ERROR_MESSAGE = 'Error in reporter'
7
10
  const PLAYWRIGHT_REPORTER_ERROR_CALLER_RE =
8
11
  /^\s*at wrapAsync .*?[\\/]playwright[\\/]lib[\\/]runner[\\/]index\.js:\d+:\d+\)?$/
@@ -27,7 +30,53 @@ function isPlaywrightReporterError (message) {
27
30
  }
28
31
  }
29
32
 
33
+ /**
34
+ * Returns whether a finalized Playwright result contains a failed suite hook.
35
+ *
36
+ * @param {Array<object>} steps
37
+ * @returns {boolean}
38
+ */
39
+ function hasFailedSuiteHook (steps) {
40
+ if (!steps) return false
41
+
42
+ for (const step of steps) {
43
+ if (step.error && (step.title === 'beforeAll hook' || step.title === 'afterAll hook')) return true
44
+ if (hasFailedSuiteHook(step.steps)) return true
45
+ }
46
+ return false
47
+ }
48
+
30
49
  class DatadogPlaywrightReporter {
50
+ /**
51
+ * Creates a reporter that observes the finalized Playwright result independently of user reporters.
52
+ *
53
+ * @param {object} [options]
54
+ * @param {boolean} [options.captureReporterErrors]
55
+ */
56
+ constructor ({ captureReporterErrors = true } = {}) {
57
+ this.captureReporterErrors = captureReporterErrors
58
+ this.fatalErrorCount = 0
59
+ }
60
+
61
+ /**
62
+ * Identifies this reporter as implementing Playwright's reporter v2 contract.
63
+ *
64
+ * @returns {'v2'}
65
+ */
66
+ version () {
67
+ return 'v2'
68
+ }
69
+
70
+ /**
71
+ * Implements the reporter v2 lifecycle hook required by older Playwright versions.
72
+ *
73
+ * @param {object} config
74
+ * @returns {void}
75
+ */
76
+ onConfigure (config) {
77
+ this.failOnFlakyTests = config.failOnFlakyTests
78
+ }
79
+
31
80
  /**
32
81
  * Restores console error after Playwright completes the reporter lifecycle.
33
82
  *
@@ -45,12 +94,96 @@ class DatadogPlaywrightReporter {
45
94
  DatadogPlaywrightReporter.originalConsoleError = undefined
46
95
  }
47
96
 
97
+ /**
98
+ * Records the suite so finalized test outcomes can be counted in onEnd.
99
+ *
100
+ * @param {object} configOrSuite
101
+ * @param {object} [suite]
102
+ * @returns {void}
103
+ */
104
+ onBegin (configOrSuite, suite) {
105
+ this.suite = suite || configOrSuite
106
+ if (suite) this.failOnFlakyTests = configOrSuite.failOnFlakyTests
107
+ suitesWithHookErrors.clear()
108
+ }
109
+
110
+ /**
111
+ * Implements the reporter v2 lifecycle hook required by older Playwright versions.
112
+ *
113
+ * @returns {void}
114
+ */
115
+ onTestBegin () {}
116
+
117
+ /**
118
+ * Implements the reporter v2 lifecycle hook required by older Playwright versions.
119
+ *
120
+ * @returns {void}
121
+ */
122
+ onStdOut () {}
123
+
124
+ /**
125
+ * Implements the reporter v2 lifecycle hook required by older Playwright versions.
126
+ *
127
+ * @returns {void}
128
+ */
129
+ onStdErr () {}
130
+
131
+ /**
132
+ * Implements the reporter v2 lifecycle hook required by older Playwright versions.
133
+ *
134
+ * @returns {void}
135
+ */
136
+ onTestEnd () {}
137
+
138
+ /**
139
+ * Implements the reporter v2 lifecycle hook required by older Playwright versions.
140
+ *
141
+ * @returns {void}
142
+ */
143
+ onStepBegin () {}
144
+
145
+ /**
146
+ * Implements the reporter v2 lifecycle hook required by older Playwright versions.
147
+ *
148
+ * @returns {void}
149
+ */
150
+ onStepEnd () {}
151
+
48
152
  /**
49
153
  * Marks the beginning of reporter finalization so later reporter errors can be identified.
50
154
  *
51
155
  * @returns {void}
52
156
  */
53
157
  onEnd () {
158
+ let failureCount = this.fatalErrorCount
159
+ let quarantinedFailureCount = 0
160
+ let hasIncompleteTests = suitesWithHookErrors.size > 0
161
+ const tests = this.suite?.allTests?.()
162
+ if (tests) {
163
+ for (const test of tests) {
164
+ const outcome = test.outcome()
165
+ const finalResult = test.results.at(-1)
166
+ hasIncompleteTests ||= hasFailedSuiteHook(finalResult?.steps)
167
+ if (outcome === 'unexpected' || (outcome === 'flaky' && this.failOnFlakyTests)) {
168
+ failureCount += 1
169
+ const finalResultStatus = finalResult?.status
170
+ const hasFailed = finalResultStatus === 'failed' || finalResultStatus === 'timedOut'
171
+ if (outcome === 'unexpected' && hasFailed && test._ddIsQuarantined && !test._ddIsAttemptToFix) {
172
+ quarantinedFailureCount += 1
173
+ }
174
+ } else if (outcome === 'skipped' && !test._ddShouldSkipEfdRetry &&
175
+ (!test._ddIsDisabled || test._ddIsAttemptToFix)) {
176
+ const { results } = test
177
+ hasIncompleteTests ||= results.some(result => result.status === 'interrupted') ||
178
+ !results.length || test.expectedStatus !== 'skipped'
179
+ }
180
+ }
181
+ }
182
+ reporterRunSummaryCh.publish({ failureCount, quarantinedFailureCount, hasIncompleteTests })
183
+ suitesWithHookErrors.clear()
184
+
185
+ if (!this.captureReporterErrors) return
186
+
54
187
  this.isFinalizing = true
55
188
  // Playwright 1.60 and 1.61 only expose reporter errors through this exact console call.
56
189
  // eslint-disable-next-line no-console
@@ -76,6 +209,13 @@ class DatadogPlaywrightReporter {
76
209
  DatadogPlaywrightReporter.consoleError = consoleError
77
210
  }
78
211
 
212
+ /**
213
+ * Implements the reporter v2 lifecycle hook required by older Playwright versions.
214
+ *
215
+ * @returns {void}
216
+ */
217
+ onExit () {}
218
+
79
219
  /**
80
220
  * Reports errors emitted by Playwright while later reporters are finalizing.
81
221
  *
@@ -83,7 +223,11 @@ class DatadogPlaywrightReporter {
83
223
  * @returns {void}
84
224
  */
85
225
  onError (error) {
86
- if (this.isFinalizing) reporterErrorCh.publish(error)
226
+ if (this.isFinalizing) {
227
+ reporterErrorCh.publish(error)
228
+ } else {
229
+ this.fatalErrorCount += 1
230
+ }
87
231
  }
88
232
 
89
233
  /**
@@ -96,4 +240,8 @@ class DatadogPlaywrightReporter {
96
240
  }
97
241
  }
98
242
 
243
+ reporterSuiteHookErrorCh.subscribe((testSuiteAbsolutePath) => {
244
+ suitesWithHookErrors.add(testSuiteAbsolutePath)
245
+ })
246
+
99
247
  module.exports = DatadogPlaywrightReporter
@@ -46,6 +46,8 @@ const testSessionStartCh = channel('ci:playwright:session:start')
46
46
  const testSessionConfigurationCh = channel('ci:playwright:session:configuration')
47
47
  const testSessionFinishCh = channel('ci:playwright:session:finish')
48
48
  const reporterErrorCh = channel('ci:playwright:reporter:error')
49
+ const reporterRunSummaryCh = channel('ci:playwright:reporter:run-summary')
50
+ const reporterSuiteHookErrorCh = channel('ci:playwright:reporter:suite-hook-error')
49
51
 
50
52
  const libraryConfigurationCh = channel('ci:playwright:library-configuration')
51
53
  const knownTestsCh = channel('ci:playwright:known-tests')
@@ -57,6 +59,7 @@ const testSuiteStartCh = channel('ci:playwright:test-suite:start')
57
59
  const testSuiteFinishCh = channel('ci:playwright:test-suite:finish')
58
60
 
59
61
  const workerReportCh = channel('ci:playwright:worker:report')
62
+ const logSubmissionFlushCh = channel('ci:log-submission:flush')
60
63
  const workerReportTelemetryCh = channel('ci:playwright:worker-report:telemetry')
61
64
  const testPageGotoCh = channel('ci:playwright:test:page-goto')
62
65
 
@@ -111,7 +114,7 @@ let testManagementAttemptToFixRetries = 0
111
114
  let testManagementTests = {}
112
115
  let isImpactedTestsEnabled = false
113
116
  let modifiedFiles = {}
114
- let quarantinedButNotAttemptToFixFqns = new Set()
117
+ let playwrightRunSummary
115
118
  let recordedTestOptimizationExecutions = new Set()
116
119
  let testsReportedInGenerateSummary = new Set()
117
120
  const newTestsWithDynamicNames = new Set()
@@ -785,6 +788,7 @@ function finishTestSuiteIfDone (testSuiteAbsolutePath, projects) {
785
788
  testSourceLine: test.location.line,
786
789
  browserName,
787
790
  isNew: test._ddIsNew,
791
+ isAttemptToFix: test._ddIsAttemptToFix,
788
792
  isDisabled: test._ddIsDisabled,
789
793
  isModified: test._ddIsModified,
790
794
  isQuarantined: test._ddIsQuarantined,
@@ -832,6 +836,7 @@ function testEndHandler ({
832
836
 
833
837
  if (hookError) {
834
838
  addErrorToTestSuite(testSuiteAbsolutePath, hookError)
839
+ reporterSuiteHookErrorCh.publish(testSuiteAbsolutePath)
835
840
  }
836
841
  return
837
842
  }
@@ -917,9 +922,7 @@ function testEndHandler ({
917
922
 
918
923
  // Check if all EFD retries failed
919
924
  const efdRetryCount = getEfdRetryCountForTest(test)
920
- if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
921
- (test._ddIsNew || test._ddIsModified) &&
922
- isEarlyFlakeDetectionEnabled &&
925
+ if (isEfdManagedTest && efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
923
926
  testStatuses.every(status => status === 'fail')) {
924
927
  test._ddHasFailedAllRetries = true
925
928
  }
@@ -1083,7 +1086,7 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1083
1086
 
1084
1087
  const projects = getProjectsFromDispatcher(dispatcher)
1085
1088
  sessionProjects = projects
1086
- const automaticFailureScreenshotPathsByTestId = new Map()
1089
+ const automaticFailureScreenshotsByTestId = new Map()
1087
1090
 
1088
1091
  if (disabledTestIds.size && !worker[kDdPlaywrightWorkerHostInstrumented] &&
1089
1092
  typeof worker.runTestGroup === 'function') {
@@ -1108,19 +1111,21 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1108
1111
  const test = getTestByTestId(dispatcher, testId)
1109
1112
  if (!test) return
1110
1113
 
1114
+ automaticFailureScreenshotsByTestId.clear()
1111
1115
  const browser = getBrowserNameFromProjects(projects, test)
1112
1116
  const shouldCreateTestSpan = test.expectedStatus === 'skipped'
1113
1117
  testBeginHandler(test, browser, shouldCreateTestSpan)
1114
1118
  })
1115
- worker.on('attach', ({ testId, path, _ddIsAutomaticFailureScreenshot }) => {
1119
+ worker.on('attach', (attachment) => {
1120
+ const { testId, _ddIsAutomaticFailureScreenshot } = attachment
1116
1121
  if (!_ddIsAutomaticFailureScreenshot) return
1117
1122
 
1118
- let screenshotPaths = automaticFailureScreenshotPathsByTestId.get(testId)
1119
- if (!screenshotPaths) {
1120
- screenshotPaths = new Set()
1121
- automaticFailureScreenshotPathsByTestId.set(testId, screenshotPaths)
1123
+ let screenshots = automaticFailureScreenshotsByTestId.get(testId)
1124
+ if (!screenshots) {
1125
+ screenshots = []
1126
+ automaticFailureScreenshotsByTestId.set(testId, screenshots)
1122
1127
  }
1123
- screenshotPaths.add(path)
1128
+ screenshots.push(attachment)
1124
1129
  })
1125
1130
  worker.on('testEnd', ({ testId, status, errors, annotations }) => {
1126
1131
  const test = getTestByTestId(dispatcher, testId)
@@ -1144,19 +1149,20 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1144
1149
  }
1145
1150
  )
1146
1151
  const testResult = test.results.at(-1)
1147
- const automaticFailureScreenshotPaths = automaticFailureScreenshotPathsByTestId.get(testId)
1148
- automaticFailureScreenshotPathsByTestId.delete(testId)
1149
- if (testStatus === 'fail' && automaticFailureScreenshotPaths?.size && testResult?.attachments?.length) {
1150
- const screenshots = []
1151
- for (const attachment of testResult.attachments) {
1152
- if (automaticFailureScreenshotPaths.has(attachment.path)) {
1153
- screenshots.push(attachment)
1152
+ if (isFailureScreenshotUploadEnabled &&
1153
+ !shouldCreateTestSpan &&
1154
+ !test._ddShouldSkipEfdRetry &&
1155
+ !disabledTestIds.has(testId)) {
1156
+ let screenshots
1157
+ if (testStatus === 'fail') {
1158
+ screenshots = automaticFailureScreenshotsByTestId.get(testId)
1159
+ if (!screenshots) {
1160
+ screenshots = []
1161
+ automaticFailureScreenshotsByTestId.set(testId, screenshots)
1154
1162
  }
1155
1163
  }
1156
- if (screenshots.length) {
1157
- worker[kDdPlaywrightFailureScreenshots] ??= []
1158
- worker[kDdPlaywrightFailureScreenshots].push(screenshots)
1159
- }
1164
+ worker[kDdPlaywrightFailureScreenshots] ??= []
1165
+ worker[kDdPlaywrightFailureScreenshots].push(screenshots)
1160
1166
  }
1161
1167
  const isAtrRetry = testResult?.retry > 0 &&
1162
1168
  isFlakyTestRetriesEnabled &&
@@ -1259,6 +1265,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1259
1265
  return async function (config) {
1260
1266
  reporterError = undefined
1261
1267
  hasReporterError = false
1268
+ playwrightRunSummary = undefined
1262
1269
  let restoreReporterConsoleError
1263
1270
  if (satisfies(playwrightVersion, '>=1.60.0') && config?.config) {
1264
1271
  const DatadogPlaywrightReporter = require('./playwright-reporter')
@@ -1433,23 +1440,8 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1433
1440
  const sessionStatus = runAllTestsReturn.status || runAllTestsReturn
1434
1441
 
1435
1442
  if (isTestManagementTestsEnabled && sessionStatus === 'failed') {
1436
- let totalFailedTestCount = 0
1437
- let totalPureQuarantinedFailedTestCount = 0
1438
-
1439
- for (const [fqn, testStatuses] of testsToTestStatuses) {
1440
- // Only count as failed if the final status (after retries) is 'fail'
1441
- const lastStatus = testStatuses.at(-1)
1442
- if (lastStatus === 'fail') {
1443
- totalFailedTestCount += 1
1444
- if (quarantinedButNotAttemptToFixFqns.has(fqn)) {
1445
- totalPureQuarantinedFailedTestCount += 1
1446
- }
1447
- }
1448
- }
1449
-
1450
- const totalIgnorableFailures = totalPureQuarantinedFailedTestCount
1451
-
1452
- if (totalFailedTestCount > 0 && totalFailedTestCount === totalIgnorableFailures) {
1443
+ const { failureCount, quarantinedFailureCount, hasIncompleteTests } = playwrightRunSummary || {}
1444
+ if (!hasIncompleteTests && quarantinedFailureCount > 0 && quarantinedFailureCount === failureCount) {
1453
1445
  runAllTestsReturn = 'passed'
1454
1446
  preventedToFail = true
1455
1447
  }
@@ -1476,7 +1468,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1476
1468
 
1477
1469
  startedSuites = []
1478
1470
  remainingTestsByFile = {}
1479
- quarantinedButNotAttemptToFixFqns = new Set()
1471
+ playwrightRunSummary = undefined
1480
1472
  recordedTestOptimizationExecutions = new Set()
1481
1473
  testsReportedInGenerateSummary = new Set()
1482
1474
  efdManagedTestKeys.clear()
@@ -1507,6 +1499,8 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1507
1499
  function reportersHook (reportersPackage) {
1508
1500
  return shimmer.wrap(reportersPackage, 'createReporters', createReporters => async function () {
1509
1501
  const reporters = await createReporters.apply(this, arguments)
1502
+ const DatadogPlaywrightReporter = require('./playwright-reporter')
1503
+ reporters.unshift(new DatadogPlaywrightReporter({ captureReporterErrors: false }))
1510
1504
  for (const reporter of reporters) {
1511
1505
  if (instrumentedPlaywrightReporters.has(reporter)) continue
1512
1506
 
@@ -1663,6 +1657,10 @@ reporterErrorCh.subscribe((error) => {
1663
1657
  recordReporterError(error)
1664
1658
  })
1665
1659
 
1660
+ reporterRunSummaryCh.subscribe((runSummary) => {
1661
+ playwrightRunSummary = runSummary
1662
+ })
1663
+
1666
1664
  /**
1667
1665
  * Records a reporter failure even when the reporter throws a falsy value.
1668
1666
  *
@@ -1894,11 +1892,6 @@ function processRootSuite (createRootSuiteReturnValue) {
1894
1892
  }
1895
1893
  if (testProperties.quarantined) {
1896
1894
  test._ddIsQuarantined = true
1897
- if (!testProperties.attemptToFix) {
1898
- // Do not skip quarantined tests, let them run and overwrite results post-run if they fail
1899
- const testFqn = getTestFullyQualifiedName(test)
1900
- quarantinedButNotAttemptToFixFqns.add(testFqn)
1901
- }
1902
1895
  }
1903
1896
  if (testProperties.attemptToFix) {
1904
1897
  test._ddIsAttemptToFix = true
@@ -1938,7 +1931,10 @@ function processRootSuite (createRootSuiteReturnValue) {
1938
1931
  const fileSuitesWithImpactedTestsToProjects = new Map()
1939
1932
  for (const impactedTest of impactedTests) {
1940
1933
  impactedTest._ddIsModified = true
1941
- if (shouldRunEarlyFlakeDetection() && impactedTest.expectedStatus !== 'skipped') {
1934
+ if (shouldRunEarlyFlakeDetection() && impactedTest.expectedStatus !== 'skipped' &&
1935
+ !impactedTest._ddIsAttemptToFix) {
1936
+ // Prevent ATR or `--retries` from retrying tests that EFD manages.
1937
+ impactedTest.retries = 0
1942
1938
  markEfdManagedTest(impactedTest)
1943
1939
  const fileSuite = getSuiteType(impactedTest, 'file')
1944
1940
  if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) {
@@ -1946,14 +1942,15 @@ function processRootSuite (createRootSuiteReturnValue) {
1946
1942
  }
1947
1943
  }
1948
1944
  }
1949
- // If something change in the file, all tests in the file are impacted, hence the () => true filter
1945
+ // If something changes in the file, all tests in the file are impacted, except attempt-to-fix tests managed above.
1950
1946
  applyRetriesToTests(
1951
1947
  fileSuitesWithImpactedTestsToProjects,
1952
- () => true,
1948
+ (test) => !test._ddIsAttemptToFix,
1953
1949
  [
1954
1950
  '_ddIsModified',
1955
1951
  '_ddIsEfdRetry',
1956
1952
  (test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null),
1953
+ (test) => test._ddIsQuarantined && '_ddIsQuarantined',
1957
1954
  ],
1958
1955
  earlyFlakeDetectionRetryPolicy.schedulingRetryCount,
1959
1956
  (copiedTest, originalTest, retryIndex) => {
@@ -1981,7 +1978,8 @@ function processRootSuite (createRootSuiteReturnValue) {
1981
1978
  const fileSuitesWithNewTestsToProjects = new Map()
1982
1979
  for (const newTest of newTests) {
1983
1980
  newTest._ddIsNew = true
1984
- if (shouldRunEarlyFlakeDetection() && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
1981
+ if (shouldRunEarlyFlakeDetection() && newTest.expectedStatus !== 'skipped' &&
1982
+ !newTest._ddIsModified && !newTest._ddIsAttemptToFix) {
1985
1983
  // Prevent ATR or `--retries` from retrying tests that EFD manages.
1986
1984
  newTest.retries = 0
1987
1985
  markEfdManagedTest(newTest)
@@ -1994,8 +1992,12 @@ function processRootSuite (createRootSuiteReturnValue) {
1994
1992
 
1995
1993
  applyRetriesToTests(
1996
1994
  fileSuitesWithNewTestsToProjects,
1997
- isNewTest,
1998
- ['_ddIsNew', '_ddIsEfdRetry'],
1995
+ (test) => !test._ddIsAttemptToFix && isNewTest(test),
1996
+ [
1997
+ '_ddIsNew',
1998
+ '_ddIsEfdRetry',
1999
+ (test) => test._ddIsQuarantined && '_ddIsQuarantined',
2000
+ ],
1999
2001
  earlyFlakeDetectionRetryPolicy.schedulingRetryCount,
2000
2002
  (copiedTest, originalTest, retryIndex) => {
2001
2003
  markEfdRetryTest(copiedTest, retryIndex, originalTest)
@@ -2214,7 +2216,8 @@ addHook({
2214
2216
 
2215
2217
  function instrumentWorkerMainMethods (workerMain) {
2216
2218
  if (!workerMain || workerMain[kDdPlaywrightWorkerInstrumented] ||
2217
- typeof workerMain._runTest !== 'function' || typeof workerMain.dispatchEvent !== 'function') {
2219
+ typeof workerMain._runTest !== 'function' || typeof workerMain.dispatchEvent !== 'function' ||
2220
+ typeof workerMain.gracefullyClose !== 'function') {
2218
2221
  return workerMain
2219
2222
  }
2220
2223
 
@@ -2230,6 +2233,15 @@ function instrumentWorkerMainMethods (workerMain) {
2230
2233
  return runTestGroup.apply(this, arguments)
2231
2234
  })
2232
2235
 
2236
+ // Playwright >=1.60 creates WorkerMain through a runtime factory, which Orchestrion cannot wrap statically.
2237
+ shimmer.wrap(workerMain, 'gracefullyClose', gracefullyClose => async function () {
2238
+ try {
2239
+ return await gracefullyClose.apply(this, arguments)
2240
+ } finally {
2241
+ await getChannelPromise(logSubmissionFlushCh)
2242
+ }
2243
+ })
2244
+
2233
2245
  shimmer.wrap(workerMain, '_runTest', _runTest => async function (test) {
2234
2246
  if (this[kDdPlaywrightDisabledTestIds]?.has(test.id)) {
2235
2247
  test._ddIsDisabled = true
@@ -2395,7 +2407,7 @@ function instrumentWorkerMainMethods (workerMain) {
2395
2407
  // We reproduce what happens in `Dispatcher#_onStepBegin` and `Dispatcher#_onStepEnd`,
2396
2408
  // since `startTime` and `duration` are not available directly in the worker process
2397
2409
  shimmer.wrap(workerMain, 'dispatchEvent', dispatchEvent => function (event, payload) {
2398
- if (event === 'testBegin' || event === 'testEnd') {
2410
+ if (event === 'testBegin') {
2399
2411
  automaticFailureScreenshotPaths.clear()
2400
2412
  } else if (event === 'stepBegin') {
2401
2413
  stepInfoByStepId[payload.stepId] = {
@@ -2447,6 +2459,7 @@ function generateSummaryWrapper (generateSummary) {
2447
2459
  line: testSourceLine,
2448
2460
  },
2449
2461
  _ddIsNew: isNew,
2462
+ _ddIsAttemptToFix: isAttemptToFix,
2450
2463
  _ddIsDisabled: isDisabled,
2451
2464
  _ddIsModified: isModified,
2452
2465
  _ddIsQuarantined: isQuarantined,
@@ -2461,6 +2474,7 @@ function generateSummaryWrapper (generateSummary) {
2461
2474
  testSourceLine,
2462
2475
  browserName,
2463
2476
  isNew,
2477
+ isAttemptToFix,
2464
2478
  isDisabled,
2465
2479
  isModified,
2466
2480
  isQuarantined,
@@ -1058,7 +1058,7 @@ function getRepeatedTestReport (task, testName, testSuiteAbsolutePath, testPrope
1058
1058
  const errors = browserEnvironment
1059
1059
  ? normalizeVitestBrowserErrors(result?.errors)
1060
1060
  : result?.errors || []
1061
- const finalAttemptStatus = finalStatus(statuses)
1061
+ const finalAttemptStatus = finalStatus(statuses, testProperties)
1062
1062
  const hasFailure = finalAttemptStatus === 'fail'
1063
1063
  const attempts = []
1064
1064
  const attemptCount = getRepeatedAttemptCount(task, statuses)
@@ -1095,8 +1095,7 @@ function getRepeatedTestReport (task, testName, testSuiteAbsolutePath, testPrope
1095
1095
  testProperties,
1096
1096
  type,
1097
1097
  statuses,
1098
- isFinalAttempt,
1099
- hasFailure
1098
+ isFinalAttempt
1100
1099
  ),
1101
1100
  isRetry: index > 0,
1102
1101
  status: attemptStatus,
@@ -1136,11 +1135,10 @@ function getRepeatedTestReport (task, testName, testSuiteAbsolutePath, testPrope
1136
1135
  * @param {'attempt_to_fix'|'early_flake_detection'|'external'} type
1137
1136
  * @param {string[]} statuses
1138
1137
  * @param {boolean} isFinalAttempt
1139
- * @param {boolean} hasFailure
1140
1138
  * @returns {boolean}
1141
1139
  */
1142
- function hasFailedAllManagedRetries (task, testProperties, type, statuses, isFinalAttempt, hasFailure) {
1143
- if (!isFinalAttempt || !hasFailure || statuses.length === 0 || !statuses.every(status => status === 'fail')) {
1140
+ function hasFailedAllManagedRetries (task, testProperties, type, statuses, isFinalAttempt) {
1141
+ if (!isFinalAttempt || statuses.length === 0 || !statuses.every(status => status === 'fail')) {
1144
1142
  return false
1145
1143
  }
1146
1144
 
@@ -1165,7 +1163,8 @@ function getAttemptToFixFinalStatus (statuses) {
1165
1163
  return statuses.includes('fail') ? 'fail' : 'pass'
1166
1164
  }
1167
1165
 
1168
- function getEarlyFlakeDetectionFinalStatus (statuses) {
1166
+ function getEarlyFlakeDetectionFinalStatus (statuses, testProperties) {
1167
+ if (testProperties.isDisabled || testProperties.isQuarantined) return 'skip'
1169
1168
  return statuses.includes('pass') ? 'pass' : 'fail'
1170
1169
  }
1171
1170
 
@@ -1274,7 +1273,6 @@ function reportFinalTestAttempt (testReport) {
1274
1273
  testProperties,
1275
1274
  'external',
1276
1275
  ['fail'],
1277
- true,
1278
1276
  true
1279
1277
  ),
1280
1278
  isRetry: (result?.retryCount || 0) > 0 || (result?.repeatCount || 0) > 0,
@@ -1604,15 +1602,18 @@ function splitNodeOptions (nodeOptions) {
1604
1602
  }
1605
1603
 
1606
1604
  function serializeNodeOptions (tokens) {
1607
- const serializedTokens = []
1605
+ let serializedTokens = ''
1606
+ let hasSerializedToken = false
1608
1607
  for (const token of tokens) {
1608
+ if (hasSerializedToken) serializedTokens += ' '
1609
1609
  if (NODE_OPTIONS_QUOTE_RE.test(token)) {
1610
- serializedTokens.push(JSON.stringify(token))
1610
+ serializedTokens += JSON.stringify(token)
1611
1611
  } else {
1612
- serializedTokens.push(token)
1612
+ serializedTokens += token
1613
1613
  }
1614
+ hasSerializedToken = true
1614
1615
  }
1615
- return serializedTokens.join(' ')
1616
+ return serializedTokens
1616
1617
  }
1617
1618
 
1618
1619
  function getTestSpecificationProject (testSpecification) {
@@ -5,6 +5,7 @@ const { performance } = require('node:perf_hooks')
5
5
  const { fileURLToPath } = require('node:url')
6
6
  const { isMainThread, parentPort } = require('node:worker_threads')
7
7
 
8
+ const { channel } = require('dc-polyfill')
8
9
  const shimmer = require('../../datadog-shimmer')
9
10
  const log = require('../../dd-trace/src/log')
10
11
  const { getEfdRetryCountForDuration } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
@@ -40,10 +41,12 @@ const {
40
41
  } = require('./vitest-util')
41
42
 
42
43
  const EFD_SUITE_ADMISSION_TIMEOUT_MS = 5000
44
+ const logSubmissionFlushCh = channel('ci:log-submission:flush')
43
45
  const taskToCtx = new WeakMap()
44
46
  const taskToTestProperties = new WeakMap()
45
47
  const taskToStatuses = new WeakMap()
46
48
  const taskToReportedErrorCount = new WeakMap()
49
+ const runnersWithLogSubmissionCleanup = new WeakSet()
47
50
  const attemptToFixTaskToStatuses = new WeakMap()
48
51
  const fileToHasConcurrentTests = new WeakMap()
49
52
  const fileToEfdSuiteAdmission = new WeakMap()
@@ -954,6 +957,14 @@ addHook({
954
957
  if (!testSuiteFinishCh.hasSubscribers) {
955
958
  return startTests.apply(this, arguments)
956
959
  }
960
+ const runner = arguments[1]
961
+ // Vitest 3+ exposes the only awaited worker-shutdown boundary; older versions keep timer/before-exit behavior.
962
+ if (logSubmissionFlushCh.hasSubscribers &&
963
+ typeof runner?.onCleanupWorkerContext === 'function' &&
964
+ !runnersWithLogSubmissionCleanup.has(runner)) {
965
+ runnersWithLogSubmissionCleanup.add(runner)
966
+ runner.onCleanupWorkerContext(() => getChannelPromise(logSubmissionFlushCh))
967
+ }
957
968
  // From >=3.0.1, the first arguments changes from a string to an object containing the filepath
958
969
  const testSuiteAbsolutePath = testPaths[0]?.filepath || testPaths[0]
959
970
  const providedContext = getProvidedContext()