UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

793 lines (700 loc) 24.9 kB
const path = require('path') const fs = require('fs') const { URL } = require('url') const log = require('../../log') const istanbul = require('istanbul-lib-coverage') const ignore = require('ignore') const { getGitMetadata } = require('./git') const { getUserProviderGitMetadata, validateGitRepositoryUrl, validateGitCommitSha } = require('./user-provided-git') const { getCIMetadata } = require('./ci') const { getRuntimeAndOSMetadata } = require('./env') const { GIT_BRANCH, GIT_COMMIT_SHA, GIT_REPOSITORY_URL, GIT_TAG, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, GIT_COMMIT_MESSAGE, CI_WORKSPACE_PATH, CI_PIPELINE_URL, CI_JOB_NAME } = require('./tags') const id = require('../../id') const { SPAN_TYPE, RESOURCE_NAME, SAMPLING_PRIORITY } = require('../../../../../ext/tags') const { SAMPLING_RULE_DECISION } = require('../../constants') const { AUTO_KEEP } = require('../../../../../ext/priority') const { version: ddTraceVersion } = require('../../../../../package.json') // session tags const TEST_SESSION_NAME = 'test_session.name' const TEST_FRAMEWORK = 'test.framework' const TEST_FRAMEWORK_VERSION = 'test.framework_version' const TEST_TYPE = 'test.type' const TEST_NAME = 'test.name' const TEST_SUITE = 'test.suite' const TEST_STATUS = 'test.status' const TEST_PARAMETERS = 'test.parameters' const TEST_SKIP_REASON = 'test.skip_reason' const TEST_IS_RUM_ACTIVE = 'test.is_rum_active' const TEST_CODE_OWNERS = 'test.codeowners' const TEST_SOURCE_FILE = 'test.source.file' const TEST_SOURCE_START = 'test.source.start' const LIBRARY_VERSION = 'library_version' const TEST_COMMAND = 'test.command' const TEST_MODULE = 'test.module' const TEST_SESSION_ID = 'test_session_id' const TEST_MODULE_ID = 'test_module_id' const TEST_SUITE_ID = 'test_suite_id' const TEST_TOOLCHAIN = 'test.toolchain' const TEST_SKIPPED_BY_ITR = 'test.skipped_by_itr' // Early flake detection const TEST_IS_NEW = 'test.is_new' const TEST_IS_RETRY = 'test.is_retry' const TEST_EARLY_FLAKE_ENABLED = 'test.early_flake.enabled' const TEST_EARLY_FLAKE_ABORT_REASON = 'test.early_flake.abort_reason' const TEST_RETRY_REASON = 'test.retry_reason' const TEST_HAS_FAILED_ALL_RETRIES = 'test.has_failed_all_retries' const CI_APP_ORIGIN = 'ciapp-test' const JEST_TEST_RUNNER = 'test.jest.test_runner' const JEST_DISPLAY_NAME = 'test.jest.display_name' const CUCUMBER_IS_PARALLEL = 'test.cucumber.is_parallel' const MOCHA_IS_PARALLEL = 'test.mocha.is_parallel' const TEST_ITR_TESTS_SKIPPED = '_dd.ci.itr.tests_skipped' const TEST_ITR_SKIPPING_ENABLED = 'test.itr.tests_skipping.enabled' const TEST_ITR_SKIPPING_TYPE = 'test.itr.tests_skipping.type' const TEST_ITR_SKIPPING_COUNT = 'test.itr.tests_skipping.count' const TEST_CODE_COVERAGE_ENABLED = 'test.code_coverage.enabled' const TEST_ITR_UNSKIPPABLE = 'test.itr.unskippable' const TEST_ITR_FORCED_RUN = 'test.itr.forced_run' const ITR_CORRELATION_ID = 'itr_correlation_id' const TEST_CODE_COVERAGE_LINES_PCT = 'test.code_coverage.lines_pct' // selenium tags const TEST_BROWSER_DRIVER = 'test.browser.driver' const TEST_BROWSER_DRIVER_VERSION = 'test.browser.driver_version' const TEST_BROWSER_NAME = 'test.browser.name' const TEST_BROWSER_VERSION = 'test.browser.version' // jest worker variables const JEST_WORKER_TRACE_PAYLOAD_CODE = 60 const JEST_WORKER_COVERAGE_PAYLOAD_CODE = 61 const JEST_WORKER_LOGS_PAYLOAD_CODE = 62 // cucumber worker variables const CUCUMBER_WORKER_TRACE_PAYLOAD_CODE = 70 // mocha worker variables const MOCHA_WORKER_TRACE_PAYLOAD_CODE = 80 // playwright worker variables const PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE = 90 // Early flake detection util strings const EFD_STRING = "Retried by Datadog's Early Flake Detection" const EFD_TEST_NAME_REGEX = new RegExp(EFD_STRING + ' \\(#\\d+\\): ', 'g') // Library Capabilities Tagging const DD_CAPABILITIES_TEST_IMPACT_ANALYSIS = '_dd.library_capabilities.test_impact_analysis' const DD_CAPABILITIES_EARLY_FLAKE_DETECTION = '_dd.library_capabilities.early_flake_detection' const DD_CAPABILITIES_AUTO_TEST_RETRIES = '_dd.library_capabilities.auto_test_retries' const DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE = '_dd.library_capabilities.test_management.quarantine' const DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE = '_dd.library_capabilities.test_management.disable' const DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX = '_dd.library_capabilities.test_management.attempt_to_fix' const UNSUPPORTED_TIA_FRAMEWORKS = ['playwright', 'vitest'] const UNSUPPORTED_TIA_FRAMEWORKS_PARALLEL_MODE = ['cucumber', 'mocha'] const UNSUPPORTED_ATTEMPT_TO_FIX_FRAMEWORKS_PARALLEL_MODE = ['mocha'] const TEST_LEVEL_EVENT_TYPES = [ 'test', 'test_suite_end', 'test_module_end', 'test_session_end' ] const TEST_RETRY_REASON_TYPES = { efd: 'early_flake_detection', atr: 'auto_test_retry', atf: 'attempt_to_fix', ext: 'external' } const DD_TEST_IS_USER_PROVIDED_SERVICE = '_dd.test.is_user_provided_service' // Dynamic instrumentation - Test optimization integration tags const DI_ERROR_DEBUG_INFO_CAPTURED = 'error.debug_info_captured' const DI_DEBUG_ERROR_PREFIX = '_dd.debug.error' const DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX = 'snapshot_id' const DI_DEBUG_ERROR_FILE_SUFFIX = 'file' const DI_DEBUG_ERROR_LINE_SUFFIX = 'line' // Test Management tags const TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX = 'test.test_management.is_attempt_to_fix' const TEST_MANAGEMENT_IS_DISABLED = 'test.test_management.is_test_disabled' const TEST_MANAGEMENT_IS_QUARANTINED = 'test.test_management.is_quarantined' const TEST_MANAGEMENT_ENABLED = 'test.test_management.enabled' const TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED = 'test.test_management.attempt_to_fix_passed' // Test Management utils strings const ATTEMPT_TO_FIX_STRING = "Retried by Datadog's Test Management" const ATTEMPT_TEST_NAME_REGEX = new RegExp(ATTEMPT_TO_FIX_STRING + ' \\(#\\d+\\): ', 'g') module.exports = { TEST_CODE_OWNERS, TEST_SESSION_NAME, TEST_FRAMEWORK, TEST_FRAMEWORK_VERSION, JEST_TEST_RUNNER, JEST_DISPLAY_NAME, CUCUMBER_IS_PARALLEL, MOCHA_IS_PARALLEL, TEST_TYPE, TEST_NAME, TEST_SUITE, TEST_STATUS, TEST_PARAMETERS, TEST_SKIP_REASON, TEST_IS_RUM_ACTIVE, TEST_SOURCE_FILE, CI_APP_ORIGIN, LIBRARY_VERSION, JEST_WORKER_TRACE_PAYLOAD_CODE, JEST_WORKER_COVERAGE_PAYLOAD_CODE, JEST_WORKER_LOGS_PAYLOAD_CODE, CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, MOCHA_WORKER_TRACE_PAYLOAD_CODE, PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE, TEST_SOURCE_START, TEST_SKIPPED_BY_ITR, TEST_IS_NEW, TEST_IS_RETRY, TEST_EARLY_FLAKE_ENABLED, TEST_EARLY_FLAKE_ABORT_REASON, TEST_RETRY_REASON, TEST_HAS_FAILED_ALL_RETRIES, getTestEnvironmentMetadata, getTestParametersString, finishAllTraceSpans, getTestParentSpan, getTestSuitePath, getCodeOwnersFileEntries, getCodeOwnersForFilename, getTestCommonTags, getTestSessionCommonTags, getTestModuleCommonTags, getTestSuiteCommonTags, TEST_COMMAND, TEST_TOOLCHAIN, TEST_SESSION_ID, TEST_MODULE_ID, TEST_SUITE_ID, TEST_ITR_TESTS_SKIPPED, TEST_MODULE, TEST_ITR_SKIPPING_ENABLED, TEST_ITR_SKIPPING_TYPE, TEST_ITR_SKIPPING_COUNT, TEST_CODE_COVERAGE_ENABLED, TEST_CODE_COVERAGE_LINES_PCT, TEST_ITR_UNSKIPPABLE, TEST_ITR_FORCED_RUN, ITR_CORRELATION_ID, addIntelligentTestRunnerSpanTags, getCoveredFilenamesFromCoverage, resetCoverage, mergeCoverage, fromCoverageMapToCoverage, getTestLineStart, removeInvalidMetadata, parseAnnotations, EFD_STRING, EFD_TEST_NAME_REGEX, removeEfdStringFromTestName, removeAttemptToFixStringFromTestName, addEfdStringToTestName, addAttemptToFixStringToTestName, getIsFaultyEarlyFlakeDetection, TEST_BROWSER_DRIVER, TEST_BROWSER_DRIVER_VERSION, TEST_BROWSER_NAME, TEST_BROWSER_VERSION, getTestSessionName, DD_CAPABILITIES_TEST_IMPACT_ANALYSIS, DD_CAPABILITIES_EARLY_FLAKE_DETECTION, DD_CAPABILITIES_AUTO_TEST_RETRIES, DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE, DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE, DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX, TEST_LEVEL_EVENT_TYPES, TEST_RETRY_REASON_TYPES, getNumFromKnownTests, getFileAndLineNumberFromError, DI_ERROR_DEBUG_INFO_CAPTURED, DI_DEBUG_ERROR_PREFIX, DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX, DI_DEBUG_ERROR_FILE_SUFFIX, DI_DEBUG_ERROR_LINE_SUFFIX, getFormattedError, DD_TEST_IS_USER_PROVIDED_SERVICE, TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX, TEST_MANAGEMENT_IS_DISABLED, TEST_MANAGEMENT_IS_QUARANTINED, TEST_MANAGEMENT_ENABLED, TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, getLibraryCapabilitiesTags } // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19 function getPkgManager () { try { return process.env.npm_config_user_agent.split(' ')[0].replace('/', '-') } catch (e) { return '' } } function validateUrl (url) { try { const urlObject = new URL(url) return (urlObject.protocol === 'https:' || urlObject.protocol === 'http:') } catch (e) { return false } } function removeInvalidMetadata (metadata) { return Object.keys(metadata).reduce((filteredTags, tag) => { if (tag === GIT_REPOSITORY_URL) { if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) { log.error('Repository URL is not a valid repository URL: %s.', metadata[GIT_REPOSITORY_URL]) return filteredTags } } if (tag === GIT_COMMIT_SHA) { if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) { log.error('Git commit SHA must be a full-length git SHA: %s.', metadata[GIT_COMMIT_SHA]) return filteredTags } } if (tag === CI_PIPELINE_URL) { if (!validateUrl(metadata[CI_PIPELINE_URL])) { return filteredTags } } filteredTags[tag] = metadata[tag] return filteredTags }, {}) } function getTestEnvironmentMetadata (testFramework, config) { // TODO: eventually these will come from the tracer (generally available) const ciMetadata = getCIMetadata() const { [GIT_COMMIT_SHA]: commitSHA, [GIT_BRANCH]: branch, [GIT_REPOSITORY_URL]: repositoryUrl, [GIT_TAG]: tag, [GIT_COMMIT_AUTHOR_NAME]: authorName, [GIT_COMMIT_AUTHOR_EMAIL]: authorEmail, [GIT_COMMIT_MESSAGE]: commitMessage, [CI_WORKSPACE_PATH]: ciWorkspacePath } = ciMetadata const gitMetadata = getGitMetadata({ commitSHA, branch, repositoryUrl, tag, authorName, authorEmail, commitMessage, ciWorkspacePath }) const userProvidedGitMetadata = getUserProviderGitMetadata() const runtimeAndOSMetadata = getRuntimeAndOSMetadata() const metadata = { [TEST_FRAMEWORK]: testFramework, [DD_TEST_IS_USER_PROVIDED_SERVICE]: (config && config.isServiceUserProvided) ? 'true' : 'false', ...gitMetadata, ...ciMetadata, ...userProvidedGitMetadata, ...runtimeAndOSMetadata } if (config && config.service) { metadata['service.name'] = config.service } return removeInvalidMetadata(metadata) } function getTestParametersString (parametersByTestName, testName) { if (!parametersByTestName[testName]) { return '' } try { // test is invoked with each parameter set sequencially const testParameters = parametersByTestName[testName].shift() return JSON.stringify({ arguments: testParameters, metadata: {} }) } catch (e) { // We can't afford to interrupt the test if `testParameters` is not serializable to JSON, // so we ignore the test parameters and move on return '' } } function getTestTypeFromFramework (testFramework) { if (testFramework === 'playwright' || testFramework === 'cypress') { return 'browser' } return 'test' } function finishAllTraceSpans (span) { span.context()._trace.started.forEach(traceSpan => { if (traceSpan !== span) { traceSpan.finish() } }) } function getTestParentSpan (tracer) { return tracer.extract('text_map', { 'x-datadog-trace-id': id().toString(10), 'x-datadog-parent-id': '0000000000000000' }) } function getTestCommonTags (name, suite, version, testFramework) { return { [SPAN_TYPE]: 'test', [TEST_TYPE]: getTestTypeFromFramework(testFramework), [SAMPLING_RULE_DECISION]: 1, [SAMPLING_PRIORITY]: AUTO_KEEP, [TEST_NAME]: name, [TEST_SUITE]: suite, [RESOURCE_NAME]: `${suite}.${name}`, [TEST_FRAMEWORK_VERSION]: version, [LIBRARY_VERSION]: ddTraceVersion } } /** * We want to make sure that test suites are reported the same way for * every OS, so we replace `path.sep` by `/` */ function getTestSuitePath (testSuiteAbsolutePath, sourceRoot) { if (!testSuiteAbsolutePath) { return sourceRoot } const testSuitePath = testSuiteAbsolutePath === sourceRoot ? testSuiteAbsolutePath : path.relative(sourceRoot, testSuiteAbsolutePath) return testSuitePath.replace(path.sep, '/') } const POSSIBLE_CODEOWNERS_LOCATIONS = [ 'CODEOWNERS', '.github/CODEOWNERS', 'docs/CODEOWNERS', '.gitlab/CODEOWNERS' ] function readCodeOwners (rootDir) { for (const location of POSSIBLE_CODEOWNERS_LOCATIONS) { try { return fs.readFileSync(path.join(rootDir, location)).toString() } catch (e) { // retry with next path } } return '' } function getCodeOwnersFileEntries (rootDir) { let codeOwnersContent let usedRootDir = rootDir let isTriedCwd = false const processCwd = process.cwd() if (!usedRootDir || usedRootDir === processCwd) { usedRootDir = processCwd isTriedCwd = true } codeOwnersContent = readCodeOwners(usedRootDir) // If we haven't found CODEOWNERS in the provided root dir, we try with process.cwd() if (!codeOwnersContent && !isTriedCwd) { codeOwnersContent = readCodeOwners(processCwd) } if (!codeOwnersContent) { return null } const entries = [] const lines = codeOwnersContent.split('\n') for (const line of lines) { const [content] = line.split('#') const trimmed = content.trim() if (trimmed === '') continue const [pattern, ...owners] = trimmed.split(/\s+/) entries.push({ pattern, owners }) } // Reverse because rules defined last take precedence return entries.reverse() } function getCodeOwnersForFilename (filename, entries) { if (!entries) { return null } for (const entry of entries) { try { const isResponsible = ignore().add(entry.pattern).ignores(filename) if (isResponsible) { return JSON.stringify(entry.owners) } } catch (e) { return null } } return null } function getTestLevelCommonTags (command, testFrameworkVersion, testFramework) { return { [TEST_FRAMEWORK_VERSION]: testFrameworkVersion, [LIBRARY_VERSION]: ddTraceVersion, [TEST_COMMAND]: command, [TEST_TYPE]: getTestTypeFromFramework(testFramework) } } function getTestSessionCommonTags (command, testFrameworkVersion, testFramework) { return { [SPAN_TYPE]: 'test_session_end', [RESOURCE_NAME]: `test_session.${command}`, [TEST_MODULE]: testFramework, [TEST_TOOLCHAIN]: getPkgManager(), ...getTestLevelCommonTags(command, testFrameworkVersion, testFramework) } } function getTestModuleCommonTags (command, testFrameworkVersion, testFramework) { return { [SPAN_TYPE]: 'test_module_end', [RESOURCE_NAME]: `test_module.${command}`, [TEST_MODULE]: testFramework, ...getTestLevelCommonTags(command, testFrameworkVersion, testFramework) } } function getTestSuiteCommonTags (command, testFrameworkVersion, testSuite, testFramework) { return { [SPAN_TYPE]: 'test_suite_end', [RESOURCE_NAME]: `test_suite.${testSuite}`, [TEST_MODULE]: testFramework, [TEST_SUITE]: testSuite, ...getTestLevelCommonTags(command, testFrameworkVersion, testFramework) } } function addIntelligentTestRunnerSpanTags ( testSessionSpan, testModuleSpan, { isSuitesSkipped, isSuitesSkippingEnabled, isCodeCoverageEnabled, testCodeCoverageLinesTotal, skippingCount, skippingType = 'suite', hasUnskippableSuites, hasForcedToRunSuites } ) { testSessionSpan.setTag(TEST_ITR_TESTS_SKIPPED, isSuitesSkipped ? 'true' : 'false') testSessionSpan.setTag(TEST_ITR_SKIPPING_ENABLED, isSuitesSkippingEnabled ? 'true' : 'false') testSessionSpan.setTag(TEST_ITR_SKIPPING_TYPE, skippingType) testSessionSpan.setTag(TEST_ITR_SKIPPING_COUNT, skippingCount) testSessionSpan.setTag(TEST_CODE_COVERAGE_ENABLED, isCodeCoverageEnabled ? 'true' : 'false') testModuleSpan.setTag(TEST_ITR_TESTS_SKIPPED, isSuitesSkipped ? 'true' : 'false') testModuleSpan.setTag(TEST_ITR_SKIPPING_ENABLED, isSuitesSkippingEnabled ? 'true' : 'false') testModuleSpan.setTag(TEST_ITR_SKIPPING_TYPE, skippingType) testModuleSpan.setTag(TEST_ITR_SKIPPING_COUNT, skippingCount) testModuleSpan.setTag(TEST_CODE_COVERAGE_ENABLED, isCodeCoverageEnabled ? 'true' : 'false') if (hasUnskippableSuites) { testSessionSpan.setTag(TEST_ITR_UNSKIPPABLE, 'true') testModuleSpan.setTag(TEST_ITR_UNSKIPPABLE, 'true') } if (hasForcedToRunSuites) { testSessionSpan.setTag(TEST_ITR_FORCED_RUN, 'true') testModuleSpan.setTag(TEST_ITR_FORCED_RUN, 'true') } // This will not be reported unless the user has manually added code coverage. // This is always the case for Mocha and Cucumber, but not for Jest. if (testCodeCoverageLinesTotal !== undefined) { testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal) testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal) } } function getCoveredFilenamesFromCoverage (coverage) { const coverageMap = istanbul.createCoverageMap(coverage) return coverageMap .files() .filter(filename => { const fileCoverage = coverageMap.fileCoverageFor(filename) const lineCoverage = fileCoverage.getLineCoverage() const isAnyLineExecuted = Object.entries(lineCoverage).some(([, numExecutions]) => !!numExecutions) return isAnyLineExecuted }) } function resetCoverage (coverage) { const coverageMap = istanbul.createCoverageMap(coverage) return coverageMap .files() .forEach(filename => { const fileCoverage = coverageMap.fileCoverageFor(filename) fileCoverage.resetHits() }) } function mergeCoverage (coverage, targetCoverage) { const coverageMap = istanbul.createCoverageMap(coverage) return coverageMap .files() .forEach(filename => { const fileCoverage = coverageMap.fileCoverageFor(filename) // If the fileCoverage is not there for this filename, // we create it to force a merge between the fileCoverages // instead of a reference assignment (which would not work if the coverage is reset later on) if (!targetCoverage.data[filename]) { targetCoverage.addFileCoverage(istanbul.createFileCoverage(filename)) } targetCoverage.addFileCoverage(fileCoverage) const targetFileCoverage = targetCoverage.fileCoverageFor(filename) // branches (.b) are copied by reference, so `resetHits` affects the copy, so we need to copy it manually Object.entries(targetFileCoverage.data.b).forEach(([key, value]) => { targetFileCoverage.data.b[key] = [...value] }) }) } function fromCoverageMapToCoverage (coverageMap) { return Object.entries(coverageMap.data).reduce((acc, [filename, fileCoverage]) => { acc[filename] = fileCoverage.data return acc }, {}) } // Get the start line of a test by inspecting a given error's stack trace function getTestLineStart (err, testSuitePath) { if (!err.stack) { return null } // From https://github.com/felixge/node-stack-trace/blob/ba06dcdb50d465cd440d84a563836e293b360427/index.js#L40 const testFileLine = err.stack.split('\n').find(line => line.includes(testSuitePath)) try { const testFileLineMatch = testFileLine.match(/at (?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/) return parseInt(testFileLineMatch[3], 10) || null } catch (e) { return null } } /** * Gets an object of test tags from an Playwright annotations array. * @param {Object[]} annotations - Annotations from a Playwright test. * @param {string} annotations[].type - Type of annotation. A string of the shape DD_TAGS[$tag_name]. * @param {string} annotations[].description - Value of the tag. */ function parseAnnotations (annotations) { return annotations.reduce((tags, annotation) => { if (!annotation?.type) { return tags } const { type, description } = annotation if (type.startsWith('DD_TAGS')) { const regex = /\[(.*?)\]/ const match = regex.exec(type) let tagValue = '' if (match) { tagValue = match[1] } if (tagValue) { tags[tagValue] = description } } return tags }, {}) } function addEfdStringToTestName (testName, numAttempt) { return `${EFD_STRING} (#${numAttempt}): ${testName}` } function addAttemptToFixStringToTestName (testName, numAttempt) { return `${ATTEMPT_TO_FIX_STRING} (#${numAttempt}): ${testName}` } function removeEfdStringFromTestName (testName) { return testName.replace(EFD_TEST_NAME_REGEX, '') } function removeAttemptToFixStringFromTestName (testName) { return testName.replace(ATTEMPT_TEST_NAME_REGEX, '') } function getIsFaultyEarlyFlakeDetection (projectSuites, testsBySuiteName, faultyThresholdPercentage) { let newSuites = 0 for (const suite of projectSuites) { if (!testsBySuiteName[suite]) { newSuites++ } } const newSuitesPercentage = (newSuites / projectSuites.length) * 100 // The faulty threshold represents a percentage, but we also want to consider // smaller projects, where big variations in the % are more likely. // This is why we also check the absolute number of new suites. return ( newSuites > faultyThresholdPercentage && newSuitesPercentage > faultyThresholdPercentage ) } function getTestSessionName (config, testCommand, envTags) { if (config.ciVisibilityTestSessionName) { return config.ciVisibilityTestSessionName } if (envTags[CI_JOB_NAME]) { return `${envTags[CI_JOB_NAME]}-${testCommand}` } return testCommand } // Calculate the number of a tests from the known tests response, which has a shape like: // { testModule1: { testSuite1: [test1, test2, test3] }, testModule2: { testSuite2: [test4, test5] } } function getNumFromKnownTests (knownTests) { if (!knownTests) { return 0 } let totalNumTests = 0 for (const testModule of Object.values(knownTests)) { for (const testSuite of Object.values(testModule)) { totalNumTests += testSuite.length } } return totalNumTests } const DEPENDENCY_FOLDERS = [ 'node_modules', 'node:', '.pnpm', '.yarn', '.pnp' ] function getFileAndLineNumberFromError (error, repositoryRoot) { // Split the stack trace into individual lines const stackLines = error.stack.split('\n') // Remove potential messages on top of the stack that are not frames const frames = stackLines.filter(line => line.includes('at ') && line.includes(repositoryRoot)) const topRelevantFrameIndex = frames.findIndex(line => line.includes(repositoryRoot) && !DEPENDENCY_FOLDERS.some(pattern => line.includes(pattern)) ) if (topRelevantFrameIndex === -1) { return [] } const topFrame = frames[topRelevantFrameIndex] // Regular expression to match the file path, line number, and column number const regex = /\s*at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/ const match = topFrame.match(regex) if (match) { const filePath = match[1] const lineNumber = Number(match[2]) return [filePath, lineNumber, topRelevantFrameIndex] } return [] } function getFormattedError (error, repositoryRoot) { const newError = new Error(error.message) if (error.stack) { newError.stack = error.stack.split('\n').filter(line => line.includes(repositoryRoot)).join('\n') } newError.name = error.name return newError } function getLibraryCapabilitiesTags (testFramework, isParallel) { function isTiaSupported (testFramework, isParallel) { if (UNSUPPORTED_TIA_FRAMEWORKS.includes(testFramework)) { return false } if (isParallel && UNSUPPORTED_TIA_FRAMEWORKS_PARALLEL_MODE.includes(testFramework)) { return false } return true } function isAttemptToFixSupported (testFramework, isParallel) { if (isParallel && UNSUPPORTED_ATTEMPT_TO_FIX_FRAMEWORKS_PARALLEL_MODE.includes(testFramework)) { return false } return true } return { [DD_CAPABILITIES_TEST_IMPACT_ANALYSIS]: isTiaSupported(testFramework, isParallel) ? '1' : undefined, [DD_CAPABILITIES_EARLY_FLAKE_DETECTION]: '1', [DD_CAPABILITIES_AUTO_TEST_RETRIES]: '1', [DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE]: '1', [DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE]: '1', [DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX]: isAttemptToFixSupported(testFramework, isParallel) ? '2' : undefined } }