dd-trace
Version:
Datadog APM tracing client for JavaScript
1,436 lines (1,293 loc) • 60.1 kB
JavaScript
'use strict'
// Capture real timers at module load, before any test can install fake timers.
const { performance } = require('perf_hooks')
const dateNow = Date.now
const { createCoverageMap } = require('../../../vendor/dist/istanbul-lib-coverage')
const satisfies = require('../../../vendor/dist/semifies')
const {
TEST_STATUS,
TEST_IS_RUM_ACTIVE,
TEST_CODE_OWNERS,
getTestEnvironmentMetadata,
CI_APP_ORIGIN,
getTestParentSpan,
getCodeOwnersFileEntries,
getCodeOwnersForFilename,
getTestCommonTags,
getTestSessionCommonTags,
getTestModuleCommonTags,
getTestSuiteCommonTags,
TEST_SUITE_ID,
TEST_MODULE_ID,
TEST_SESSION_ID,
TEST_COMMAND,
TEST_MODULE,
TEST_SOURCE_START,
finishAllTraceSpans,
getCoveredFilesFromCoverage,
getExecutableFilesFromCoverage,
getRelativeCoverageFiles,
getTestCoverageLinesPercentage,
applySkippedCoverageToCoverage,
mergeCoverage,
getTestSuitePath,
addIntelligentTestRunnerSpanTags,
TEST_SKIPPED_BY_ITR,
TEST_ITR_UNSKIPPABLE,
TEST_ITR_FORCED_RUN,
TEST_ITR_SKIPPING_ENABLED,
ITR_CORRELATION_ID,
TEST_SOURCE_FILE,
TEST_IS_NEW,
TEST_IS_RETRY,
TEST_EARLY_FLAKE_ENABLED,
TEST_EARLY_FLAKE_ABORT_REASON,
getTestSessionName,
TEST_SESSION_NAME,
TEST_RETRY_REASON,
DD_TEST_IS_USER_PROVIDED_SERVICE,
TEST_MANAGEMENT_IS_QUARANTINED,
TEST_MANAGEMENT_ENABLED,
TEST_MANAGEMENT_IS_DISABLED,
TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX,
TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED,
TEST_HAS_FAILED_ALL_RETRIES,
getLibraryCapabilitiesTags,
TEST_RETRY_REASON_TYPES,
getPullRequestDiff,
getModifiedFilesFromDiff,
getSessionRequestErrorTags,
DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS,
DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS,
DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS,
DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS,
getSessionItrSkippingEnabledTags,
TEST_IS_MODIFIED,
TEST_HAS_DYNAMIC_NAME,
getIsFaultyEarlyFlakeDetection,
DYNAMIC_NAME_RE,
recordAttemptToFixExecution,
logAttemptToFixTestExecution,
logTestOptimizationSummary,
getEfdRetryCount,
getMaxEfdRetryCount,
getPullRequestBaseBranch,
TEST_FINAL_STATUS,
getTestOptimizationRequestResults,
} = require('../../dd-trace/src/plugins/util/test')
const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
const { RESOURCE_NAME } = require('../../../ext/tags')
const getConfig = require('../../dd-trace/src/config')
const { appClosing: appClosingTelemetry } = require('../../dd-trace/src/telemetry')
const log = require('../../dd-trace/src/log')
const {
TELEMETRY_EVENT_CREATED,
TELEMETRY_EVENT_FINISHED,
TELEMETRY_ITR_FORCED_TO_RUN,
TELEMETRY_CODE_COVERAGE_EMPTY,
TELEMETRY_ITR_UNSKIPPABLE,
TELEMETRY_CODE_COVERAGE_NUM_FILES,
incrementCountMetric,
distributionMetric,
TELEMETRY_ITR_SKIPPED,
TELEMETRY_TEST_SESSION,
} = require('../../dd-trace/src/ci-visibility/telemetry')
const {
GIT_REPOSITORY_URL,
GIT_COMMIT_SHA,
GIT_BRANCH,
CI_PROVIDER_NAME,
CI_WORKSPACE_PATH,
GIT_COMMIT_MESSAGE,
GIT_TAG,
GIT_PULL_REQUEST_BASE_BRANCH_SHA,
GIT_COMMIT_HEAD_SHA,
GIT_PULL_REQUEST_BASE_BRANCH,
GIT_COMMIT_HEAD_MESSAGE,
} = require('../../dd-trace/src/plugins/util/tags')
const {
OS_VERSION,
OS_PLATFORM,
OS_ARCHITECTURE,
RUNTIME_NAME,
RUNTIME_VERSION,
} = require('../../dd-trace/src/plugins/util/env')
const { DD_MAJOR } = require('../../../version')
const {
resolveOriginalSourceFile,
resolveSourceLineForTest,
shouldTrustInvocationDetailsLine,
} = require('./source-map-utils')
const TEST_FRAMEWORK_NAME = 'cypress'
let hasWarnedDeprecatedCypressVersion = false
const CYPRESS_STATUS_TO_TEST_STATUS = {
passed: 'pass',
failed: 'fail',
pending: 'skip',
skipped: 'skip',
}
function getSessionStatus (summary) {
if (summary.totalFailed !== undefined && summary.totalFailed > 0) {
return 'fail'
}
if (summary.totalSkipped !== undefined && summary.totalSkipped === summary.totalTests) {
return 'skip'
}
return 'pass'
}
function getCypressVersion (details) {
if (details?.cypressVersion) {
return details.cypressVersion
}
if (details?.config?.version) {
return details.config.version
}
return ''
}
function warnDeprecatedCypressVersion (version) {
if (DD_MAJOR >= 6 || hasWarnedDeprecatedCypressVersion || !version || !satisfies(version, '<12.0.0')) {
return
}
hasWarnedDeprecatedCypressVersion = true
// console.warn does not seem to work reliably in Cypress, so use console.log instead.
// eslint-disable-next-line no-console
console.log(
'WARNING: dd-trace support for Cypress<12.0.0 is deprecated' +
' and will not be supported in dd-trace v6. Please upgrade Cypress to >=12.0.0.'
)
}
function getRootDir (details) {
if (details?.config) {
return details.config.projectRoot || details.config.repoRoot || process.cwd()
}
return process.cwd()
}
function getCypressCommand (details) {
if (!details) {
return TEST_FRAMEWORK_NAME
}
return `${TEST_FRAMEWORK_NAME} ${details.specPattern || ''}`
}
function getIsTestIsolationEnabled (cypressConfig) {
if (!cypressConfig) {
// If we can't read testIsolation config parameter, we default to allowing retries
return true
}
return cypressConfig.testIsolation === undefined ? true : cypressConfig.testIsolation
}
function getLibraryConfiguration (tracer, testConfiguration) {
return new Promise(resolve => {
if (!tracer._tracer._exporter?.getLibraryConfiguration) {
return resolve({ err: new Error('Test Optimization was not initialized correctly') })
}
tracer._tracer._exporter.getLibraryConfiguration(testConfiguration, (err, libraryConfig) => {
resolve({ err, libraryConfig })
})
})
}
function getSkippableTests (tracer, testConfiguration) {
return new Promise(resolve => {
if (!tracer._tracer._exporter?.getSkippableSuites) {
return resolve({ err: new Error('Test Optimization was not initialized correctly') })
}
tracer._tracer._exporter.getSkippableSuites(
testConfiguration,
(err, skippableTests, correlationId, skippableTestsCoverage) => {
resolve({
err,
skippableTests,
correlationId,
skippableTestsCoverage,
})
}
)
})
}
function getKnownTests (tracer, testConfiguration) {
return new Promise(resolve => {
if (!tracer._tracer._exporter?.getKnownTests) {
return resolve({ err: new Error('Test Optimization was not initialized correctly') })
}
tracer._tracer._exporter.getKnownTests(testConfiguration, (err, knownTests) => {
resolve({
err,
knownTests,
})
})
})
}
function getTestManagementTests (tracer, testConfiguration) {
return new Promise(resolve => {
if (!tracer._tracer._exporter?.getTestManagementTests) {
return resolve({ err: new Error('Test Optimization was not initialized correctly') })
}
tracer._tracer._exporter.getTestManagementTests(testConfiguration, (err, testManagementTests) => {
resolve({
err,
testManagementTests,
})
})
})
}
function getModifiedFiles (testEnvironmentMetadata) {
const {
[GIT_PULL_REQUEST_BASE_BRANCH]: pullRequestBaseBranch,
[GIT_PULL_REQUEST_BASE_BRANCH_SHA]: pullRequestBaseBranchSha,
[GIT_COMMIT_HEAD_SHA]: commitHeadSha,
} = testEnvironmentMetadata
const baseBranchSha = pullRequestBaseBranchSha || getPullRequestBaseBranch(pullRequestBaseBranch)
if (baseBranchSha) {
const diff = getPullRequestDiff(baseBranchSha, commitHeadSha)
const modifiedFiles = getModifiedFilesFromDiff(diff)
if (modifiedFiles) {
return modifiedFiles
}
}
throw new Error('Modified tests could not be retrieved')
}
function getSuiteStatus (suiteStats) {
if (!suiteStats) {
return 'skip'
}
if (suiteStats.failures !== undefined && suiteStats.failures > 0) {
return 'fail'
}
if (suiteStats.tests !== undefined &&
(suiteStats.tests === suiteStats.pending || suiteStats.tests === suiteStats.skipped)) {
return 'skip'
}
return 'pass'
}
function getMatchingCypressTest (cypressTests, testName, attemptIndex, testStatus, preferIndexedMatch = false) {
let matchingTestByIndex
let matchingTestByStatus
let matchingTestIndex = 0
for (const cypressTest of cypressTests) {
if (cypressTest.title.join(' ') !== testName) {
continue
}
if (matchingTestIndex === attemptIndex) {
matchingTestByIndex = cypressTest
}
matchingTestIndex++
if (!matchingTestByStatus && CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state] === testStatus) {
matchingTestByStatus = cypressTest
}
}
return preferIndexedMatch
? matchingTestByIndex || matchingTestByStatus
: matchingTestByStatus || matchingTestByIndex
}
function isCypressHookFailure (cypressTest) {
return CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state] === 'fail' &&
/\bhook\b/.test(String(cypressTest.displayError || ''))
}
const FINAL_STATUS_RETRY_KIND = {
none: 'none',
atr: 'atr',
efd: 'efd',
atf: 'atf',
}
function getFinalStatusRetryKind ({ finishedTest, finishedTestAttempts, flakyTestRetriesCount }) {
// Infer retry kind from the executions we actually saw so ATR enabled with
// a retry count of 0 is still treated as a single final execution.
if (finishedTest.isAttemptToFix) {
return FINAL_STATUS_RETRY_KIND.atf
}
if (finishedTestAttempts.some(testAttempt => testAttempt.isEfdRetry)) {
return FINAL_STATUS_RETRY_KIND.efd
}
if (finishedTestAttempts.length > 1 && flakyTestRetriesCount > 0) {
return FINAL_STATUS_RETRY_KIND.atr
}
return FINAL_STATUS_RETRY_KIND.none
}
function getFinalStatus ({
status,
retryKind,
hasFailedAllRetries,
hasPassedAllAtfRetries,
isQuarantined,
isDisabled,
}) {
// If the test is quarantined or disabled, its final status is skip unless attempt-to-fix takes precedence.
if (status === 'skip' || (retryKind !== FINAL_STATUS_RETRY_KIND.atf && (isQuarantined || isDisabled))) {
return 'skip'
}
switch (retryKind) {
case FINAL_STATUS_RETRY_KIND.atr:
case FINAL_STATUS_RETRY_KIND.efd:
// These modes report the aggregate result across attempts.
return hasFailedAllRetries ? 'fail' : 'pass'
case FINAL_STATUS_RETRY_KIND.atf:
// Attempt-to-fix only passes if every execution passed.
return hasPassedAllAtfRetries ? 'pass' : 'fail'
default:
return status
}
}
class CypressPlugin {
_isInit = false
testEnvironmentMetadata = getTestEnvironmentMetadata(TEST_FRAMEWORK_NAME)
finishedTestsByFile = {}
testStatuses = {}
hasLibraryConfiguration = false
isItrEnabled = false
isTestsSkipped = false
isSuitesSkippingEnabled = false
isCodeCoverageEnabled = false
isCoverageReportUploadEnabled = false
isFlakyTestRetriesEnabled = false
flakyTestRetriesCount = 0
isEarlyFlakeDetectionEnabled = false
isEarlyFlakeDetectionFaulty = false
isKnownTestsEnabled = false
earlyFlakeDetectionNumRetries = 0
earlyFlakeDetectionSlowTestRetries = {}
efdRetryCountByTest = {}
efdSlowAbortedTests = {}
earlyFlakeDetectionFaultyThreshold = 0
testsToSkip = []
skippedTests = []
skippableTestsCoverage = {}
testSessionCoverageMap = createCoverageMap()
hasForcedToRunSuites = false
hasUnskippableSuites = false
unskippableSuites = []
knownTests = []
isTestManagementTestsEnabled = false
testManagementAttemptToFixRetries = 0
isImpactedTestsEnabled = false
modifiedFiles = []
newTestsWithDynamicNames = new Set()
attemptToFixExecutions = new Map()
loggedAttemptToFixTests = new Set()
constructor () {
const {
[GIT_REPOSITORY_URL]: repositoryUrl,
[GIT_COMMIT_SHA]: sha,
[OS_VERSION]: osVersion,
[OS_PLATFORM]: osPlatform,
[OS_ARCHITECTURE]: osArchitecture,
[RUNTIME_NAME]: runtimeName,
[RUNTIME_VERSION]: runtimeVersion,
[GIT_BRANCH]: branch,
[CI_PROVIDER_NAME]: ciProviderName,
[CI_WORKSPACE_PATH]: repositoryRoot,
[GIT_COMMIT_MESSAGE]: commitMessage,
[GIT_TAG]: tag,
[GIT_PULL_REQUEST_BASE_BRANCH_SHA]: pullRequestBaseSha,
[GIT_COMMIT_HEAD_SHA]: commitHeadSha,
[GIT_COMMIT_HEAD_MESSAGE]: commitHeadMessage,
} = this.testEnvironmentMetadata
this.repositoryRoot = repositoryRoot || process.cwd()
this.ciProviderName = ciProviderName
this.codeOwnersEntries = getCodeOwnersFileEntries(repositoryRoot)
this.testConfiguration = {
repositoryUrl,
sha,
osVersion,
osPlatform,
osArchitecture,
runtimeName,
runtimeVersion,
branch,
testLevel: 'test',
commitMessage,
tag,
pullRequestBaseSha,
commitHeadSha,
commitHeadMessage,
}
}
/**
* Resets state that is scoped to a single Cypress run so the singleton plugin
* can be reused safely across multiple programmatic cypress.run() calls.
*
* @returns {void}
*/
resetRunState () {
this._isInit = false
this.finishedTestsByFile = {}
this.testStatuses = {}
this.hasLibraryConfiguration = false
this.isItrEnabled = false
this.isTestsSkipped = false
this.isSuitesSkippingEnabled = false
this.isCodeCoverageEnabled = false
this.isCoverageReportUploadEnabled = false
this.isFlakyTestRetriesEnabled = false
this.flakyTestRetriesCount = 0
this.isEarlyFlakeDetectionEnabled = false
this.isEarlyFlakeDetectionFaulty = false
this.isKnownTestsEnabled = false
this.earlyFlakeDetectionNumRetries = 0
this.earlyFlakeDetectionSlowTestRetries = {}
this.efdRetryCountByTest = {}
this.efdSlowAbortedTests = {}
this.earlyFlakeDetectionFaultyThreshold = 0
this.testsToSkip = []
this.skippedTests = []
this.skippableTestsCoverage = {}
this.testSessionCoverageMap = createCoverageMap()
this.hasForcedToRunSuites = false
this.hasUnskippableSuites = false
this.unskippableSuites = []
this.knownTests = []
this.knownTestsByTestSuite = undefined
this.isTestManagementTestsEnabled = false
this.testManagementAttemptToFixRetries = 0
this.testManagementTests = undefined
this.isImpactedTestsEnabled = false
this.modifiedFiles = []
this.attemptToFixExecutions = new Map()
this.loggedAttemptToFixTests = new Set()
this.activeTestSpan = null
this.testSuiteSpan = null
this.testModuleSpan = null
this.testSessionSpan = null
this.command = undefined
this.frameworkVersion = undefined
this.rootDir = undefined
this.itrCorrelationId = undefined
this.isTestIsolationEnabled = undefined
this.rumFlushWaitMillis = undefined
this._pendingRequestErrorTags = []
this.libraryConfigurationPromise = undefined
this._timeOrigin = 0
this._perfOrigin = 0
}
/**
* Returns the current time in the same coordinate system used by span
* start/finish. Captured at session span creation so it shares the same
* epoch as the trace without reaching into span internals.
*
* @returns {number}
*/
_now () {
return this._timeOrigin + performance.now() - this._perfOrigin
}
/**
* Returns the directory used to normalize coverage file names.
*
* @returns {string}
*/
getCoverageRootDir () {
return this.repositoryRoot || this.rootDir || process.cwd()
}
/**
* Returns whether the backend supplied skipped-test coverage data.
*
* @returns {boolean}
*/
hasSkippableTestsCoverage () {
return !!(this.skippableTestsCoverage &&
typeof this.skippableTestsCoverage === 'object' &&
Object.keys(this.skippableTestsCoverage).length > 0)
}
/**
* Returns whether skipped test coverage should be backfilled into the session coverage map.
*
* @returns {boolean}
*/
shouldBackfillSkippedCoverage () {
return this.isItrEnabled &&
this.isCoverageReportUploadEnabled &&
this.isTestsSkipped &&
this.hasSkippableTestsCoverage()
}
/**
* Adds a test's Istanbul coverage to the aggregated session coverage map.
*
* @param {object} coverage
* @returns {void}
*/
addTestSessionCoverage (coverage) {
mergeCoverage(coverage, this.testSessionCoverageMap)
}
/**
* Applies backend skipped-test coverage to the aggregated session coverage map.
*
* @returns {boolean}
*/
applySkippedCoverageToTestSessionCoverage () {
if (!this.shouldBackfillSkippedCoverage()) {
return false
}
return applySkippedCoverageToCoverage(
this.testSessionCoverageMap,
this.skippableTestsCoverage,
this.getCoverageRootDir()
)
}
/**
* Calculates the total session code coverage percentage when product rules allow reporting it.
*
* @param {boolean} hasBackfilledCoverage
* @returns {number | undefined}
*/
getTestCodeCoverageLinesTotal (hasBackfilledCoverage) {
if (!this.testSessionCoverageMap.files().length || (this.isTestsSkipped && !hasBackfilledCoverage)) {
return
}
return getTestCoverageLinesPercentage(this.testSessionCoverageMap, undefined, this.getCoverageRootDir())
}
/**
* Returns repository-relative executable-line coverage files for the test session.
*
* @returns {Array<{ filename: string, bitmap: Buffer }>}
*/
getTestSessionCoverageFiles () {
return getRelativeCoverageFiles(
getExecutableFilesFromCoverage(this.testSessionCoverageMap),
this.getCoverageRootDir()
)
}
/**
* Uploads executable-line coverage for the test session when backend configuration enables it.
*
* @returns {void}
*/
reportTestSessionCoverage () {
const exporter = this.tracer._tracer._exporter
if (
!this.testSessionSpan ||
!this.isCoverageReportUploadEnabled ||
!exporter?.exportCoverage
) {
return
}
const files = this.getTestSessionCoverageFiles()
if (!files.length) {
return
}
exporter.exportCoverage({
sessionId: this.testSessionSpan.context()._traceId,
files,
})
}
// Init function returns a promise that resolves with the Cypress configuration
// Depending on the received configuration, the Cypress configuration can be modified:
// for example, to enable retries for failed tests.
init (tracer, cypressConfig) {
this.resetRunState()
this._isInit = true
this.tracer = tracer
this.cypressConfig = cypressConfig
warnDeprecatedCypressVersion(cypressConfig.version)
this.isTestIsolationEnabled = getIsTestIsolationEnabled(cypressConfig)
this.rumFlushWaitMillis = getConfig().DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS
if (!this.isTestIsolationEnabled) {
log.warn('Test isolation is disabled, retries will not be enabled')
}
// we have to do it here because the tracer is not initialized in the constructor
this.testEnvironmentMetadata[DD_TEST_IS_USER_PROVIDED_SERVICE] =
tracer._tracer._config.isServiceUserProvided ? 'true' : 'false'
this._pendingRequestErrorTags = []
this.libraryConfigurationPromise = getLibraryConfiguration(this.tracer, this.testConfiguration)
.then((libraryConfigurationResponse) => {
if (libraryConfigurationResponse.err) {
log.error('Cypress plugin library config response error', libraryConfigurationResponse.err)
this._pendingRequestErrorTags.push({
tag: DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS,
value: 'true',
})
} else {
this.hasLibraryConfiguration = true
const {
libraryConfig: {
isItrEnabled,
isSuitesSkippingEnabled,
isCodeCoverageEnabled,
isCoverageReportUploadEnabled,
isEarlyFlakeDetectionEnabled,
earlyFlakeDetectionNumRetries,
earlyFlakeDetectionSlowTestRetries,
earlyFlakeDetectionFaultyThreshold,
isFlakyTestRetriesEnabled,
flakyTestRetriesCount,
isKnownTestsEnabled,
isTestManagementEnabled,
testManagementAttemptToFixRetries,
isImpactedTestsEnabled,
},
} = libraryConfigurationResponse
this.isItrEnabled = isItrEnabled
this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
this.isCodeCoverageEnabled = isCodeCoverageEnabled
this.isCoverageReportUploadEnabled = isCoverageReportUploadEnabled
this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
this.earlyFlakeDetectionSlowTestRetries = earlyFlakeDetectionSlowTestRetries ?? {}
this.earlyFlakeDetectionFaultyThreshold = earlyFlakeDetectionFaultyThreshold
this.isKnownTestsEnabled = isKnownTestsEnabled
if (isFlakyTestRetriesEnabled && this.isTestIsolationEnabled) {
this.isFlakyTestRetriesEnabled = true
this.flakyTestRetriesCount = flakyTestRetriesCount ?? 0
this.cypressConfig.retries.runMode = this.flakyTestRetriesCount
} else {
this.flakyTestRetriesCount = 0
}
this.isTestManagementTestsEnabled = isTestManagementEnabled
this.testManagementAttemptToFixRetries = testManagementAttemptToFixRetries
this.isImpactedTestsEnabled = isImpactedTestsEnabled
}
return this.cypressConfig
})
return this.libraryConfigurationPromise
}
getIsTestModified (testSuiteAbsolutePath) {
const relativeTestSuitePath = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
if (!this.modifiedFiles) {
return false
}
const lines = this.modifiedFiles[relativeTestSuitePath]
if (!lines) {
return false
}
return lines.length > 0
}
getTestSuiteProperties (testSuite) {
return this.testManagementTests?.cypress?.suites?.[testSuite]?.tests || {}
}
getTestProperties (testSuite, testName) {
const { attempt_to_fix: isAttemptToFix, disabled: isDisabled, quarantined: isQuarantined } =
this.getTestSuiteProperties(testSuite)?.[testName]?.properties || {}
return { isAttemptToFix, isDisabled, isQuarantined }
}
/**
* Returns how many EFD retries must be scheduled before the first duration is known.
*
* @returns {number}
*/
getConfiguredEfdRetryCount () {
const { earlyFlakeDetectionSlowTestRetries } = this
if (!earlyFlakeDetectionSlowTestRetries || !Object.keys(earlyFlakeDetectionSlowTestRetries).length) {
return this.earlyFlakeDetectionNumRetries
}
return getMaxEfdRetryCount(earlyFlakeDetectionSlowTestRetries)
}
/**
* Returns the selected EFD retry count for a test, or the scheduling count if it has not run yet.
*
* @param {string} testSuite
* @param {string} testName
* @returns {number}
*/
getEfdRetryCountForTest (testSuite, testName) {
const testSuiteRetries = this.efdRetryCountByTest[testSuite]
if (!testSuiteRetries || testSuiteRetries[testName] === undefined) {
return this.getConfiguredEfdRetryCount()
}
return testSuiteRetries[testName]
}
/**
* Stores the selected EFD retry count for a test after its first execution duration is known.
*
* @param {string} testSuite
* @param {string} testName
* @param {number | undefined} duration
* @returns {number}
*/
setEfdRetryCountForTest (testSuite, testName, duration) {
if (!this.efdRetryCountByTest[testSuite]) {
this.efdRetryCountByTest[testSuite] = {}
}
const retryCount = getEfdRetryCount(duration ?? 0, this.earlyFlakeDetectionSlowTestRetries)
this.efdRetryCountByTest[testSuite][testName] = retryCount
if (retryCount === 0) {
if (!this.efdSlowAbortedTests[testSuite]) {
this.efdSlowAbortedTests[testSuite] = {}
}
this.efdSlowAbortedTests[testSuite][testName] = true
}
return retryCount
}
/**
* Returns whether an EFD retry clone is beyond the selected retry count and should be discarded.
*
* @param {string} testSuite
* @param {string} testName
* @param {number} efdRetryIndex
* @returns {boolean}
*/
shouldSkipEfdRetry (testSuite, testName, efdRetryIndex) {
const testSuiteRetries = this.efdRetryCountByTest[testSuite]
return testSuiteRetries?.[testName] !== undefined && efdRetryIndex > testSuiteRetries[testName]
}
getTestSuiteSpan ({ testSuite, testSuiteAbsolutePath }) {
const testSuiteSpanMetadata = {
...getTestSuiteCommonTags(this.command, this.frameworkVersion, testSuite, TEST_FRAMEWORK_NAME),
...this.getSessionRequestErrorTags(),
...this.getSessionItrSkippingEnabledTags(),
}
this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
if (testSuiteAbsolutePath) {
const resolvedSuiteAbsolutePath = resolveOriginalSourceFile(testSuiteAbsolutePath) || testSuiteAbsolutePath
const testSourceFile = getTestSuitePath(resolvedSuiteAbsolutePath, this.repositoryRoot)
testSuiteSpanMetadata[TEST_SOURCE_FILE] = testSourceFile
testSuiteSpanMetadata[TEST_SOURCE_START] = 1
const codeOwners = this.getTestCodeOwners({ testSuite, testSourceFile })
if (codeOwners) {
testSuiteSpanMetadata[TEST_CODE_OWNERS] = codeOwners
}
}
return this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_suite`, {
childOf: this.testModuleSpan,
tags: {
[COMPONENT]: TEST_FRAMEWORK_NAME,
...this.testEnvironmentMetadata,
...testSuiteSpanMetadata,
},
integrationName: TEST_FRAMEWORK_NAME,
})
}
getTestSpan ({ testName, testSuite, isUnskippable, isForcedToRun, testSourceFile, isDisabled, isQuarantined }) {
const testSuiteTags = {
[TEST_MODULE]: TEST_FRAMEWORK_NAME,
}
if (this.testSuiteSpan) {
testSuiteTags[TEST_SUITE_ID] = this.testSuiteSpan.context().toSpanId()
}
if (this.testSessionSpan && this.testModuleSpan) {
testSuiteTags[TEST_SESSION_ID] = this.testSessionSpan.context().toTraceId()
testSuiteTags[TEST_MODULE_ID] = this.testModuleSpan.context().toSpanId()
Object.assign(testSuiteTags, this.getSessionRequestErrorTags())
// If testSuiteSpan couldn't be created, we'll use the testModuleSpan as the parent
if (!this.testSuiteSpan) {
testSuiteTags[TEST_SUITE_ID] = this.testModuleSpan.context().toSpanId()
}
}
const childOf = getTestParentSpan(this.tracer)
const {
resource,
...testSpanMetadata
} = getTestCommonTags(testName, testSuite, this.cypressConfig.version, TEST_FRAMEWORK_NAME)
if (testSourceFile) {
testSpanMetadata[TEST_SOURCE_FILE] = testSourceFile
}
Object.assign(testSpanMetadata, this.getSessionItrSkippingEnabledTags())
const codeOwners = this.getTestCodeOwners({ testSuite, testSourceFile })
if (codeOwners) {
testSpanMetadata[TEST_CODE_OWNERS] = codeOwners
}
if (isUnskippable) {
this.hasUnskippableSuites = true
incrementCountMetric(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' })
testSpanMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
}
if (isForcedToRun) {
this.hasForcedToRunSuites = true
incrementCountMetric(TELEMETRY_ITR_FORCED_TO_RUN, { testLevel: 'suite' })
testSpanMetadata[TEST_ITR_FORCED_RUN] = 'true'
}
if (isDisabled) {
testSpanMetadata[TEST_MANAGEMENT_IS_DISABLED] = 'true'
}
if (isQuarantined) {
testSpanMetadata[TEST_MANAGEMENT_IS_QUARANTINED] = 'true'
}
this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'test', { hasCodeOwners: !!codeOwners })
return this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test`, {
childOf,
tags: {
[COMPONENT]: TEST_FRAMEWORK_NAME,
[ORIGIN_KEY]: CI_APP_ORIGIN,
...testSpanMetadata,
...this.testEnvironmentMetadata,
...testSuiteTags,
},
integrationName: TEST_FRAMEWORK_NAME,
})
}
/**
* Returns request error tags from the test session span for propagation to test spans.
* @returns {Record<string, string>}
*/
getSessionRequestErrorTags () {
return getSessionRequestErrorTags(this.testSessionSpan)
}
/**
* Returns ITR skipping-enabled tags from the test session span for propagation to child events.
*
* @returns {Record<string, string>}
*/
getSessionItrSkippingEnabledTags () {
return getSessionItrSkippingEnabledTags(this.testSessionSpan)
}
ciVisEvent (name, testLevel, tags = {}) {
incrementCountMetric(name, {
testLevel,
testFramework: 'cypress',
isUnsupportedCIProvider: !this.ciProviderName,
...tags,
})
}
async beforeRun (details) {
// We need to make sure that the plugin is initialized before running the tests
// This is for the case where the user has not returned the promise from the init function
await this.libraryConfigurationPromise
this.command = getCypressCommand(details)
this.frameworkVersion = getCypressVersion(details)
this.rootDir = getRootDir(details)
const {
knownTestsResponse,
testManagementTestsResponse,
skippableSuitesResponse: skippableTestsRequestResponse,
} = await getTestOptimizationRequestResults({
isKnownTestsEnabled: this.isKnownTestsEnabled,
isTestManagementTestsEnabled: this.isTestManagementTestsEnabled,
isSuitesSkippingEnabled: this.isSuitesSkippingEnabled,
getKnownTests: () => getKnownTests(this.tracer, this.testConfiguration),
getTestManagementTests: () => getTestManagementTests(this.tracer, this.testConfiguration),
getSkippableSuites: () => getSkippableTests(this.tracer, {
...this.testConfiguration,
isCoverageReportUploadEnabled: this.isCoverageReportUploadEnabled,
}),
})
if (this.isKnownTestsEnabled) {
const currentKnownTestsResponse = knownTestsResponse || await getKnownTests(this.tracer, this.testConfiguration)
if (currentKnownTestsResponse.err) {
log.error('Cypress known tests response error', currentKnownTestsResponse.err)
this._pendingRequestErrorTags.push({ tag: DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, value: 'true' })
this.isEarlyFlakeDetectionEnabled = false
this.isKnownTestsEnabled = false
} else {
if (currentKnownTestsResponse.knownTests?.[TEST_FRAMEWORK_NAME]) {
this.knownTestsByTestSuite = currentKnownTestsResponse.knownTests[TEST_FRAMEWORK_NAME]
} else {
this.isEarlyFlakeDetectionEnabled = false
this.isKnownTestsEnabled = false
}
}
}
if (this.isKnownTestsEnabled && details.specs) {
const testSuites = details.specs.map(({ relative }) => relative)
const isFaulty = getIsFaultyEarlyFlakeDetection(
testSuites,
this.knownTestsByTestSuite,
this.earlyFlakeDetectionFaultyThreshold
)
if (isFaulty) {
log.warn('New test detection is disabled because the number of new test files is too high.')
this.isEarlyFlakeDetectionEnabled = false
this.isEarlyFlakeDetectionFaulty = true
this.isKnownTestsEnabled = false
}
}
if (this.isSuitesSkippingEnabled) {
const skippableTestsResponse =
skippableTestsRequestResponse || await getSkippableTests(this.tracer, {
...this.testConfiguration,
isCoverageReportUploadEnabled: this.isCoverageReportUploadEnabled,
})
if (skippableTestsResponse.err) {
log.error('Cypress skippable tests response error', skippableTestsResponse.err)
this._pendingRequestErrorTags.push({ tag: DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, value: 'true' })
} else {
const { skippableTests, correlationId, skippableTestsCoverage } = skippableTestsResponse
this.testsToSkip = skippableTests || []
this.skippableTestsCoverage = skippableTestsCoverage || {}
this.itrCorrelationId = correlationId
incrementCountMetric(TELEMETRY_ITR_SKIPPED, { testLevel: 'test' }, this.testsToSkip.length)
}
}
if (this.isTestManagementTestsEnabled) {
const currentTestManagementTestsResponse =
testManagementTestsResponse || await getTestManagementTests(this.tracer, this.testConfiguration)
if (currentTestManagementTestsResponse.err) {
log.error('Cypress test management tests response error', currentTestManagementTestsResponse.err)
this._pendingRequestErrorTags.push({
tag: DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS,
value: 'true',
})
this.isTestManagementTestsEnabled = false
} else {
this.testManagementTests = currentTestManagementTestsResponse.testManagementTests
}
}
if (this.isImpactedTestsEnabled) {
try {
this.modifiedFiles = getModifiedFiles(this.testEnvironmentMetadata)
} catch (error) {
log.error(error)
this.isImpactedTestsEnabled = false
}
}
// `details.specs` are test files
if (details.specs) {
for (const { absolute, relative } of details.specs) {
const isUnskippableSuite = isMarkedAsUnskippable({ path: absolute })
if (isUnskippableSuite) {
this.unskippableSuites.push(relative)
}
}
}
const childOf = getTestParentSpan(this.tracer)
const testSessionSpanMetadata =
getTestSessionCommonTags(this.command, this.frameworkVersion, TEST_FRAMEWORK_NAME)
const testModuleSpanMetadata =
getTestModuleCommonTags(this.command, this.frameworkVersion, TEST_FRAMEWORK_NAME)
if (this.isEarlyFlakeDetectionEnabled) {
testSessionSpanMetadata[TEST_EARLY_FLAKE_ENABLED] = 'true'
}
if (this.isEarlyFlakeDetectionFaulty) {
testSessionSpanMetadata[TEST_EARLY_FLAKE_ABORT_REASON] = 'faulty'
}
const trimmedCommand = DD_MAJOR < 6 ? this.command : 'cypress run'
const testSessionName = getTestSessionName(
this.tracer._tracer._config,
trimmedCommand,
this.testEnvironmentMetadata
)
if (this.tracer._tracer._exporter?.addMetadataTags) {
const metadataTags = { '*': { [TEST_COMMAND]: this.command, [TEST_SESSION_NAME]: testSessionName } }
const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, this.frameworkVersion)
metadataTags.test = {
...libraryCapabilitiesTags,
}
this.tracer._tracer._exporter.addMetadataTags(metadataTags)
}
// Capture time references that match what startSpan records internally
// (trace.startTime = Date.now(), trace.ticks = performance.now()).
// This lets _now() produce values in the same coordinate system as
// span._startTime without accessing span internals.
this._timeOrigin = dateNow()
this._perfOrigin = performance.now()
this.testSessionSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_session`, {
childOf,
tags: {
[COMPONENT]: TEST_FRAMEWORK_NAME,
...this.testEnvironmentMetadata,
...testSessionSpanMetadata,
},
integrationName: TEST_FRAMEWORK_NAME,
})
for (const { tag, value } of this._pendingRequestErrorTags) {
this.testSessionSpan.setTag(tag, value)
}
this._pendingRequestErrorTags = []
this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'session')
const sessionRequestErrorTags = getSessionRequestErrorTags(this.testSessionSpan)
this.testModuleSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_module`, {
childOf: this.testSessionSpan,
tags: {
[COMPONENT]: TEST_FRAMEWORK_NAME,
...this.testEnvironmentMetadata,
...testModuleSpanMetadata,
...sessionRequestErrorTags,
},
integrationName: TEST_FRAMEWORK_NAME,
})
if (this.hasLibraryConfiguration) {
const skippingEnabled = this.isSuitesSkippingEnabled ? 'true' : 'false'
this.testSessionSpan.setTag(TEST_ITR_SKIPPING_ENABLED, skippingEnabled)
this.testModuleSpan.setTag(TEST_ITR_SKIPPING_ENABLED, skippingEnabled)
}
this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'module')
return details
}
afterRun (suiteStats) {
if (!this._isInit) {
log.warn('Attemping to call afterRun without initializating the plugin first')
return
}
if (this.testSessionSpan && this.testModuleSpan) {
const testStatus = getSessionStatus(suiteStats)
const hasBackfilledCoverage = this.applySkippedCoverageToTestSessionCoverage()
const testCodeCoverageLinesTotal = this.getTestCodeCoverageLinesTotal(hasBackfilledCoverage)
this.testModuleSpan.setTag(TEST_STATUS, testStatus)
this.testSessionSpan.setTag(TEST_STATUS, testStatus)
addIntelligentTestRunnerSpanTags(
this.testSessionSpan,
this.testModuleSpan,
{
isSuitesSkipped: this.isTestsSkipped,
isSuitesSkippingEnabled: this.isSuitesSkippingEnabled,
isCodeCoverageEnabled: this.isCodeCoverageEnabled,
testCodeCoverageLinesTotal,
skippingType: 'test',
skippingCount: this.skippedTests.length,
hasForcedToRunSuites: this.hasForcedToRunSuites,
hasUnskippableSuites: this.hasUnskippableSuites,
}
)
this.reportTestSessionCoverage()
if (this.isTestManagementTestsEnabled) {
this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
}
logTestOptimizationSummary({
attemptToFixExecutions: this.attemptToFixExecutions,
newTestsWithDynamicNames: this.newTestsWithDynamicNames,
})
this.testModuleSpan.finish()
this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
this.testSessionSpan.finish()
this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
incrementCountMetric(TELEMETRY_TEST_SESSION, {
provider: this.ciProviderName,
autoInjected: !!getConfig().DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
})
finishAllTraceSpans(this.testSessionSpan)
}
return new Promise(resolve => {
const finishAfterRun = () => {
this._isInit = false
appClosingTelemetry()
resolve(null)
}
const exporter = this.tracer._tracer._exporter
if (!exporter) {
finishAfterRun()
return
}
if (exporter.flush) {
exporter.flush(() => {
finishAfterRun()
})
} else if (exporter._writer) {
exporter._writer.flush(() => {
finishAfterRun()
})
} else {
finishAfterRun()
}
})
}
afterSpec (spec, results) {
const { tests, stats } = results || {}
const cypressTests = tests || []
const finishedTests = this.finishedTestsByFile[spec.relative] || []
if (!this.testSuiteSpan) {
// dd:testSuiteStart hasn't been triggered for whatever reason
// We will create the test suite span on the spot if that's the case
log.warn('There was an error creating the test suite event.')
this.testSuiteSpan = this.getTestSuiteSpan({
testSuite: spec.relative,
testSuiteAbsolutePath: spec.absolute,
})
}
// Get tests that didn't go through `dd:afterEach`
// and create a skipped test span for each of them
for (const { title } of cypressTests) {
const cypressTestName = title.join(' ')
const isTestFinished = finishedTests.find(({ testName }) => cypressTestName === testName)
if (isTestFinished) {
continue
}
const isSkippedByItr = this.testsToSkip.find(test =>
cypressTestName === test.name && spec.relative === test.suite
)
const testSourceFile = spec.absolute && this.repositoryRoot
? getTestSuitePath(spec.absolute, this.repositoryRoot)
: spec.relative
const skippedTestSpan = this.getTestSpan({ testName: cypressTestName, testSuite: spec.relative, testSourceFile })
skippedTestSpan.setTag(TEST_FINAL_STATUS, 'skip')
skippedTestSpan.setTag(TEST_STATUS, 'skip')
if (isSkippedByItr) {
skippedTestSpan.setTag(TEST_SKIPPED_BY_ITR, 'true')
}
if (this.itrCorrelationId) {
skippedTestSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
}
const { isDisabled, isQuarantined } = this.getTestProperties(spec.relative, cypressTestName)
if (isDisabled) {
skippedTestSpan.setTag(TEST_MANAGEMENT_IS_DISABLED, 'true')
} else if (isQuarantined) {
skippedTestSpan.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true')
}
skippedTestSpan.finish()
}
// Make sure that reported test statuses are the same as Cypress reports.
// This is not always the case, such as when an `after` hook fails:
// Cypress will report the last run test as failed, but we don't know that yet at `dd:afterEach`
let latestError
const finishedTestsByTestName = finishedTests.reduce((acc, finishedTest) => {
if (!acc[finishedTest.testName]) {
acc[finishedTest.testName] = []
}
acc[finishedTest.testName].push(finishedTest)
return acc
}, {})
for (const [testName, finishedTestAttempts] of Object.entries(finishedTestsByTestName)) {
for (const [attemptIndex, finishedTest] of finishedTestAttempts.entries()) {
// We can check if this is the last attempt regardless of the retry mechanism
const isLastAttempt = attemptIndex === finishedTestAttempts.length - 1
const isDatadogManagedAttempt = finishedTest.isEfdManagedTest || finishedTest.isAttemptToFix
const cypressTest = isDatadogManagedAttempt
? getMatchingCypressTest(cypressTests, testName, attemptIndex, finishedTest.testStatus, isLastAttempt) ||
cypressTests.find(test => test.title.join(' ') === testName)
: cypressTests.find(test => test.title.join(' ') === testName)
if (!cypressTest) {
continue
}
// finishedTests can include multiple tests with the same name if they have been retried
// by early flake detection. Cypress is unaware of this so .attempts does not necessarily have
// the same length as `finishedTestAttempts`
const shouldUseCapturedStatus = isDatadogManagedAttempt && !(isLastAttempt && isCypressHookFailure(cypressTest))
let cypressTestStatus = shouldUseCapturedStatus
? finishedTest.testStatus
: CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state]
if (!finishedTest.isEfdManagedTest && !finishedTest.isAttemptToFix &&
cypressTest.attempts && cypressTest.attempts[attemptIndex]) {
cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.attempts[attemptIndex].state]
const isAtrRetry = attemptIndex > 0 &&
this.isFlakyTestRetriesEnabled &&
!finishedTest.isAttemptToFix &&
!finishedTest.isEfdRetry
if (attemptIndex > 0) {
finishedTest.testSpan.setTag(TEST_IS_RETRY, 'true')
if (finishedTest.isEfdRetry) {
finishedTest.testSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
} else if (isAtrRetry) {
finishedTest.testSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
} else {
finishedTest.testSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext)
}
}
}
if (finishedTest.isEfdManagedTest && finishedTest.testStatus !== 'skip' && cypressTestStatus === 'skip') {
cypressTestStatus = finishedTest.testStatus
}
if (cypressTest.displayError) {
latestError = new Error(cypressTest.displayError)
}
// Update test status - but NOT for non-ATF quarantined tests where we intentionally
// report 'fail' to Datadog even though Cypress sees it as 'pass'
const isQuarantinedTest = finishedTest.testSpan?.context()?.getTag(TEST_MANAGEMENT_IS_QUARANTINED) === 'true'
if (cypressTestStatus !== finishedTest.testStatus && (!isQuarantinedTest || finishedTest.isAttemptToFix)) {
finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus)
finishedTest.testSpan.setTag('error', latestError)
if (finishedTest.isAttemptToFix && cypressTestStatus === 'fail') {
finishedTest.testSpan.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'false')
}
}
if (this.itrCorrelationId) {
finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
}
const resolvedSpecAbsolutePath = spec.absolute
? resolveOriginalSourceFile(spec.absolute) || spec.absolute
: spec.absolute
const testSourceFile = resolvedSpecAbsolutePath && this.repositoryRoot
? getTestSuitePath(resolvedSpecAbsolutePath, this.repositoryRoot)
: spec.relative
if (testSourceFile) {
finishedTest.testSpan.setTag(TEST_SOURCE_FILE, testSourceFile)
}
const codeOwners = this.getTestCodeOwners({ testSuite: spec.relative, testSourceFile })
if (codeOwners) {
finishedTest.testSpan.setTag(TEST_CODE_OWNERS, codeOwners)
}
if (isLastAttempt) {
const testSpanTags = finishedTest.testSpan.context().getTags()
const retryKind = getFinalStatusRetryKind({
finishedTest,
finishedTestAttempts,
flakyTestRetriesCount: this.flakyTestRetriesCount,
})
const hasFailedAllRetries = testSpanTags[TEST_HAS_FAILED_ALL_RETRIES] === 'true'
const hasPassedAllAtfRetries =
testSpanTags[TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED] === 'true'
const isQuarantined = testSpanTags[TEST_MANAGEMENT_IS_QUARANTINED] === 'true'
const isDisabled = testSpanTags[TEST_MANAGEMENT_IS_DISABLED] === 'true'
const finalStatus = getFinalStatus({
status: cypressTestStatus,
retryKind,
hasFailedAllRetries,
hasPassedAllAtfRetries,
isQuarantined,
isDisabled,
})
if (finalStatus) {
finishedTest.testSpan.setTag(TEST_FINAL_STATUS, finalStatus)
}
}
finishedTest.testSpan.finish(finishedTest.finishTime)
}
}
if (this.testSuiteSpan) {
const status = getSuiteStatus(stats)
this.testSuiteSpan.setTag(TEST_STATUS, status)
if (latestError) {
this.testSuiteSpan.setTag('error', latestError)
}
this.testSuiteSpan.finish()
this.testSuiteSpan = null
this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
}
}
getTasks () {
return {
'dd:testSuiteStart': ({ testSuite, testSuiteAbsolutePath }) => {
const suitePayload = {
isEarlyFlakeDetectionEnabled: this.isEarlyFlakeDetectionEnabled,
knownTestsForSuite: this.knownTestsByTestSuite?.[testSuite] || [],
earlyFlakeDetectionNumRetries: this.getConfiguredEfdRetryCount(),
earlyFlakeDetectionSlowTestRetries: this.earlyFlakeDetectionSlowTestRetries,
isKnownTestsEnabled: this.isKnownTestsEnabled,
isTestManagementEnabled: this.isTestManagementTestsEnabled,
testManagementAttemptToFixRetries: this.testManagementAttemptToFixRetries,
testManagementTests: this.getTestSuiteProperties(testSuite),
isImpactedTestsEnabled: this.isImpactedTestsEnabled,
isModifiedTest: this.getIsTestModified(testSuiteAbsolutePath),
repositoryRoot: this.repositoryRoot,
isTestIsolationEnabled: this.isTestIsolationEnabled,
rumFlushWaitMillis: this.rumFlushWaitMillis,
}
this.testSuiteSpan ||= this.getTestSuiteSpan({ testSuite, testSuiteAbsolutePath })
return suitePayload
},
'dd:beforeEach': (test) => {
const { testName, testSuite, isEfdRetry, efdRetryIndex } = test
if (isEfdRetry && this.shouldSkipEfdRetry(testSuite, testName, efdRetryIndex)) {
return { shouldSkip: true, shouldDiscard: true }
}
const shouldSkip = this.testsToSkip.some(test => {
return testName === test.name && testSuite === test.suite
})
const isUnskippable = this.unskippableSuites.includes(testSuite)
const isForcedToRun = shouldSkip && isUnskippable
const { isAttemptToFix, isDisabled, isQuarantined } = this.getTestProperties(testSuite, testName)
// skip test
if (shouldSkip && !isUnskippable) {
this.skippedTests.push(test)
this.isTestsSkipped = true
return { shouldSkip: true }
}
if (isAttemptToFix) {
logAttemptToFixTestExecution(testSuite, testName, this.loggedAttemptToFixTests)
}
// For disabled tests (not attemptToFix), skip them
if (!isAttemptToFix && isDisabled) {
return { shouldSkip: true }
}
// Quarantined tests (not attemptToFix) run normally but their failures are caught
// by Cypress.on('fail') in support.js and suppressed, so Cypress sees them as passed
if (!this.activeTestSpan) {
this.activeTestSpan = this.getTestSpan({
testName,
testSuite,
isUnskippable,
isForcedToRun,
isDisabled,
isQuarantined,
})
}
return this.activeTestSpan ? { traceId: this.activeTestSpan.context().toTraceId() } : {}
},
'dd:afterEach': ({ test, coverage, commands }) => {
if (!this.activeTestSpan) {
log.warn('There is no active test span in dd:afterEach handler')
return null
}
const {
state,
error,
isRUMActive,
testSourceLine,
testSourceStack,
testSuite,
testSuiteAbsolutePath,
testName,
tes