dd-trace 5.110.0 → 5.112.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 (164) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/README.md +13 -8
  3. package/ci/init.js +21 -2
  4. package/ci/vitest-no-worker-init-setup.mjs +430 -0
  5. package/ext/tags.js +2 -0
  6. package/index.d.ts +34 -1
  7. package/initialize.mjs +5 -6
  8. package/loader-hook.mjs +76 -55
  9. package/package.json +37 -34
  10. package/packages/datadog-instrumentations/src/ai.js +45 -0
  11. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  12. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  13. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  14. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  15. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  16. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  17. package/packages/datadog-instrumentations/src/fs.js +8 -6
  18. package/packages/datadog-instrumentations/src/graphql.js +26 -495
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  20. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  25. package/packages/datadog-instrumentations/src/jest.js +713 -63
  26. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  27. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  28. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  29. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  30. package/packages/datadog-instrumentations/src/mongoose.js +13 -2
  31. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  32. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1297 -0
  33. package/packages/datadog-instrumentations/src/vitest-main.js +1594 -0
  34. package/packages/datadog-instrumentations/src/vitest-util.js +254 -0
  35. package/packages/datadog-instrumentations/src/vitest-worker.js +823 -0
  36. package/packages/datadog-instrumentations/src/vitest.js +11 -1855
  37. package/packages/datadog-plugin-ai/src/index.js +1 -1
  38. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  39. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  40. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +19 -10
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  43. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
  44. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  45. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  46. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  47. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  48. package/packages/datadog-plugin-cucumber/src/index.js +15 -24
  49. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +360 -14
  50. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  51. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  52. package/packages/datadog-plugin-cypress/src/support.js +45 -1
  53. package/packages/datadog-plugin-express/src/code_origin.js +1 -1
  54. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
  55. package/packages/datadog-plugin-graphql/src/execute.js +639 -12
  56. package/packages/datadog-plugin-graphql/src/index.js +37 -9
  57. package/packages/datadog-plugin-graphql/src/parse.js +24 -4
  58. package/packages/datadog-plugin-graphql/src/utils.js +9 -2
  59. package/packages/datadog-plugin-graphql/src/validate.js +17 -6
  60. package/packages/datadog-plugin-http/src/index.js +6 -8
  61. package/packages/datadog-plugin-jest/src/index.js +31 -15
  62. package/packages/datadog-plugin-mocha/src/index.js +40 -15
  63. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  64. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  65. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  66. package/packages/datadog-plugin-openai/src/services.js +2 -2
  67. package/packages/datadog-plugin-playwright/src/index.js +4 -3
  68. package/packages/datadog-plugin-vitest/src/index.js +120 -72
  69. package/packages/datadog-shimmer/src/shimmer.js +37 -16
  70. package/packages/dd-trace/src/aiguard/index.js +9 -14
  71. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  72. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  73. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  74. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  75. package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
  76. package/packages/dd-trace/src/appsec/channels.js +3 -1
  77. package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
  78. package/packages/dd-trace/src/appsec/graphql.js +14 -11
  79. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +23 -23
  80. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  81. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
  82. package/packages/dd-trace/src/appsec/index.js +10 -1
  83. package/packages/dd-trace/src/appsec/lambda.js +135 -0
  84. package/packages/dd-trace/src/appsec/reporter.js +49 -10
  85. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
  86. package/packages/dd-trace/src/appsec/waf/index.js +16 -4
  87. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
  88. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  89. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  90. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
  91. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +5 -2
  92. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  93. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  94. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +6 -2
  95. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  96. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +130 -20
  97. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
  98. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
  99. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
  100. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
  101. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
  102. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
  103. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  104. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
  105. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
  106. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
  107. package/packages/dd-trace/src/config/defaults.js +3 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +71 -57
  109. package/packages/dd-trace/src/config/helper.js +1 -0
  110. package/packages/dd-trace/src/config/index.js +35 -22
  111. package/packages/dd-trace/src/config/major-overrides.js +4 -2
  112. package/packages/dd-trace/src/config/remote_config.js +1 -1
  113. package/packages/dd-trace/src/config/supported-configurations.json +127 -56
  114. package/packages/dd-trace/src/constants.js +7 -0
  115. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  116. package/packages/dd-trace/src/datastreams/processor.js +11 -3
  117. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  118. package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
  119. package/packages/dd-trace/src/llmobs/index.js +1 -1
  120. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  121. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  122. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  123. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  124. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
  125. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  126. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  127. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  128. package/packages/dd-trace/src/llmobs/sdk.js +3 -3
  129. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  130. package/packages/dd-trace/src/llmobs/tagger.js +3 -3
  131. package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
  132. package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
  133. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
  134. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
  135. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
  136. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  137. package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
  138. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  139. package/packages/dd-trace/src/opentracing/tracer.js +6 -1
  140. package/packages/dd-trace/src/plugin_manager.js +23 -7
  141. package/packages/dd-trace/src/plugins/ci_plugin.js +159 -10
  142. package/packages/dd-trace/src/plugins/index.js +2 -0
  143. package/packages/dd-trace/src/plugins/util/llm.js +6 -1
  144. package/packages/dd-trace/src/plugins/util/test.js +15 -9
  145. package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
  146. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
  147. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
  148. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  149. package/packages/dd-trace/src/proxy.js +8 -8
  150. package/packages/dd-trace/src/span_processor.js +5 -6
  151. package/packages/dd-trace/src/span_stats.js +96 -78
  152. package/packages/dd-trace/src/startup-log.js +2 -1
  153. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  154. package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
  155. package/packages/dd-trace/src/telemetry/index.js +2 -2
  156. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  157. package/packages/dd-trace/src/telemetry/send-data.js +8 -8
  158. package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
  159. package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
  160. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  161. package/vendor/dist/protobufjs/index.js +1 -1
  162. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  163. package/vendor/dist/shell-quote/index.js +1 -1
  164. package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
