UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

284 lines (263 loc) 7.94 kB
'use strict' const getConfig = require('../../config') const request = require('../requests/request') const log = require('../../log') const { incrementCountMetric, distributionMetric, TELEMETRY_ITR_SKIPPABLE_TESTS, TELEMETRY_ITR_SKIPPABLE_TESTS_MS, TELEMETRY_ITR_SKIPPABLE_TESTS_ERRORS, TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES, TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS, TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, } = require('../../ci-visibility/telemetry') const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache') const { validateSkippableTestsResponse } = require('../test-optimization-http-cache-schema') function parseJsonResponse (rawJson) { return typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson } function parseSkippableSuitesResponse ( rawJson, { testLevel = 'suite', isCoverageReportUploadEnabled = false, validateRequiredFields = false, validationMode = false, } = {} ) { const parsedResponse = parseJsonResponse(rawJson) if (validateRequiredFields) { validateSkippableTestsResponse(parsedResponse, { validationMode }) } const coverage = {} for (const [filename, bitmap] of Object.entries(parsedResponse.meta?.coverage || {})) { coverage[filename.replaceAll('\\', '/')] = bitmap } const skippableItems = parsedResponse .data .filter(({ type }) => type === testLevel) const skippableSuites = [] let numExcludedByMissingLineCoverage = 0 for (const { attributes: { suite, name, _is_missing_line_code_coverage: isMissingLineCodeCoverage, }, } of skippableItems) { // Only reject candidates without backend line coverage when we need that coverage to backfill reports. if (isCoverageReportUploadEnabled && isMissingLineCodeCoverage) { numExcludedByMissingLineCoverage++ continue } skippableSuites.push(testLevel === 'suite' ? suite : { suite, name }) } const correlationId = parsedResponse.meta?.correlation_id return { skippableSuites, correlationId, coverage, numReceivedSkippableItems: skippableItems.length, numExcludedByMissingLineCoverage, } } /** * Logs how missing line coverage affected the skippable candidates without logging their contents. * * @param {{ * skippableSuites: Array<string|{suite: string, name: string}>, * numReceivedSkippableItems?: number, * numExcludedByMissingLineCoverage?: number * }} result - Parsed skippable response. * @param {'suite'|'test'} testLevel - Test optimization skipping level. * @returns {void} */ function logSkippableSuitesResponse (result, testLevel) { if (result.numReceivedSkippableItems === undefined) { log.debug('Number of received skippable %ss: %d', testLevel, result.skippableSuites.length) return } log.debug( 'Received %d skippable %s candidates; excluded %d because line coverage is missing; %d remain.', result.numReceivedSkippableItems, testLevel, result.numExcludedByMissingLineCoverage, result.skippableSuites.length ) if ( result.numReceivedSkippableItems > 0 && result.numExcludedByMissingLineCoverage === result.numReceivedSkippableItems ) { log.warn( 'All %d skippable %s candidates were excluded: coverage upload is enabled but line coverage is missing.', result.numReceivedSkippableItems, testLevel ) } } function getSkippableSuites ({ url, isEvpProxy, evpProxyPrefix, isGzipCompatible, env, service, repositoryUrl, sha, osVersion, osPlatform, osArchitecture, runtimeName, runtimeVersion, custom, testLevel = 'suite', isCoverageReportUploadEnabled = false, }, done) { const cacheKey = buildCacheKey('skippable', [ sha, service, env, repositoryUrl, osPlatform, osVersion, osArchitecture, runtimeName, runtimeVersion, testLevel, custom, isCoverageReportUploadEnabled, ]) withCache(cacheKey, (activeCacheKey, cb) => { fetchFromApi({ url, isEvpProxy, evpProxyPrefix, isGzipCompatible, env, service, repositoryUrl, sha, osVersion, osPlatform, osArchitecture, runtimeName, runtimeVersion, custom, testLevel, isCoverageReportUploadEnabled, cacheKey: activeCacheKey, }, cb) }, (err, data) => { if (err) return done(err) logSkippableSuitesResponse(data, testLevel) done(null, data.skippableSuites, data.correlationId, data.coverage) }) } /** * Fetches skippable suites from the API and writes the result to cache on success. * * @param {object} params * @param {string} params.url * @param {boolean} params.isEvpProxy * @param {string} params.evpProxyPrefix * @param {boolean} params.isGzipCompatible * @param {string} params.env * @param {string} params.service * @param {string} params.repositoryUrl * @param {string} params.sha * @param {string} params.osVersion * @param {string} params.osPlatform * @param {string} params.osArchitecture * @param {string} params.runtimeName * @param {string} params.runtimeVersion * @param {object} [params.custom] * @param {string} [params.testLevel] * @param {boolean} [params.isCoverageReportUploadEnabled] * @param {string | null} params.cacheKey * @param {Function} done */ function fetchFromApi ({ url, isEvpProxy, evpProxyPrefix, isGzipCompatible, env, service, repositoryUrl, sha, osVersion, osPlatform, osArchitecture, runtimeName, runtimeVersion, custom, testLevel, isCoverageReportUploadEnabled, cacheKey, }, done) { const options = { path: '/api/v2/ci/tests/skippable', method: 'POST', headers: { 'Content-Type': 'application/json', }, timeout: 20_000, url, } if (isGzipCompatible) { options.headers['accept-encoding'] = 'gzip' } if (isEvpProxy) { options.path = `${evpProxyPrefix}/api/v2/ci/tests/skippable` options.headers['X-Datadog-EVP-Subdomain'] = 'api' } else { const { DD_API_KEY } = getConfig() if (!DD_API_KEY) { return done(new Error('Skippable suites were not fetched because Datadog API key is not defined.')) } options.headers['dd-api-key'] = DD_API_KEY } const data = JSON.stringify({ data: { type: 'test_params', attributes: { test_level: testLevel, configurations: { 'os.platform': osPlatform, 'os.version': osVersion, 'os.architecture': osArchitecture, 'runtime.name': runtimeName, 'runtime.version': runtimeVersion, custom, }, service, env, repository_url: repositoryUrl, sha, }, }, }) incrementCountMetric(TELEMETRY_ITR_SKIPPABLE_TESTS) const startTime = Date.now() request(data, options, (err, res, statusCode) => { distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_MS, {}, Date.now() - startTime) if (err) { incrementCountMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_ERRORS, { statusCode }) done(err) } else { try { const parsedResponse = parseJsonResponse(res) const result = parseSkippableSuitesResponse(parsedResponse, { testLevel, isCoverageReportUploadEnabled, validateRequiredFields: true, }) const skippableItems = parsedResponse.data.filter(({ type }) => type === testLevel) incrementCountMetric( testLevel === 'test' ? TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS : TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES, {}, skippableItems.length ) distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, {}, res.length) writeToCache(cacheKey, result) done(null, result) } catch (err) { done(err) } } }) } module.exports = { getSkippableSuites, logSkippableSuitesResponse, parseSkippableSuitesResponse }