dd-trace
Version:
Datadog APM tracing client for JavaScript
793 lines (700 loc) • 24.9 kB
JavaScript
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
}
}