dd-trace
Version:
Datadog APM tracing client for JavaScript
826 lines (727 loc) • 28.4 kB
JavaScript
'use strict'
const { storage } = require('../../../datadog-core')
const { COMPONENT } = require('../constants')
const log = require('../log')
const { discoverCoverageReports } = require('../ci-visibility/coverage-report-discovery')
const {
incrementCountMetric,
distributionMetric,
TELEMETRY_EVENT_CREATED,
TELEMETRY_ITR_SKIPPED,
} = require('../ci-visibility/telemetry')
const getDiClient = require('../ci-visibility/dynamic-instrumentation')
const { DD_MAJOR } = require('../../../../version')
const id = require('../id')
const { OS_VERSION, OS_PLATFORM, OS_ARCHITECTURE, RUNTIME_NAME, RUNTIME_VERSION } = require('./util/env')
const {
CI_PROVIDER_NAME,
GIT_REPOSITORY_URL,
GIT_COMMIT_SHA,
GIT_BRANCH,
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('./util/tags')
const Plugin = require('./plugin')
const { getRepositoryRoot } = require('./util/git')
const {
getTestEnvironmentMetadata,
getTestSessionName,
getCodeOwnersFileEntries,
getTestParentSpan,
getTestCommonTags,
getCodeOwnersForFilename,
TEST_CODE_OWNERS,
CI_APP_ORIGIN,
getTestSessionCommonTags,
getTestModuleCommonTags,
TEST_SUITE_ID,
TEST_MODULE_ID,
TEST_SESSION_ID,
TEST_COMMAND,
TEST_MODULE,
TEST_SESSION_NAME,
getTestSuiteCommonTags,
TEST_STATUS,
TEST_SKIPPED_BY_ITR,
TEST_ITR_SKIPPING_ENABLED,
ITR_CORRELATION_ID,
TEST_SOURCE_FILE,
TEST_LEVEL_EVENT_TYPES,
TEST_SUITE,
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,
getLibraryCapabilitiesTags,
getPullRequestDiff,
getModifiedFilesFromDiff,
getPullRequestBaseBranch,
getSessionRequestErrorTags,
DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS,
DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS,
DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS,
DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS,
getSessionItrSkippingEnabledTags,
TEST_IS_TEST_FRAMEWORK_WORKER,
TEST_IS_NEW,
TEST_IS_RUM_ACTIVE,
TEST_BROWSER_DRIVER,
TEST_MANAGEMENT_IS_QUARANTINED,
TEST_MANAGEMENT_IS_DISABLED,
TEST_IS_MODIFIED,
TEST_IS_RETRY,
TEST_RETRY_REASON,
DD_CAPABILITIES_TEST_IMPACT_ANALYSIS,
} = require('./util/test')
const legacyStorage = storage('legacy')
const FRAMEWORK_TO_TRIMMED_COMMAND = {
vitest: 'vitest run',
mocha: 'mocha',
cucumber: 'cucumber-js',
playwright: 'playwright test',
jest: 'jest',
}
const WORKER_EXPORTER_TO_TEST_FRAMEWORK = {
vitest_worker: 'vitest',
jest_worker: 'jest',
cucumber_worker: 'cucumber',
mocha_worker: 'mocha',
playwright_worker: 'playwright',
}
const TEST_FRAMEWORKS_TO_SKIP_GIT_METADATA_EXTRACTION = new Set([
'vitest',
'jest',
'mocha',
'cucumber',
])
function setItrSkippingEnabledTagFromLibraryConfig (plugin, frameworkVersion) {
const libraryCapabilitiesTags = getLibraryCapabilitiesTags(plugin.constructor.id, frameworkVersion)
if (!libraryCapabilitiesTags[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS] ||
!plugin.libraryConfig ||
!plugin.testSessionSpan ||
!plugin.testModuleSpan) {
return
}
const skippingEnabled = plugin.libraryConfig.isSuitesSkippingEnabled ? 'true' : 'false'
plugin.testSessionSpan.setTag(TEST_ITR_SKIPPING_ENABLED, skippingEnabled)
plugin.testModuleSpan.setTag(TEST_ITR_SKIPPING_ENABLED, skippingEnabled)
}
function getTestSuiteLevelVisibilityTags (testSuiteSpan, testFramework) {
const testSuiteSpanContext = testSuiteSpan.context()
const suiteTags = {
[TEST_SUITE_ID]: testSuiteSpanContext.toSpanId(),
[TEST_SESSION_ID]: testSuiteSpanContext.toTraceId(),
[TEST_COMMAND]: testSuiteSpanContext._tags[TEST_COMMAND],
[TEST_MODULE]: testFramework,
}
if (testSuiteSpanContext._parentId) {
suiteTags[TEST_MODULE_ID] = testSuiteSpanContext._parentId.toString(10)
}
return suiteTags
}
module.exports = class CiPlugin extends Plugin {
constructor (...args) {
super(...args)
this.fileLineToProbeId = new Map()
this.rootDir = process.cwd() // fallback in case :session:start events are not emitted
this._testSuiteSpansByTestSuite = new Map()
this._pendingRequestErrorTags = []
this.addSub(`ci:${this.constructor.id}:library-configuration`, (ctx) => {
const { onDone, frameworkVersion } = ctx
ctx.currentStore = legacyStorage.getStore()
if (!this.tracer._exporter || !this.tracer._exporter.getLibraryConfiguration) {
return onDone({ err: new Error('Test optimization was not initialized correctly') })
}
this.tracer._exporter.getLibraryConfiguration(this.testConfiguration, (err, libraryConfig) => {
if (err) {
log.error('Library configuration could not be fetched. %s', err.message)
this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, err)
} else {
this.libraryConfig = libraryConfig
setItrSkippingEnabledTagFromLibraryConfig(this, frameworkVersion)
}
const requestErrorTags = this.testSessionSpan
? getSessionRequestErrorTags(this.testSessionSpan)
: Object.fromEntries(this._pendingRequestErrorTags.map(({ tag, value }) => [tag, value]))
const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, frameworkVersion)
const metadataTags = {
test: {
...libraryCapabilitiesTags,
},
}
this.tracer._exporter.addMetadataTags(metadataTags)
onDone({ err, libraryConfig, requestErrorTags })
})
})
this.addBind(`ci:${this.constructor.id}:test-suite:skippable`, (ctx) => {
return ctx.currentStore
})
this.addSub(`ci:${this.constructor.id}:test-suite:skippable`, ({ onDone }) => {
if (!this.tracer._exporter?.getSkippableSuites) {
return onDone({ err: new Error('Test optimization was not initialized correctly') })
}
this.tracer._exporter.getSkippableSuites(this.testConfiguration, (err, skippableSuites, itrCorrelationId) => {
if (err) {
log.error('Skippable suites could not be fetched. %s', err.message)
this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, err)
} else {
this.itrCorrelationId = itrCorrelationId
}
onDone({ err, skippableSuites, itrCorrelationId })
})
})
this.addSub(`ci:${this.constructor.id}:session:start`, ({ command, frameworkVersion, rootDir }) => {
const childOf = getTestParentSpan(this.tracer)
const testSessionSpanMetadata = getTestSessionCommonTags(command, frameworkVersion, this.constructor.id)
const testModuleSpanMetadata = getTestModuleCommonTags(command, frameworkVersion, this.constructor.id)
this.command = command
this.frameworkVersion = frameworkVersion
// only for playwright
this.rootDir = rootDir
const testSessionName = getTestSessionName(
this.config,
DD_MAJOR < 6 ? this.command : FRAMEWORK_TO_TRIMMED_COMMAND[this.constructor.id],
this.testEnvironmentMetadata
)
const metadataTags = {}
for (const testLevel of TEST_LEVEL_EVENT_TYPES) {
metadataTags[testLevel] = {
[TEST_SESSION_NAME]: testSessionName,
}
}
// tracer might not be initialized correctly
if (this.tracer._exporter.addMetadataTags) {
this.tracer._exporter.addMetadataTags(metadataTags)
}
this.testSessionSpan = this.tracer.startSpan(`${this.constructor.id}.test_session`, {
childOf,
tags: {
[COMPONENT]: this.constructor.id,
...this.testEnvironmentMetadata,
...testSessionSpanMetadata,
},
integrationName: this.constructor.id,
})
for (const { tag, value } of this._pendingRequestErrorTags) {
this.testSessionSpan.setTag(tag, value)
}
this._pendingRequestErrorTags = []
// TODO: add telemetry tag when we can add `is_agentless_log_submission_enabled` for agentless log submission
this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'session')
this.testModuleSpan = this.tracer.startSpan(`${this.constructor.id}.test_module`, {
childOf: this.testSessionSpan,
tags: {
[COMPONENT]: this.constructor.id,
...this.testEnvironmentMetadata,
...testModuleSpanMetadata,
...getSessionRequestErrorTags(this.testSessionSpan),
},
integrationName: this.constructor.id,
})
setItrSkippingEnabledTagFromLibraryConfig(this, frameworkVersion)
this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'module')
})
this.addSub(`ci:${this.constructor.id}:itr:skipped-suites`, ({ skippedSuites, frameworkVersion }) => {
const testCommand = this.testSessionSpan.context()._tags[TEST_COMMAND]
for (const testSuite of skippedSuites) {
const testSuiteMetadata = {
...getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, this.constructor.id),
...getSessionRequestErrorTags(this.testSessionSpan),
...getSessionItrSkippingEnabledTags(this.testSessionSpan),
}
if (this.itrCorrelationId) {
testSuiteMetadata[ITR_CORRELATION_ID] = this.itrCorrelationId
}
this.tracer.startSpan(`${this.constructor.id}.test_suite`, {
childOf: this.testModuleSpan,
tags: {
[COMPONENT]: this.constructor.id,
...this.testEnvironmentMetadata,
...testSuiteMetadata,
[TEST_STATUS]: 'skip',
[TEST_SKIPPED_BY_ITR]: 'true',
},
integrationName: this.constructor.id,
}).finish()
}
this.telemetry.count(TELEMETRY_ITR_SKIPPED, { testLevel: 'suite' }, skippedSuites.length)
})
this.addBind(`ci:${this.constructor.id}:known-tests`, (ctx) => {
return ctx.currentStore
})
this.addSub(`ci:${this.constructor.id}:known-tests`, ({ onDone }) => {
if (!this.tracer._exporter?.getKnownTests) {
return onDone({ err: new Error('Test optimization was not initialized correctly') })
}
this.tracer._exporter.getKnownTests(this.testConfiguration, (err, knownTests) => {
if (err) {
log.error('Known tests could not be fetched. %s', err.message)
this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, err)
if (this.libraryConfig) {
this.libraryConfig.isEarlyFlakeDetectionEnabled = false
this.libraryConfig.isKnownTestsEnabled = false
}
}
onDone({ err, knownTests })
})
})
this.addBind(`ci:${this.constructor.id}:test-management-tests`, (ctx) => {
return ctx.currentStore
})
this.addSub(`ci:${this.constructor.id}:test-management-tests`, ({ onDone }) => {
if (!this.tracer._exporter?.getTestManagementTests) {
return onDone({ err: new Error('Test optimization was not initialized correctly') })
}
this.tracer._exporter.getTestManagementTests(this.testConfiguration, (err, testManagementTests) => {
if (err) {
log.error('Test management tests could not be fetched. %s', err.message)
this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, err)
if (this.libraryConfig) {
this.libraryConfig.isTestManagementEnabled = false
}
}
onDone({ err, testManagementTests })
})
})
this.addBind(`ci:${this.constructor.id}:modified-files`, (ctx) => {
return ctx.currentStore
})
this.addSub(`ci:${this.constructor.id}:modified-files`, ({ onDone }) => {
const {
[GIT_PULL_REQUEST_BASE_BRANCH]: pullRequestBaseBranch,
[GIT_PULL_REQUEST_BASE_BRANCH_SHA]: pullRequestBaseBranchSha,
[GIT_COMMIT_HEAD_SHA]: commitHeadSha,
} = this.testEnvironmentMetadata
const baseBranchSha = pullRequestBaseBranchSha || getPullRequestBaseBranch(pullRequestBaseBranch)
if (baseBranchSha) {
const diff = getPullRequestDiff(baseBranchSha, commitHeadSha)
const modifiedFiles = getModifiedFilesFromDiff(diff)
if (modifiedFiles) {
return onDone({ err: null, modifiedFiles })
}
}
// TODO: Add telemetry for this type of error
return onDone({ err: new Error('No modified tests could have been retrieved') })
})
this.addSub(`ci:${this.constructor.id}:worker-report:trace`, traces => {
const formattedTraces = JSON.parse(traces)
for (const trace of formattedTraces) {
for (const span of trace) {
span.span_id = id(span.span_id)
span.trace_id = id(span.trace_id)
span.parent_id = id(span.parent_id)
if (span.name?.startsWith(`${this.constructor.id}.`)) {
span.meta[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
if (span.name === `${this.constructor.id}.test` || span.name === `${this.constructor.id}.test_suite`) {
Object.assign(span.meta, getSessionItrSkippingEnabledTags(this.testSessionSpan))
}
// augment with git information (since it will not be available in the worker)
for (const key in this.testEnvironmentMetadata) {
// CAREFUL: this bypasses the metadata/metrics distinction
// Be careful not to pass numbers in `meta`
if (key.startsWith('git.')) {
span.meta[key] = this.testEnvironmentMetadata[key]
}
}
}
// Only test hooks run in the cucumber worker, so the test events do not have the
// test session, test module and test suite ids. We have to update them here.
if (span.name === 'cucumber.test' || span.name === 'mocha.test') {
const testSuite = span.meta[TEST_SUITE]
const testSuiteSpan = this._testSuiteSpansByTestSuite.get(testSuite)
if (!testSuiteSpan) {
log.warn('Test suite span not found for test span with test suite %s', testSuite)
continue
}
const testSuiteTags = getTestSuiteLevelVisibilityTags(testSuiteSpan, this.constructor.id)
span.meta = {
...span.meta,
...testSuiteTags,
...getSessionRequestErrorTags(this.testSessionSpan),
}
}
// Jest and Vitest worker test spans are serialized in the worker and may not include
// request error tags; add them from the session span in the main process.
if ((span.name === 'jest.test' || span.name === 'vitest.test' || span.name === 'vitest.test_suite') &&
this.testSessionSpan) {
Object.assign(span.meta, getSessionRequestErrorTags(this.testSessionSpan))
}
}
this.tracer._exporter.export(trace)
}
})
this.addSub(`ci:${this.constructor.id}:worker-report:logs`, (logsPayloads) => {
for (const { logMessage } of JSON.parse(logsPayloads)) {
this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, logMessage)
}
})
}
get telemetry () {
const testFramework = this.constructor.id
const exporter = this.tracer?._exporter
// TODO: only jest worker supported yet
const isSupportedWorker = exporter && typeof exporter.exportTelemetry === 'function'
const ciProviderName = this.ciProviderName
if (isSupportedWorker) {
// In supported worker: send telemetry events to main process
return {
ciVisEvent: function (name, testLevel, tags = {}) {
exporter.exportTelemetry({
type: 'ciVisEvent',
name,
testLevel,
testFramework,
isUnsupportedCIProvider: !ciProviderName,
tags,
})
},
count: function (name, tags, value = 1) {
exporter.exportTelemetry({
type: 'count',
name,
tags,
value,
})
},
distribution: function (name, tags, measure) {
exporter.exportTelemetry({
type: 'distribution',
name,
tags,
measure,
})
},
}
}
// In main process or unsupported worker: execute telemetry directly
return {
ciVisEvent: function (name, testLevel, tags = {}) {
incrementCountMetric(name, {
testLevel,
testFramework,
isUnsupportedCIProvider: !ciProviderName,
...tags,
})
},
count: function (name, tags, value = 1) {
incrementCountMetric(name, tags, value)
},
distribution: function (name, tags, measure) {
distributionMetric(name, tags, measure)
},
}
}
/**
* Adds a hidden _dd tag to the test session span when a test-optimization request fails.
* If the session span does not exist yet (e.g. library-configuration failed before session:start),
* the tag is queued and applied when the span is created.
* @param {string} tag - Tag name (e.g. DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS)
* @param {Error} err - Request error
*/
_addRequestErrorTag (tag, err) {
const value = 'true'
if (this.testSessionSpan) {
this.testSessionSpan.setTag(tag, value)
if (this.testModuleSpan) {
this.testModuleSpan.setTag(tag, value)
}
} else {
this._pendingRequestErrorTags.push({ tag, value })
}
}
/**
* Returns request error tags from the test session span for propagation to module, suite and 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)
}
/**
* @param {import('../config/config-base')} config - Tracer configuration
* @param {boolean} shouldGetEnvironmentData - Whether to get environment data
*/
configure (config, shouldGetEnvironmentData = true) {
super.configure(config)
if (!shouldGetEnvironmentData) {
return
}
if (config.isTestDynamicInstrumentationEnabled && !this.di) {
this.di = getDiClient()
}
if (this.testConfiguration) { // no need to recalculate as it's constant
return
}
const exporter = this.config.experimental?.exporter
const workerTestFramework = WORKER_EXPORTER_TO_TEST_FRAMEWORK[exporter]
this.shouldSkipGitMetadataExtraction = workerTestFramework &&
TEST_FRAMEWORKS_TO_SKIP_GIT_METADATA_EXTRACTION.has(workerTestFramework)
this.testEnvironmentMetadata = getTestEnvironmentMetadata(
this.constructor.id,
this.config,
this.shouldSkipGitMetadataExtraction
)
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 || getRepositoryRoot() || process.cwd()
this.codeOwnersEntries = getCodeOwnersFileEntries(this.repositoryRoot)
this.ciProviderName = ciProviderName
this.testConfiguration = {
repositoryUrl,
sha,
osVersion,
osPlatform,
osArchitecture,
runtimeName,
runtimeVersion,
branch,
testLevel: 'suite',
commitMessage,
tag,
pullRequestBaseSha,
commitHeadSha,
commitHeadMessage,
}
}
getCodeOwners (tags) {
const {
[TEST_SOURCE_FILE]: testSourceFile,
[TEST_SUITE]: testSuite,
} = tags
// We'll try with the test source file if available (it could be different from the test suite)
let codeOwners = getCodeOwnersForFilename(testSourceFile, this.codeOwnersEntries)
if (!codeOwners) {
codeOwners = getCodeOwnersForFilename(testSuite, this.codeOwnersEntries)
}
return codeOwners
}
startTestSpan (testName, testSuite, testSuiteSpan, extraTags = {}) {
const childOf = getTestParentSpan(this.tracer)
let testTags = {
...getTestCommonTags(
testName,
testSuite,
this.frameworkVersion,
this.constructor.id
),
[COMPONENT]: this.constructor.id,
...extraTags,
}
const codeOwners = this.getCodeOwners(testTags)
if (codeOwners) {
testTags[TEST_CODE_OWNERS] = codeOwners
}
if (testSuiteSpan) {
// This is a hack to get good time resolution on test events, while keeping
// the test event as the root span of its trace.
childOf._trace.startTime = testSuiteSpan.context()._trace.startTime
childOf._trace.ticks = testSuiteSpan.context()._trace.ticks
const suiteTags = {
[TEST_SUITE_ID]: testSuiteSpan.context().toSpanId(),
[TEST_SESSION_ID]: testSuiteSpan.context().toTraceId(),
[TEST_COMMAND]: testSuiteSpan.context()._tags[TEST_COMMAND],
[TEST_MODULE]: this.constructor.id,
...getSessionRequestErrorTags(this.testSessionSpan),
}
if (testSuiteSpan.context()._parentId) {
suiteTags[TEST_MODULE_ID] = testSuiteSpan.context()._parentId.toString(10)
}
testTags = {
...testTags,
...suiteTags,
}
}
Object.assign(testTags, getSessionItrSkippingEnabledTags(this.testSessionSpan))
this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'test', { hasCodeOwners: !!codeOwners })
const testSpan = this.tracer
.startSpan(`${this.constructor.id}.test`, {
childOf,
tags: {
...this.testEnvironmentMetadata,
...testTags,
},
integrationName: this.constructor.id,
})
testSpan.context()._trace.origin = CI_APP_ORIGIN
return testSpan
}
onDiBreakpointHit ({ snapshot }) {
if (!this.activeTestSpan || this.activeTestSpan.context()._isFinished) {
// This is unexpected and is caused by a race condition.
log.warn('Breakpoint snapshot could not be attached to the active test span')
return
}
const stackIndex = this.testErrorStackIndex
this.activeTestSpan.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true')
this.activeTestSpan.setTag(
`${DI_DEBUG_ERROR_PREFIX}.${stackIndex}.${DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX}`,
snapshot.id
)
this.activeTestSpan.setTag(
`${DI_DEBUG_ERROR_PREFIX}.${stackIndex}.${DI_DEBUG_ERROR_FILE_SUFFIX}`,
snapshot.probe.location.file
)
this.activeTestSpan.setTag(
`${DI_DEBUG_ERROR_PREFIX}.${stackIndex}.${DI_DEBUG_ERROR_LINE_SUFFIX}`,
Number(snapshot.probe.location.lines[0])
)
const activeTestSpanContext = this.activeTestSpan.context()
this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, {
debugger: { snapshot },
dd: {
trace_id: activeTestSpanContext.toTraceId(),
span_id: activeTestSpanContext.toSpanId(),
},
})
}
removeAllDiProbes () {
if (this.fileLineToProbeId.size === 0) {
return Promise.resolve()
}
log.debug('Removing all Dynamic Instrumentation probes')
const promises = []
for (const fileLine of this.fileLineToProbeId.keys()) {
const [file, line] = fileLine.split(':')
promises.push(this.removeDiProbe({ file, line }))
}
return Promise.all(promises)
}
removeDiProbe ({ file, line }) {
const probeId = this.fileLineToProbeId.get(`${file}:${line}`)
log.warn('Removing probe from %s:%s, with id: %s', file, line, probeId)
this.fileLineToProbeId.delete(probeId)
return this.di.removeProbe(probeId)
}
addDiProbe (err) {
if (!err?.stack) {
log.warn('Can not add breakpoint if the test error does not have a stack')
return
}
const [file, line, stackIndex] = getFileAndLineNumberFromError(err, this.repositoryRoot)
if (!file || !Number.isInteger(line)) {
log.warn('Could not add breakpoint for dynamic instrumentation')
return
}
log.debug('Adding breakpoint for Dynamic Instrumentation')
this.testErrorStackIndex = stackIndex
const activeProbeKey = `${file}:${line}`
if (this.fileLineToProbeId.has(activeProbeKey)) {
log.warn('Probe already set for this line')
const oldProbeId = this.fileLineToProbeId.get(activeProbeKey)
return {
probeId: oldProbeId,
setProbePromise: Promise.resolve(),
stackIndex,
file,
line,
}
}
const [probeId, setProbePromise] = this.di.addLineProbe({ file, line }, this.onDiBreakpointHit.bind(this))
this.fileLineToProbeId.set(activeProbeKey, probeId)
return {
probeId,
setProbePromise,
stackIndex,
file,
line,
}
}
/**
* Uploads coverage reports if enabled. This is the common logic used by plugins.
* @param {object} options - Upload options
* @param {string} options.rootDir - The root directory where coverage reports are located
* @param {Function} [options.onDone] - Callback to signal completion
*/
uploadCoverageReports ({ rootDir, onDone }) {
const done = onDone || (() => {})
// Check if the exporter supports coverage report upload
if (!this.tracer._exporter?.uploadCoverageReport) {
log.debug('Exporter does not support coverage report upload')
done()
return
}
const coverageReports = discoverCoverageReports(rootDir)
if (coverageReports.length === 0) {
log.debug('No coverage reports found to upload')
done()
return
}
log.debug('Coverage report upload is enabled, found %d report(s) to upload', coverageReports.length)
// Upload reports sequentially (one file per request)
let uploadedCount = 0
let failedCount = 0
let reportIndex = 0
const uploadNextReport = () => {
if (reportIndex >= coverageReports.length) {
// All reports processed, log summary
if (failedCount > 0) {
log.warn('Coverage report upload completed: %d succeeded, %d failed', uploadedCount, failedCount)
} else {
log.info('Coverage report upload completed: %d report(s) uploaded', uploadedCount)
}
done()
return
}
const { filePath, format } = coverageReports[reportIndex]
reportIndex++
this.tracer._exporter.uploadCoverageReport(
{ filePath, format, testEnvironmentMetadata: this.testEnvironmentMetadata },
(err) => {
if (err) {
failedCount++
log.error('Failed to upload coverage report %s: %s', filePath, err.message)
} else {
uploadedCount++
}
// Process next report
uploadNextReport()
}
)
}
uploadNextReport()
}
getTestTelemetryTags (testSpan) {
const activeSpanTags = testSpan.context()._tags
return {
hasCodeOwners: !!activeSpanTags[TEST_CODE_OWNERS] || undefined,
isNew: activeSpanTags[TEST_IS_NEW] === 'true' || undefined,
isRum: activeSpanTags[TEST_IS_RUM_ACTIVE] === 'true' || undefined,
browserDriver: activeSpanTags[TEST_BROWSER_DRIVER],
isQuarantined: activeSpanTags[TEST_MANAGEMENT_IS_QUARANTINED] === 'true' || undefined,
isDisabled: activeSpanTags[TEST_MANAGEMENT_IS_DISABLED] === 'true' || undefined,
isModified: activeSpanTags[TEST_IS_MODIFIED] === 'true' || undefined,
isRetry: activeSpanTags[TEST_IS_RETRY] === 'true' || undefined,
retryReason: activeSpanTags[TEST_RETRY_REASON],
isFailedTestReplayEnabled: activeSpanTags[DI_ERROR_DEBUG_INFO_CAPTURED] === 'true' || undefined,
}
}
}