UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

826 lines (727 loc) 28.4 kB
'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, } } }