@@ -2,6 +2,8 @@
2
2
 
3
3
  // Capture real timers at module load, before any test can install fake timers.
4
4
  const { performance } = require('perf_hooks')
5
+ const { statSync } = require('node:fs')
6
+ const { basename } = require('node:path')
5
7
  const dateNow = Date.now
6
8
 
7
9
  const { createCoverageMap } = require('../../../vendor/dist/istanbul-lib-coverage')
@@ -77,6 +79,8 @@ const {
77
79
  getMaxEfdRetryCount,
78
80
  getPullRequestBaseBranch,
79
81
  TEST_FINAL_STATUS,
82
+ TEST_FAILURE_SCREENSHOT_UPLOADED,
83
+ TEST_FAILURE_SCREENSHOT_UPLOAD_ERROR,
80
84
  getTestOptimizationRequestResults,
81
85
  } = require('../../dd-trace/src/plugins/util/test')
82
86
  const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
@@ -136,6 +140,127 @@ const CYPRESS_STATUS_TO_TEST_STATUS = {
136
140
  skipped: 'skip',
137
141
  }
138
142
 
143
+ const SCREENSHOT_ATTEMPT_RE = /\(attempt \d+\)/
144
+ const SCREENSHOT_UPLOAD_RESULT_UPLOADED = 'uploaded'
145
+ const SCREENSHOT_UPLOAD_RESULT_ERROR = 'error'
146
+
147
+ function getScreenshotFilePath (screenshot) {
148
+ return typeof screenshot === 'string' ? screenshot : screenshot?.path
149
+ }
150
+
151
+ function isFailureScreenshotByMetadata (screenshot, screenshotFilePath) {
152
+ if (screenshot !== null && typeof screenshot === 'object' && screenshot.testFailure !== undefined) {
153
+ return screenshot.testFailure === true
154
+ }
155
+ return screenshotFilePath.includes('(failed)')
156
+ }
157
+
158
+ /**
159
+ * Resolves a screenshot's capture time (epoch ms) for the media upload. Cypress
160
+ * screenshot objects carry an ISO `takenAt`; falls back to the file's mtime, then
161
+ * to the current time. Stamped once here and reused on retry via the idempotency
162
+ * key, so the stored object is overwritten rather than duplicated.
163
+ *
164
+ * @param {object|string} screenshot - Cypress screenshot object or path
165
+ * @param {string} filePath - Resolved screenshot file path
166
+ * @returns {number} Capture time in epoch milliseconds
167
+ */
168
+ function getScreenshotCapturedAtMs (screenshot, filePath) {
169
+ const takenAt = screenshot !== null && typeof screenshot === 'object' ? screenshot.takenAt : undefined
170
+ if (takenAt) {
171
+ const parsedMs = new Date(takenAt).getTime()
172
+ if (Number.isInteger(parsedMs) && parsedMs > 0) {
173
+ return parsedMs
174
+ }
175
+ }
176
+ try {
177
+ return Math.floor(statSync(filePath).mtimeMs)
178
+ } catch {
179
+ return dateNow()
180
+ }
181
+ }
182
+
183
+ function isFailureScreenshotForUpload (screenshot) {
184
+ const screenshotFilePath = getScreenshotFilePath(screenshot)
185
+ if (!screenshotFilePath) {
186
+ return false
187
+ }
188
+ if (screenshot !== null && typeof screenshot === 'object') {
189
+ // after:screenshot details omit testFailure for manual cy.screenshot() captures, so this
190
+ // path cannot safely fall back to the '(failed)' filename marker.
191
+ return screenshot.testFailure === true
192
+ }
193
+ return screenshotFilePath.includes('(failed)')
194
+ }
195
+
196
+ function isFailureScreenshot (screenshot) {
197
+ const screenshotFilePath = getScreenshotFilePath(screenshot)
198
+ // Require an explicit failure signal: prefer the `testFailure` metadata, and fall back to the
199
+ // '(failed)' filename marker only when the RunResult omits `testFailure`. This keeps manual
200
+ // cy.screenshot() captures out of the failure-screenshot upload (privacy).
201
+ return !!screenshotFilePath && isFailureScreenshotByMetadata(screenshot, screenshotFilePath)
202
+ }
203
+
204
+ function getAttemptScreenshots (cypressTest, attemptIndex) {
205
+ if (!Array.isArray(cypressTest.attempts)) {
206
+ return []
207
+ }
208
+ const attempt = cypressTest.attempts[attemptIndex]
209
+ if (!Array.isArray(attempt?.screenshots)) {
210
+ return []
211
+ }
212
+ return attempt.screenshots.filter(isFailureScreenshot)
213
+ }
214
+
215
+ function isScreenshotForTestAttempt (screenshot, titleParts, attemptIndex) {
216
+ const screenshotFilePath = getScreenshotFilePath(screenshot)
217
+ if (!screenshotFilePath || !isFailureScreenshot(screenshot)) {
218
+ return false
219
+ }
220
+ for (const titlePart of titleParts) {
221
+ if (!screenshotFilePath.includes(titlePart)) {
222
+ return false
223
+ }
224
+ }
225
+ if (attemptIndex === 0) {
226
+ return !SCREENSHOT_ATTEMPT_RE.test(screenshotFilePath)
227
+ }
228
+ return screenshotFilePath.includes(`(attempt ${attemptIndex + 1})`)
229
+ }
230
+
231
+ function getTestScreenshots (cypressTest, attemptIndex, specScreenshots) {
232
+ const attemptScreenshots = getAttemptScreenshots(cypressTest, attemptIndex)
233
+ if (attemptScreenshots.length > 0) {
234
+ return attemptScreenshots
235
+ }
236
+ if (!Array.isArray(specScreenshots)) {
237
+ return []
238
+ }
239
+ const titleParts = Array.isArray(cypressTest.title) ? cypressTest.title : []
240
+ return specScreenshots.filter(screenshot => isScreenshotForTestAttempt(screenshot, titleParts, attemptIndex))
241
+ }
242
+
243
+ function getScreenshotUploadResult (uploadResults) {
244
+ let hasUploaded = false
245
+ for (const uploadResult of uploadResults) {
246
+ if (uploadResult === SCREENSHOT_UPLOAD_RESULT_ERROR) {
247
+ return SCREENSHOT_UPLOAD_RESULT_ERROR
248
+ }
249
+ if (uploadResult === SCREENSHOT_UPLOAD_RESULT_UPLOADED) {
250
+ hasUploaded = true
251
+ }
252
+ }
253
+ return hasUploaded ? SCREENSHOT_UPLOAD_RESULT_UPLOADED : undefined
254
+ }
255
+
256
+ function setScreenshotUploadTags (testSpan, uploadResult) {
257
+ if (uploadResult === SCREENSHOT_UPLOAD_RESULT_ERROR) {
258
+ testSpan.setTag(TEST_FAILURE_SCREENSHOT_UPLOAD_ERROR, 'true')
259
+ } else if (uploadResult === SCREENSHOT_UPLOAD_RESULT_UPLOADED) {
260
+ testSpan.setTag(TEST_FAILURE_SCREENSHOT_UPLOADED, 'true')
261
+ }
262
+ }
263
+
139
264
  function getSessionStatus (summary) {
140
265
  if (summary.totalFailed !== undefined && summary.totalFailed > 0) {
141
266
  return 'fail'
@@ -390,6 +515,7 @@ class CypressPlugin {
390
515
  earlyFlakeDetectionFaultyThreshold = 0
391
516
  testsToSkip = []
392
517
  skippedTests = []
518
+ skippedTestIds = new Set()
393
519
  skippableTestsCoverage = {}
394
520
  testSessionCoverageMap = createCoverageMap()
395
521
  hasForcedToRunSuites = false
@@ -403,6 +529,11 @@ class CypressPlugin {
403
529
  newTestsWithDynamicNames = new Set()
404
530
  attemptToFixExecutions = new Map()
405
531
  loggedAttemptToFixTests = new Set()
532
+ uploadedScreenshotPaths = new Set()
533
+ screenshotUploadPromisesByTraceId = new Map()
534
+ afterScreenshotHandler = undefined
535
+ lastFinishedTest = null
536
+ pendingScreenshotUploads = []
406
537
 
407
538
  constructor () {
408
539
  const {
@@ -473,6 +604,7 @@ class CypressPlugin {
473
604
  this.earlyFlakeDetectionFaultyThreshold = 0
474
605
  this.testsToSkip = []
475
606
  this.skippedTests = []
607
+ this.skippedTestIds = new Set()
476
608
  this.skippableTestsCoverage = {}
477
609
  this.testSessionCoverageMap = createCoverageMap()
478
610
  this.hasForcedToRunSuites = false
@@ -487,6 +619,10 @@ class CypressPlugin {
487
619
  this.modifiedFiles = []
488
620
  this.attemptToFixExecutions = new Map()
489
621
  this.loggedAttemptToFixTests = new Set()
622
+ this.uploadedScreenshotPaths = new Set()
623
+ this.screenshotUploadPromisesByTraceId = new Map()
624
+ this.lastFinishedTest = null
625
+ this.pendingScreenshotUploads = []
490
626
  this.activeTestSpan = null
491
627
  this.testSuiteSpan = null
492
628
  this.testModuleSpan = null
@@ -503,6 +639,50 @@ class CypressPlugin {
503
639
  this._perfOrigin = 0
504
640
  }
505
641
 
642
+ /**
643
+ * Returns a stable after:screenshot handler so auto-instrumentation can recognize
644
+ * the handler registered by the manual plugin and avoid treating it as a user hook.
645
+ *
646
+ * @returns {Function} Datadog after:screenshot handler
647
+ */
648
+ getAfterScreenshotHandler () {
649
+ if (!this.afterScreenshotHandler) {
650
+ this.afterScreenshotHandler = this.afterScreenshot.bind(this)
651
+ }
652
+ return this.afterScreenshotHandler
653
+ }
654
+
655
+ /**
656
+ * Tracks a screenshot upload by its owning test trace id so the test event can be tagged
657
+ * before the span is finished.
658
+ *
659
+ * @param {string} traceId - Test trace id used for the upload
660
+ * @param {Promise<string|undefined>} uploadPromise - Promise resolving to the upload outcome
661
+ * @returns {void}
662
+ */
663
+ addScreenshotUploadPromise (traceId, uploadPromise) {
664
+ const uploadPromises = this.screenshotUploadPromisesByTraceId.get(traceId)
665
+ if (uploadPromises) {
666
+ uploadPromises.push(uploadPromise)
667
+ } else {
668
+ this.screenshotUploadPromisesByTraceId.set(traceId, [uploadPromise])
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Returns the aggregate upload outcome for all screenshots associated with a test trace id.
674
+ *
675
+ * @param {string} traceId - Test trace id used for the upload
676
+ * @returns {Promise<string|undefined>|undefined} Promise resolving to the aggregate upload outcome
677
+ */
678
+ getScreenshotUploadResultPromise (traceId) {
679
+ const uploadPromises = this.screenshotUploadPromisesByTraceId.get(traceId)
680
+ if (!uploadPromises?.length) {
681
+ return
682
+ }
683
+ return Promise.all(uploadPromises).then(getScreenshotUploadResult)
684
+ }
685
+
506
686
  /**
507
687
  * Returns the current time in the same coordinate system used by span
508
688
  * start/finish. Captured at session span creation so it shares the same
@@ -625,6 +805,37 @@ class CypressPlugin {
625
805
  })
626
806
  }
627
807
 
808
+ /**
809
+ * Warns when screenshot upload is enabled but Cypress cannot produce or send the screenshots.
810
+ *
811
+ * @param {object} cypressConfig - Cypress resolved config
812
+ * @param {object} tracer - dd-trace proxy tracer
813
+ * @param {object} testOptimizationConfig - Test Optimization config
814
+ * @returns {void}
815
+ */
816
+ warnIfMisconfiguredTestFailureScreenshots (cypressConfig, tracer, testOptimizationConfig) {
817
+ if (!testOptimizationConfig.DD_TEST_FAILURE_SCREENSHOTS_ENABLED) {
818
+ return
819
+ }
820
+
821
+ if (cypressConfig.screenshotOnRunFailure === false) {
822
+ log.warn(
823
+ '%s %s',
824
+ 'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but Cypress screenshotOnRunFailure is false.',
825
+ 'Datadog cannot upload failure screenshots unless Cypress is configured to capture them.'
826
+ )
827
+ return
828
+ }
829
+
830
+ if (!tracer?._tracer?._exporter?.canUploadTestScreenshots?.()) {
831
+ log.warn(
832
+ '%s %s',
833
+ 'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but Cypress failure screenshot upload is only supported',
834
+ 'in agentless mode.'
835
+ )
836
+ }
837
+ }
838
+
628
839
  // Init function returns a promise that resolves with the Cypress configuration
629
840
  // Depending on the received configuration, the Cypress configuration can be modified:
630
841
  // for example, to enable retries for failed tests.
@@ -637,7 +848,9 @@ class CypressPlugin {
637
848
 
638
849
  this.isTestIsolationEnabled = getIsTestIsolationEnabled(cypressConfig)
639
850
 
640
- this.rumFlushWaitMillis = getConfig().DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS
851
+ const testOptimizationConfig = getConfig().testOptimization
852
+ this.rumFlushWaitMillis = testOptimizationConfig.DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS
853
+ this.warnIfMisconfiguredTestFailureScreenshots(cypressConfig, tracer, testOptimizationConfig)
641
854
 
642
855
  if (!this.isTestIsolationEnabled) {
643
856
  log.warn('Test isolation is disabled, retries will not be enabled')
@@ -1148,7 +1361,7 @@ class CypressPlugin {
1148
1361
  this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
1149
1362
  incrementCountMetric(TELEMETRY_TEST_SESSION, {
1150
1363
  provider: this.ciProviderName,
1151
- autoInjected: !!getConfig().DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
1364
+ autoInjected: !!getConfig().testOptimization.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
1152
1365
  })
1153
1366
 
1154
1367
  finishAllTraceSpans(this.testSessionSpan)
@@ -1180,10 +1393,42 @@ class CypressPlugin {
1180
1393
  })
1181
1394
  }
1182
1395
 
1396
+ /**
1397
+ * Uploads failure screenshots as soon as Cypress creates them.
1398
+ *
1399
+ * @param {object} details - Cypress screenshot details
1400
+ * @returns {void}
1401
+ */
1402
+ afterScreenshot (details) {
1403
+ const lastFailedTestSpan = this.lastFinishedTest?.testStatus === 'fail'
1404
+ ? this.lastFinishedTest.testSpan
1405
+ : undefined
1406
+ const testSpan = this.activeTestSpan || lastFailedTestSpan
1407
+
1408
+ if (!testSpan) {
1409
+ return
1410
+ }
1411
+
1412
+ if (!isFailureScreenshotForUpload(details)) {
1413
+ return
1414
+ }
1415
+
1416
+ const screenshotUploadPromise = this.uploadTestScreenshots({
1417
+ screenshots: [details],
1418
+ traceId: testSpan.context().toTraceId(),
1419
+ })
1420
+ if (screenshotUploadPromise) {
1421
+ this.pendingScreenshotUploads.push(screenshotUploadPromise)
1422
+ }
1423
+ }
1424
+
1183
1425
  afterSpec (spec, results) {
1184
- const { tests, stats } = results || {}
1426
+ const { tests, stats, screenshots } = results || {}
1185
1427
  const cypressTests = tests || []
1428
+ const specScreenshots = screenshots || []
1186
1429
  const finishedTests = this.finishedTestsByFile[spec.relative] || []
1430
+ const screenshotUploadPromises = []
1431
+ const testSpanFinishPromises = []
1187
1432
 
1188
1433
  if (!this.testSuiteSpan) {
1189
1434
  // dd:testSuiteStart hasn't been triggered for whatever reason
@@ -1290,6 +1535,20 @@ class CypressPlugin {
1290
1535
  if (cypressTest.displayError) {
1291
1536
  latestError = new Error(cypressTest.displayError)
1292
1537
  }
1538
+
1539
+ let failedTestTraceId
1540
+ if (cypressTestStatus === 'fail' || finishedTest.testStatus === 'fail' || cypressTest.displayError) {
1541
+ failedTestTraceId = finishedTest.testSpan.context().toTraceId()
1542
+ const testScreenshots = getTestScreenshots(cypressTest, attemptIndex, specScreenshots)
1543
+ const screenshotUploadPromise = this.uploadTestScreenshots({
1544
+ screenshots: testScreenshots,
1545
+ traceId: failedTestTraceId,
1546
+ })
1547
+ if (screenshotUploadPromise) {
1548
+ screenshotUploadPromises.push(screenshotUploadPromise)
1549
+ }
1550
+ }
1551
+
1293
1552
  // Update test status - but NOT for non-ATF quarantined tests where we intentionally
1294
1553
  // report 'fail' to Datadog even though Cypress sees it as 'pass'
1295
1554
  const isQuarantinedTest = finishedTest.testSpan?.context()?.getTag(TEST_MANAGEMENT_IS_QUARANTINED) === 'true'
@@ -1346,20 +1605,102 @@ class CypressPlugin {
1346
1605
  }
1347
1606
  }
1348
1607
 
1349
- finishedTest.testSpan.finish(finishedTest.finishTime)
1608
+ const screenshotUploadResultPromise = failedTestTraceId
1609
+ ? this.getScreenshotUploadResultPromise(failedTestTraceId)
1610
+ : undefined
1611
+ if (screenshotUploadResultPromise) {
1612
+ testSpanFinishPromises.push(screenshotUploadResultPromise.then((uploadResult) => {
1613
+ setScreenshotUploadTags(finishedTest.testSpan, uploadResult)
1614
+ this.screenshotUploadPromisesByTraceId.delete(failedTestTraceId)
1615
+ finishedTest.testSpan.finish(finishedTest.finishTime)
1616
+ }))
1617
+ } else {
1618
+ finishedTest.testSpan.finish(finishedTest.finishTime)
1619
+ }
1350
1620
  }
1351
1621
  }
1352
1622
 
1353
- if (this.testSuiteSpan) {
1354
- const status = getSuiteStatus(stats)
1355
- this.testSuiteSpan.setTag(TEST_STATUS, status)
1623
+ const finishSuite = () => {
1624
+ if (this.testSuiteSpan) {
1625
+ const status = getSuiteStatus(stats)
1626
+ this.testSuiteSpan.setTag(TEST_STATUS, status)
1627
+
1628
+ if (latestError) {
1629
+ this.testSuiteSpan.setTag('error', latestError)
1630
+ }
1631
+ this.testSuiteSpan.finish()
1632
+ this.testSuiteSpan = null
1633
+ this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
1634
+ }
1635
+ }
1636
+
1637
+ const waitForScreenshotUploads = () => {
1638
+ if (screenshotUploadPromises.length > 0 || this.pendingScreenshotUploads.length > 0) {
1639
+ const pendingScreenshotUploads = this.pendingScreenshotUploads
1640
+ this.pendingScreenshotUploads = []
1641
+ return Promise.all([...pendingScreenshotUploads, ...screenshotUploadPromises]).then(() => null)
1642
+ }
1643
+ }
1644
+
1645
+ finishSuite()
1646
+
1647
+ const screenshotUploadsPromise = waitForScreenshotUploads()
1648
+ if (testSpanFinishPromises.length > 0) {
1649
+ const testSpansPromise = Promise.all(testSpanFinishPromises).then(() => null)
1650
+ if (screenshotUploadsPromise) {
1651
+ return Promise.all([testSpansPromise, screenshotUploadsPromise]).then(() => null)
1652
+ }
1653
+ return testSpansPromise
1654
+ }
1655
+
1656
+ return screenshotUploadsPromise
1657
+ }
1356
1658
 
1357
- if (latestError) {
1358
- this.testSuiteSpan.setTag('error', latestError)
1659
+ /**
1660
+ * Uploads failure screenshots for a Cypress test attempt.
1661
+ *
1662
+ * @param {object} options - Upload options
1663
+ * @param {Array<object|string>} options.screenshots - Cypress screenshot objects or screenshot paths
1664
+ * @param {string} options.traceId - Test trace id used as the screenshot key
1665
+ * @returns {Promise<string|undefined>|undefined}
1666
+ */
1667
+ uploadTestScreenshots ({ screenshots, traceId }) {
1668
+ const exporter = this.tracer?._tracer?._exporter
1669
+ if (!screenshots.length ||
1670
+ !exporter?.canUploadTestScreenshots?.() ||
1671
+ !exporter.uploadTestScreenshot) {
1672
+ return
1673
+ }
1674
+
1675
+ const uploadPromises = []
1676
+
1677
+ for (const screenshot of screenshots) {
1678
+ const filePath = getScreenshotFilePath(screenshot)
1679
+ if (!filePath || this.uploadedScreenshotPaths.has(filePath)) {
1680
+ continue
1359
1681
  }
1360
- this.testSuiteSpan.finish()
1361
- this.testSuiteSpan = null
1362
- this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
1682
+ this.uploadedScreenshotPaths.add(filePath)
1683
+ // Stable per-artifact key (trace + filename) so an upload retry overwrites the
1684
+ // same stored object instead of duplicating; the Cypress filename encodes the
1685
+ // test name and attempt, so it is unique within the trace.
1686
+ const idempotencyKey = `${traceId}:${basename(filePath)}`
1687
+ const capturedAtMs = getScreenshotCapturedAtMs(screenshot, filePath)
1688
+ uploadPromises.push(new Promise(resolve => {
1689
+ exporter.uploadTestScreenshot({
1690
+ filePath,
1691
+ traceId,
1692
+ idempotencyKey,
1693
+ capturedAtMs,
1694
+ }, (err) => {
1695
+ resolve(err ? SCREENSHOT_UPLOAD_RESULT_ERROR : SCREENSHOT_UPLOAD_RESULT_UPLOADED)
1696
+ })
1697
+ }))
1698
+ }
1699
+
1700
+ if (uploadPromises.length > 0) {
1701
+ const uploadPromise = Promise.all(uploadPromises).then(getScreenshotUploadResult)
1702
+ this.addScreenshotUploadPromise(traceId, uploadPromise)
1703
+ return uploadPromise
1363
1704
  }
1364
1705
  }
1365
1706
 
@@ -1386,7 +1727,7 @@ class CypressPlugin {
1386
1727
  return suitePayload
1387
1728
  },
1388
1729
  'dd:beforeEach': (test) => {
1389
- const { testName, testSuite, isEfdRetry, efdRetryIndex } = test
1730
+ const { testId, testName, testSuite, isEfdRetry, efdRetryIndex } = test
1390
1731
  if (isEfdRetry && this.shouldSkipEfdRetry(testSuite, testName, efdRetryIndex)) {
1391
1732
  return { shouldSkip: true, shouldDiscard: true }
1392
1733
  }
@@ -1398,7 +1739,11 @@ class CypressPlugin {
1398
1739
  const { isAttemptToFix, isDisabled, isQuarantined } = this.getTestProperties(testSuite, testName)
1399
1740
  // skip test
1400
1741
  if (shouldSkip && !isUnskippable) {
1401
- this.skippedTests.push(test)
1742
+ const skippedTestId = `${testSuite}:${testId || testName}`
1743
+ if (!this.skippedTestIds.has(skippedTestId)) {
1744
+ this.skippedTestIds.add(skippedTestId)
1745
+ this.skippedTests.push(test)
1746
+ }
1402
1747
  this.isTestsSkipped = true
1403
1748
  return { shouldSkip: true }
1404
1749
  }
@@ -1637,6 +1982,7 @@ class CypressPlugin {
1637
1982
  } else {
1638
1983
  this.finishedTestsByFile[testSuite] = [finishedTest]
1639
1984
  }
1985
+ this.lastFinishedTest = finishedTest
1640
1986
  // test spans are finished at after:spec
1641
1987
  this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
1642
1988
  hasCodeOwners: !!activeSpanTags[TEST_CODE_OWNERS],
@@ -18,7 +18,14 @@ class CypressPlugin extends Plugin {
18
18
  // Mirrors the guard in the manual plugin entrypoint (plugin.js).
19
19
  if (this._tracer._tracer instanceof NoopTracer) return
20
20
 
21
- const { on, config, userAfterSpecHandlers, userAfterRunHandlers, cleanupWrapper } = payload
21
+ const {
22
+ on,
23
+ config,
24
+ userAfterSpecHandlers,
25
+ userAfterRunHandlers,
26
+ userAfterScreenshotHandlers,
27
+ cleanupWrapper,
28
+ } = payload
22
29
 
23
30
  const registerAfterRunWithCleanup = (afterRunHandler) => {
24
31
  on('after:run', (results) => {
@@ -34,16 +41,54 @@ class CypressPlugin extends Plugin {
34
41
  }
35
42
 
36
43
  const cypressPlugin = require('./cypress-plugin')
44
+ const datadogAfterScreenshotHandler = cypressPlugin.getAfterScreenshotHandler()
45
+
46
+ // Cypress keeps a single after:screenshot handler, so registering the
47
+ // plugin's would drop any user handler (e.g. one that moves/renames a
48
+ // screenshot). Chain the user handler(s) first, threading the returned
49
+ // { path, size, dimensions } forward — Cypress uses the final path for
50
+ // downstream steps — then run the plugin's afterScreenshot on those
51
+ // details and propagate the value back to Cypress. The user handler runs
52
+ // regardless of whether screenshot upload is enabled.
53
+ const registerAfterScreenshot = (afterScreenshotHandler) => {
54
+ const filteredUserAfterScreenshotHandlers = userAfterScreenshotHandlers.filter(
55
+ handler => handler !== afterScreenshotHandler
56
+ )
57
+ if (filteredUserAfterScreenshotHandlers.length === 0) {
58
+ if (afterScreenshotHandler) on('after:screenshot', afterScreenshotHandler)
59
+ return
60
+ }
61
+
62
+ on('after:screenshot', (details) => {
63
+ const chain = filteredUserAfterScreenshotHandlers.reduce(
64
+ (p, h) => p.then((latestDetails) => Promise.resolve(h(latestDetails)).then(
65
+ // Merge the user handler's (possibly partial) return into the running details
66
+ // rather than replacing them, so Cypress metadata such as `testFailure` and
67
+ // `takenAt` survives a handler that only returns { path, size, dimensions }.
68
+ (returned) => (returned == null ? latestDetails : { ...latestDetails, ...returned })
69
+ )),
70
+ Promise.resolve(details)
71
+ )
72
+ return chain.then((finalDetails) => {
73
+ if (afterScreenshotHandler) afterScreenshotHandler(finalDetails)
74
+ return finalDetails
75
+ })
76
+ })
77
+ }
37
78
 
38
79
  if (cypressPlugin._isInit) {
39
- // Already initialized by manual plugin call — just chain user handlers
80
+ // Already initialized by manual plugin call — just chain user handlers.
81
+ // Pass the plugin's afterScreenshot so chaining a user handler doesn't drop the upload
82
+ // (the chained registration replaces the one plugin.js set, so it must include it).
40
83
  for (const h of userAfterSpecHandlers) on('after:spec', h)
84
+ registerAfterScreenshot(datadogAfterScreenshotHandler)
41
85
  registerAfterRunWithCleanup()
42
86
  payload.registered = true
43
87
  return
44
88
  }
45
89
 
46
90
  on('before:run', cypressPlugin.beforeRun.bind(cypressPlugin))
91
+ registerAfterScreenshot(datadogAfterScreenshotHandler)
47
92
 
48
93
  on('after:spec', (spec, results) => {
49
94
  const chain = userAfterSpecHandlers.reduce(
@@ -40,6 +40,7 @@ module.exports = function CypressPlugin (on, config) {
40
40
  }
41
41
 
42
42
  on('before:run', cypressPlugin.beforeRun.bind(cypressPlugin))
43
+ on('after:screenshot', cypressPlugin.getAfterScreenshotHandler())
43
44
  on('after:spec', cypressPlugin.afterSpec.bind(cypressPlugin))
44
45
  on('after:run', cypressPlugin.afterRun.bind(cypressPlugin))
45
46
  on('task', cypressPlugin.getTasks())
@@ -13,6 +13,8 @@ let testManagementTests = {}
13
13
  let isImpactedTestsEnabled = false
14
14
  let isModifiedTest = false
15
15
  let isTestIsolationEnabled = false
16
+ let hasWarnedMissingBeforeEachTaskResult = false
17
+ let hasWarnedMissingBeforeEachRetryResult = false
16
18
  // Array of test names that have been retried and the reason
17
19
  const retryReasonsByTestName = new Map()
18
20
  // Track test errors suppressed by test management so we can still report them to Datadog.
@@ -86,6 +88,47 @@ function getTestProperties (testName) {
86
88
  return { isAttemptToFix, isDisabled, isQuarantined }
87
89
  }
88
90
 
91
+ /**
92
+ * @param {string} message
93
+ * @returns {void}
94
+ */
95
+ function warnMissingBeforeEachTaskResult (message) {
96
+ // eslint-disable-next-line no-console
97
+ console.warn(message)
98
+ }
99
+
100
+ /**
101
+ * @param {object} test
102
+ * @returns {Cypress.Chainable<{ traceId?: string, shouldSkip?: boolean, shouldDiscard?: boolean }>}
103
+ */
104
+ function runBeforeEachTask (test) {
105
+ return cy.task('dd:beforeEach', test).then((taskResult) => {
106
+ if (taskResult !== undefined && taskResult !== null) {
107
+ return taskResult
108
+ }
109
+
110
+ if (!hasWarnedMissingBeforeEachTaskResult) {
111
+ hasWarnedMissingBeforeEachTaskResult = true
112
+ warnMissingBeforeEachTaskResult('Datadog Cypress dd:beforeEach task returned no result. Retrying once.')
113
+ }
114
+
115
+ return cy.task('dd:beforeEach', test).then((retryTaskResult) => {
116
+ if (retryTaskResult !== undefined && retryTaskResult !== null) {
117
+ return retryTaskResult
118
+ }
119
+
120
+ if (!hasWarnedMissingBeforeEachRetryResult) {
121
+ hasWarnedMissingBeforeEachRetryResult = true
122
+ warnMissingBeforeEachTaskResult(
123
+ 'Datadog Cypress dd:beforeEach task returned no result after retry. Continuing with an empty task result.'
124
+ )
125
+ }
126
+
127
+ return {}
128
+ })
129
+ })
130
+ }
131
+
89
132
  // Catch test failures for quarantined tests and suppress them
90
133
  // By not re-throwing the error, Cypress marks the test as passed
91
134
  // This allows quarantined tests to run but not affect the exit code
@@ -263,7 +306,8 @@ beforeEach(function () {
263
306
  originalWindow = win
264
307
  })
265
308
 
266
- cy.task('dd:beforeEach', {
309
+ runBeforeEachTask({
310
+ testId: currentTest.id,
267
311
  testName,
268
312
  testSuite: Cypress.mocha.getRootSuite().file,
269
313
  isEfdRetry: Cypress.mocha.getRunner().suite.ctx.currentTest._ddIsEfdRetry,
@@ -31,7 +31,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
31
31
  layerTags.set(layer, entryTags(topOfStackFunc))
32
32
  }
33
33
 
34
- if (this._tracerConfig.remoteConfig?.enabled) {
34
+ if (this._tracerConfig.remoteConfig.DD_REMOTE_CONFIGURATION_ENABLED) {
35
35
  // When RC is enabled, use manual subscriptions (always pre-compute)
36
36
  // This allows tags to be computed even when CO is disabled, so runtime enabling works
37
37
  dc.channel('apm:express:route:added').subscribe(handleRouteAdded)
@@ -20,7 +20,7 @@ class FastifyCodeOriginForSpansPlugin extends Plugin {
20
20
  web.getContext(req)?.span?.addTags(tags)
21
21
  })
22
22
 
23
- if (this._tracerConfig.remoteConfig?.enabled) {
23
+ if (this._tracerConfig.remoteConfig.DD_REMOTE_CONFIGURATION_ENABLED) {
24
24
  // When RC is enabled, use manual subscription (always pre-computes)
25
25
  // This allows tags to be computed even when CO is disabled, so runtime enabling works
26
26
  dc.channel('apm:fastify:route:added').subscribe(handleRouteAdded)