dd-trace 6.0.0 → 6.1.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.
- package/LICENSE-3rdparty.csv +1 -2
- package/index.d.ts +21 -1
- package/package.json +32 -29
- package/packages/datadog-instrumentations/src/ai.js +45 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
- package/packages/datadog-instrumentations/src/child_process.js +3 -3
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
- package/packages/datadog-instrumentations/src/cucumber.js +102 -43
- package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
- package/packages/datadog-instrumentations/src/fastify.js +27 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/jest.js +713 -63
- package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
- package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
- package/packages/datadog-instrumentations/src/mongodb.js +74 -0
- package/packages/datadog-instrumentations/src/mongoose.js +4 -5
- package/packages/datadog-instrumentations/src/playwright.js +13 -9
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +28 -1
- package/packages/datadog-instrumentations/src/vitest-util.js +5 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +104 -34
- package/packages/datadog-plugin-ai/src/index.js +1 -1
- package/packages/datadog-plugin-ai/src/tracing.js +66 -3
- package/packages/datadog-plugin-ai/src/utils.js +17 -4
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
- package/packages/datadog-plugin-child_process/src/index.js +13 -2
- package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
- package/packages/datadog-plugin-cucumber/src/index.js +14 -23
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
- package/packages/datadog-plugin-cypress/src/index.js +47 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
- package/packages/datadog-plugin-graphql/src/execute.js +21 -10
- package/packages/datadog-plugin-graphql/src/index.js +17 -1
- package/packages/datadog-plugin-graphql/src/utils.js +9 -2
- package/packages/datadog-plugin-graphql/src/validate.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +30 -14
- package/packages/datadog-plugin-mocha/src/index.js +39 -14
- package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
- package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
- package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
- package/packages/datadog-plugin-playwright/src/index.js +3 -2
- package/packages/datadog-plugin-vitest/src/index.js +51 -9
- package/packages/dd-trace/src/aiguard/index.js +9 -14
- package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
- package/packages/dd-trace/src/appsec/graphql.js +9 -6
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +37 -0
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugin_manager.js +16 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
// Capture real timers at module load time, before any test can install fake timers.
|
|
4
|
-
const realDateNow = Date.now.bind(Date)
|
|
5
|
-
const realSetTimeout = setTimeout
|
|
6
|
-
|
|
7
3
|
const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
|
|
8
4
|
const { storage } = require('../../datadog-core')
|
|
9
5
|
const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
|
|
@@ -54,9 +50,6 @@ const {
|
|
|
54
50
|
TELEMETRY_TEST_SESSION,
|
|
55
51
|
} = require('../../dd-trace/src/ci-visibility/telemetry')
|
|
56
52
|
|
|
57
|
-
const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200
|
|
58
|
-
const BREAKPOINT_SET_GRACE_PERIOD_MS = 400
|
|
59
|
-
|
|
60
53
|
const isCucumberWorker = !!getEnvironmentVariable('CUCUMBER_WORKER_ID')
|
|
61
54
|
|
|
62
55
|
class CucumberPlugin extends CiPlugin {
|
|
@@ -248,17 +241,14 @@ class CucumberPlugin extends CiPlugin {
|
|
|
248
241
|
ctx.currentStore = { ...store, span }
|
|
249
242
|
|
|
250
243
|
this.activeTestSpan = span
|
|
251
|
-
// Time we give the breakpoint to be hit
|
|
252
244
|
if (promises && this.runningTestProbe) {
|
|
253
|
-
promises.hitBreakpointPromise =
|
|
254
|
-
realSetTimeout(resolve, BREAKPOINT_HIT_GRACE_PERIOD_MS)
|
|
255
|
-
})
|
|
245
|
+
promises.hitBreakpointPromise = this.waitForDiBreakpointHits()
|
|
256
246
|
}
|
|
257
247
|
|
|
258
248
|
return ctx.currentStore
|
|
259
249
|
})
|
|
260
250
|
|
|
261
|
-
this.addSub('ci:cucumber:test:retry', ({ span, isFirstAttempt, error, isAtrRetry }) => {
|
|
251
|
+
this.addSub('ci:cucumber:test:retry', ({ span, isFirstAttempt, error, isAtrRetry, promises, canWaitForDi }) => {
|
|
262
252
|
if (!isFirstAttempt) {
|
|
263
253
|
span.setTag(TEST_IS_RETRY, 'true')
|
|
264
254
|
if (isAtrRetry) {
|
|
@@ -268,18 +258,18 @@ class CucumberPlugin extends CiPlugin {
|
|
|
268
258
|
}
|
|
269
259
|
}
|
|
270
260
|
span.setTag('error', error)
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// However, Cucumber doesn't have a mechanism for waiting asyncrounously here, so for now, we'll have to
|
|
281
|
-
// fall back to a fixed syncronous delay.
|
|
261
|
+
if (canWaitForDi !== false && promises && this.di && error && this.libraryConfig?.isDiEnabled) {
|
|
262
|
+
if (isFirstAttempt) {
|
|
263
|
+
const probeInformation = this.addDiProbe(error)
|
|
264
|
+
if (probeInformation) {
|
|
265
|
+
const { file, line, stackIndex, setProbePromise } = probeInformation
|
|
266
|
+
this.runningTestProbe = { file, line }
|
|
267
|
+
this.testErrorStackIndex = stackIndex
|
|
268
|
+
this.prepareDiBreakpointHitWait()
|
|
269
|
+
promises.setProbePromise = this.waitForDiOperation(setProbePromise)
|
|
282
270
|
}
|
|
271
|
+
} else if (this.runningTestProbe) {
|
|
272
|
+
this.prepareDiBreakpointHitWait()
|
|
283
273
|
}
|
|
284
274
|
}
|
|
285
275
|
span.setTag(TEST_STATUS, 'fail')
|
|
@@ -410,6 +400,7 @@ class CucumberPlugin extends CiPlugin {
|
|
|
410
400
|
this.tracer._exporter.flush()
|
|
411
401
|
}
|
|
412
402
|
this.activeTestSpan = null
|
|
403
|
+
this.cancelDiBreakpointHitWait()
|
|
413
404
|
if (this.runningTestProbe) {
|
|
414
405
|
this.removeDiProbe(this.runningTestProbe)
|
|
415
406
|
this.runningTestProbe = null
|
|
@@ -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'
|
|
@@ -404,6 +529,11 @@ class CypressPlugin {
|
|
|
404
529
|
newTestsWithDynamicNames = new Set()
|
|
405
530
|
attemptToFixExecutions = new Map()
|
|
406
531
|
loggedAttemptToFixTests = new Set()
|
|
532
|
+
uploadedScreenshotPaths = new Set()
|
|
533
|
+
screenshotUploadPromisesByTraceId = new Map()
|
|
534
|
+
afterScreenshotHandler = undefined
|
|
535
|
+
lastFinishedTest = null
|
|
536
|
+
pendingScreenshotUploads = []
|
|
407
537
|
|
|
408
538
|
constructor () {
|
|
409
539
|
const {
|
|
@@ -489,6 +619,10 @@ class CypressPlugin {
|
|
|
489
619
|
this.modifiedFiles = []
|
|
490
620
|
this.attemptToFixExecutions = new Map()
|
|
491
621
|
this.loggedAttemptToFixTests = new Set()
|
|
622
|
+
this.uploadedScreenshotPaths = new Set()
|
|
623
|
+
this.screenshotUploadPromisesByTraceId = new Map()
|
|
624
|
+
this.lastFinishedTest = null
|
|
625
|
+
this.pendingScreenshotUploads = []
|
|
492
626
|
this.activeTestSpan = null
|
|
493
627
|
this.testSuiteSpan = null
|
|
494
628
|
this.testModuleSpan = null
|
|
@@ -505,6 +639,50 @@ class CypressPlugin {
|
|
|
505
639
|
this._perfOrigin = 0
|
|
506
640
|
}
|
|
507
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
|
+
|
|
508
686
|
/**
|
|
509
687
|
* Returns the current time in the same coordinate system used by span
|
|
510
688
|
* start/finish. Captured at session span creation so it shares the same
|
|
@@ -627,6 +805,37 @@ class CypressPlugin {
|
|
|
627
805
|
})
|
|
628
806
|
}
|
|
629
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
|
+
|
|
630
839
|
// Init function returns a promise that resolves with the Cypress configuration
|
|
631
840
|
// Depending on the received configuration, the Cypress configuration can be modified:
|
|
632
841
|
// for example, to enable retries for failed tests.
|
|
@@ -639,7 +848,9 @@ class CypressPlugin {
|
|
|
639
848
|
|
|
640
849
|
this.isTestIsolationEnabled = getIsTestIsolationEnabled(cypressConfig)
|
|
641
850
|
|
|
642
|
-
|
|
851
|
+
const testOptimizationConfig = getConfig().testOptimization
|
|
852
|
+
this.rumFlushWaitMillis = testOptimizationConfig.DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS
|
|
853
|
+
this.warnIfMisconfiguredTestFailureScreenshots(cypressConfig, tracer, testOptimizationConfig)
|
|
643
854
|
|
|
644
855
|
if (!this.isTestIsolationEnabled) {
|
|
645
856
|
log.warn('Test isolation is disabled, retries will not be enabled')
|
|
@@ -1182,10 +1393,42 @@ class CypressPlugin {
|
|
|
1182
1393
|
})
|
|
1183
1394
|
}
|
|
1184
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
|
+
|
|
1185
1425
|
afterSpec (spec, results) {
|
|
1186
|
-
const { tests, stats } = results || {}
|
|
1426
|
+
const { tests, stats, screenshots } = results || {}
|
|
1187
1427
|
const cypressTests = tests || []
|
|
1428
|
+
const specScreenshots = screenshots || []
|
|
1188
1429
|
const finishedTests = this.finishedTestsByFile[spec.relative] || []
|
|
1430
|
+
const screenshotUploadPromises = []
|
|
1431
|
+
const testSpanFinishPromises = []
|
|
1189
1432
|
|
|
1190
1433
|
if (!this.testSuiteSpan) {
|
|
1191
1434
|
// dd:testSuiteStart hasn't been triggered for whatever reason
|
|
@@ -1292,6 +1535,20 @@ class CypressPlugin {
|
|
|
1292
1535
|
if (cypressTest.displayError) {
|
|
1293
1536
|
latestError = new Error(cypressTest.displayError)
|
|
1294
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
|
+
|
|
1295
1552
|
// Update test status - but NOT for non-ATF quarantined tests where we intentionally
|
|
1296
1553
|
// report 'fail' to Datadog even though Cypress sees it as 'pass'
|
|
1297
1554
|
const isQuarantinedTest = finishedTest.testSpan?.context()?.getTag(TEST_MANAGEMENT_IS_QUARANTINED) === 'true'
|
|
@@ -1348,20 +1605,102 @@ class CypressPlugin {
|
|
|
1348
1605
|
}
|
|
1349
1606
|
}
|
|
1350
1607
|
|
|
1351
|
-
|
|
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
|
+
}
|
|
1352
1620
|
}
|
|
1353
1621
|
}
|
|
1354
1622
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
+
}
|
|
1358
1658
|
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
|
1361
1681
|
}
|
|
1362
|
-
this.
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
|
1365
1704
|
}
|
|
1366
1705
|
}
|
|
1367
1706
|
|
|
@@ -1643,6 +1982,7 @@ class CypressPlugin {
|
|
|
1643
1982
|
} else {
|
|
1644
1983
|
this.finishedTestsByFile[testSuite] = [finishedTest]
|
|
1645
1984
|
}
|
|
1985
|
+
this.lastFinishedTest = finishedTest
|
|
1646
1986
|
// test spans are finished at after:spec
|
|
1647
1987
|
this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
|
|
1648
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 {
|
|
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())
|