dd-trace 6.8.0 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ci/diagnose.js +27 -25
- package/ci/init.js +6 -6
- package/ci/runbook.md +114 -20
- package/ci/test-optimization-validation/approval-artifacts.js +36 -3
- package/ci/test-optimization-validation/approval.js +78 -22
- package/ci/test-optimization-validation/blocker-category.js +24 -0
- package/ci/test-optimization-validation/ci-discovery.js +23 -17
- package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
- package/ci/test-optimization-validation/ci-remediation.js +1 -1
- package/ci/test-optimization-validation/cli.js +280 -52
- package/ci/test-optimization-validation/command-blocker.js +159 -20
- package/ci/test-optimization-validation/command-output-policy.js +9 -3
- package/ci/test-optimization-validation/command-runner.js +9 -7
- package/ci/test-optimization-validation/environment.js +4 -2
- package/ci/test-optimization-validation/executable.js +15 -13
- package/ci/test-optimization-validation/execution-lock.js +70 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
- package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
- package/ci/test-optimization-validation/generated-files.js +38 -28
- package/ci/test-optimization-validation/generated-test-contract.js +8 -4
- package/ci/test-optimization-validation/generated-verifier.js +24 -17
- package/ci/test-optimization-validation/literal-glob.js +52 -0
- package/ci/test-optimization-validation/manifest-loader.js +3 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
- package/ci/test-optimization-validation/manifest-schema.js +80 -17
- package/ci/test-optimization-validation/offline-fixtures.js +13 -4
- package/ci/test-optimization-validation/package-check.js +94 -0
- package/ci/test-optimization-validation/plan-writer.js +183 -10
- package/ci/test-optimization-validation/preflight-runner.js +110 -31
- package/ci/test-optimization-validation/project-build-artifact.js +31 -0
- package/ci/test-optimization-validation/redaction.js +18 -27
- package/ci/test-optimization-validation/report-writer.js +182 -48
- package/ci/test-optimization-validation/result-semantics.js +20 -3
- package/ci/test-optimization-validation/runner-command.js +40 -20
- package/ci/test-optimization-validation/runner-contract.js +79 -43
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
- package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
- package/ci/test-optimization-validation/static-diagnosis.js +10 -3
- package/ci/test-optimization-validation/test-output.js +2 -1
- package/ci/test-optimization-validation/validation-blocker.js +21 -0
- package/ci/vitest-no-worker-init-setup.mjs +30 -34
- package/index.d.ts +98 -1
- package/init.js +1 -17
- package/initialize.mjs +11 -0
- package/loader-hook.mjs +6 -4
- package/package.json +5 -3
- package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
- package/packages/datadog-esbuild/src/utils.js +16 -1
- package/packages/datadog-instrumentations/src/ai.js +61 -34
- package/packages/datadog-instrumentations/src/bluebird.js +6 -6
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
- package/packages/datadog-instrumentations/src/cucumber.js +124 -47
- package/packages/datadog-instrumentations/src/dns.js +21 -2
- package/packages/datadog-instrumentations/src/electron.js +1 -0
- package/packages/datadog-instrumentations/src/fastify.js +3 -1
- package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
- package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +33 -9
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +118 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
- package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
- package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +979 -233
- package/packages/datadog-instrumentations/src/mocha/main.js +107 -9
- package/packages/datadog-instrumentations/src/mocha/utils.js +64 -42
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
- package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +155 -23
- package/packages/datadog-instrumentations/src/openai.js +14 -0
- package/packages/datadog-instrumentations/src/pg.js +180 -6
- package/packages/datadog-instrumentations/src/playwright.js +173 -79
- package/packages/datadog-instrumentations/src/promise-js.js +4 -4
- package/packages/datadog-instrumentations/src/promise.js +3 -3
- package/packages/datadog-instrumentations/src/router.js +195 -19
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -10
- package/packages/datadog-instrumentations/src/vitest-main.js +360 -56
- package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
- package/packages/datadog-instrumentations/src/webdriverio.js +194 -18
- package/packages/datadog-instrumentations/src/when.js +3 -3
- package/packages/datadog-instrumentations/src/ws.js +5 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
- package/packages/datadog-plugin-cucumber/src/index.js +5 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
- package/packages/datadog-plugin-cypress/src/support.js +7 -4
- package/packages/datadog-plugin-electron/src/net.js +10 -4
- package/packages/datadog-plugin-http/src/client.js +1 -1
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +1 -18
- package/packages/datadog-plugin-jest/src/util.js +2 -0
- package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
- package/packages/datadog-plugin-mocha/src/index.js +318 -1
- package/packages/datadog-plugin-next/src/index.js +57 -9
- package/packages/datadog-plugin-openai/src/tracing.js +4 -0
- package/packages/datadog-plugin-pg/src/index.js +65 -1
- package/packages/datadog-plugin-playwright/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +95 -7
- package/packages/datadog-shimmer/src/shimmer.js +6 -2
- package/packages/dd-trace/src/aiguard/sdk.js +3 -0
- package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
- package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
- package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
- package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
- package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
- package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
- package/packages/dd-trace/src/appsec/index.js +48 -476
- package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
- package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
- package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/helper.js +2 -0
- package/packages/dd-trace/src/config/index.js +16 -5
- package/packages/dd-trace/src/config/remote_config.js +1 -1
- package/packages/dd-trace/src/config/supported-configurations.json +8 -0
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
- package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
- package/packages/dd-trace/src/guardrails/log.js +1 -10
- package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
- package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
- package/packages/dd-trace/src/llmobs/noop.js +2 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +149 -68
- package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
- package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
- package/packages/dd-trace/src/log/channels.js +1 -9
- package/packages/dd-trace/src/log/levels.js +12 -0
- package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
- package/packages/dd-trace/src/opentelemetry/span.js +4 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/ci.js +6 -0
- package/packages/dd-trace/src/plugins/util/git.js +6 -4
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
- package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
- package/packages/dd-trace/src/plugins/util/test.js +319 -75
- package/packages/dd-trace/src/plugins/util/url.js +1 -1
- package/packages/dd-trace/src/plugins/util/web.js +23 -1
- package/packages/dd-trace/src/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
- package/packages/dd-trace/src/proxy.js +1 -0
- package/packages/dd-trace/src/ritm.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
- package/packages/dd-trace/src/sampling_rule.js +4 -2
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
- package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
- package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
- package/vendor/dist/pprof-format/index.js +1 -1
- package/version.js +10 -8
- /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
|
@@ -11,6 +11,10 @@ const { getRootSpan } = require('./utils')
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @deprecated in favor of trackUserLoginSuccessV2
|
|
14
|
+
* @param {import('../../tracer')} tracer
|
|
15
|
+
* @param {import('../../../../../index').User} user
|
|
16
|
+
* @param {Record<string, string>} [metadata]
|
|
17
|
+
* @returns {void}
|
|
14
18
|
*/
|
|
15
19
|
function trackUserLoginSuccessEvent (tracer, user, metadata) {
|
|
16
20
|
// TODO: better user check here and in _setUser() ?
|
|
@@ -40,6 +44,11 @@ function trackUserLoginSuccessEvent (tracer, user, metadata) {
|
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* @deprecated in favor of trackUserLoginFailureV2
|
|
47
|
+
* @param {import('../../tracer')} tracer
|
|
48
|
+
* @param {string} userId
|
|
49
|
+
* @param {boolean} exists
|
|
50
|
+
* @param {Record<string, string>} [metadata]
|
|
51
|
+
* @returns {void}
|
|
43
52
|
*/
|
|
44
53
|
function trackUserLoginFailureEvent (tracer, userId, exists, metadata) {
|
|
45
54
|
if (!userId || typeof userId !== 'string') {
|
|
@@ -5,6 +5,8 @@ const path = require('node:path')
|
|
|
5
5
|
|
|
6
6
|
const log = require('../log')
|
|
7
7
|
|
|
8
|
+
const BIGINT_LSTAT_OPTIONS = { bigint: true, throwIfNoEntry: false }
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Coverage report file patterns to search for
|
|
10
12
|
* Each entry contains the relative path from root and the format identifier
|
|
@@ -38,7 +40,7 @@ const COVERAGE_REPORT_PATTERNS = [
|
|
|
38
40
|
/**
|
|
39
41
|
* Discovers code coverage report files in the given root directory
|
|
40
42
|
* @param {string} rootDir - The root directory to search for coverage reports
|
|
41
|
-
* @returns {Array<{filePath: string, format: string}>}
|
|
43
|
+
* @returns {Array<{filePath: string, fileDevice: bigint, fileInode: bigint, format: string}>}
|
|
42
44
|
*/
|
|
43
45
|
function discoverCoverageReports (rootDir) {
|
|
44
46
|
if (!rootDir) {
|
|
@@ -46,27 +48,57 @@ function discoverCoverageReports (rootDir) {
|
|
|
46
48
|
return []
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
let resolvedRoot
|
|
52
|
+
try {
|
|
53
|
+
const unresolvedRoot = path.resolve(rootDir)
|
|
54
|
+
const rootStats = fs.lstatSync(unresolvedRoot, BIGINT_LSTAT_OPTIONS)
|
|
55
|
+
if (rootStats?.isSymbolicLink() || !rootStats?.isDirectory()) {
|
|
56
|
+
log.debug('Coverage report root is not a regular directory: %s', unresolvedRoot)
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
resolvedRoot = fs.realpathSync(unresolvedRoot)
|
|
60
|
+
const resolvedRootStats = fs.lstatSync(resolvedRoot, BIGINT_LSTAT_OPTIONS)
|
|
61
|
+
if (!resolvedRootStats || resolvedRootStats.dev !== rootStats.dev || resolvedRootStats.ino !== rootStats.ino) {
|
|
62
|
+
log.debug('Coverage report root changed while resolving it: %s', unresolvedRoot)
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
log.debug('Error checking coverage report root %s: %s', rootDir, error.message)
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
const discoveredReports = []
|
|
50
71
|
|
|
51
72
|
for (const pattern of COVERAGE_REPORT_PATTERNS) {
|
|
52
|
-
|
|
73
|
+
let currentPath = resolvedRoot
|
|
53
74
|
|
|
54
75
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (stats.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
let stats
|
|
77
|
+
for (const pathSegment of pattern.path.split('/')) {
|
|
78
|
+
currentPath = path.join(currentPath, pathSegment)
|
|
79
|
+
stats = fs.lstatSync(currentPath, BIGINT_LSTAT_OPTIONS)
|
|
80
|
+
if (!stats || stats.isSymbolicLink()) break
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A hard link can name a file outside the root without changing its real path.
|
|
84
|
+
if (stats?.isFile() && stats.nlink === 1n) {
|
|
85
|
+
const resolvedPath = fs.realpathSync(currentPath)
|
|
86
|
+
const pathFromRoot = path.relative(resolvedRoot, resolvedPath)
|
|
87
|
+
if (pathFromRoot === '..' || pathFromRoot.startsWith(`..${path.sep}`) || path.isAbsolute(pathFromRoot)) {
|
|
88
|
+
log.debug('Coverage report resolved outside the root directory: %s', currentPath)
|
|
89
|
+
continue
|
|
65
90
|
}
|
|
91
|
+
discoveredReports.push({
|
|
92
|
+
filePath: resolvedPath,
|
|
93
|
+
fileDevice: stats.dev,
|
|
94
|
+
fileInode: stats.ino,
|
|
95
|
+
format: pattern.format,
|
|
96
|
+
})
|
|
97
|
+
log.debug('Found coverage report: %s (format: %s)', resolvedPath, pattern.format)
|
|
66
98
|
}
|
|
67
|
-
} catch (
|
|
99
|
+
} catch (error) {
|
|
68
100
|
// Log but don't fail if we can't access a file
|
|
69
|
-
log.debug('Error checking coverage report path %s: %s',
|
|
101
|
+
log.debug('Error checking coverage report path %s: %s', currentPath, error.message)
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
104
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const EARLY_FLAKE_DETECTION_RETRY_THRESHOLDS = [
|
|
4
|
+
{ limitMs: 5000, key: '5s' },
|
|
5
|
+
{ limitMs: 10_000, key: '10s' },
|
|
6
|
+
{ limitMs: 30_000, key: '30s' },
|
|
7
|
+
{ limitMs: 300_000, key: '5m' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
// A bucket the settings validator accepts but this module ignores silently loses its retries.
|
|
11
|
+
const EARLY_FLAKE_DETECTION_RETRY_BUCKETS =
|
|
12
|
+
Object.freeze(EARLY_FLAKE_DETECTION_RETRY_THRESHOLDS.map(({ key }) => key))
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} EfdDurationRetryCount
|
|
16
|
+
* @property {number} durationLimitMs
|
|
17
|
+
* @property {number} retryCount
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} EfdRetryPolicy
|
|
22
|
+
* @property {readonly EfdDurationRetryCount[]} durationRetryCounts
|
|
23
|
+
* @property {number} schedulingRetryCount
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {number} durationMs
|
|
28
|
+
* @param {EfdRetryPolicy} retryPolicy
|
|
29
|
+
* @returns {number}
|
|
30
|
+
*/
|
|
31
|
+
function getEfdRetryCountForDuration (durationMs, retryPolicy) {
|
|
32
|
+
for (const { durationLimitMs, retryCount } of retryPolicy.durationRetryCounts) {
|
|
33
|
+
if (durationMs < durationLimitMs) {
|
|
34
|
+
return retryCount
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {Record<string, unknown> | undefined} retriesByDuration
|
|
42
|
+
* @returns {EfdRetryPolicy}
|
|
43
|
+
*/
|
|
44
|
+
function createEfdRetryPolicy (retriesByDuration = {}) {
|
|
45
|
+
const durationRetryCounts = []
|
|
46
|
+
let schedulingRetryCount = 0
|
|
47
|
+
for (const { limitMs: durationLimitMs, key } of EARLY_FLAKE_DETECTION_RETRY_THRESHOLDS) {
|
|
48
|
+
const configuredRetryCount = retriesByDuration[key]
|
|
49
|
+
const retryCount = Number.isSafeInteger(configuredRetryCount) && configuredRetryCount >= 0
|
|
50
|
+
? configuredRetryCount
|
|
51
|
+
: 0
|
|
52
|
+
durationRetryCounts.push(Object.freeze({ durationLimitMs, retryCount }))
|
|
53
|
+
if (retryCount > schedulingRetryCount) {
|
|
54
|
+
schedulingRetryCount = retryCount
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
durationRetryCounts: Object.freeze(durationRetryCounts),
|
|
59
|
+
schedulingRetryCount,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {EfdRetryPolicy | undefined} retryPolicy
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
function hasEfdRetries (retryPolicy) {
|
|
68
|
+
return (retryPolicy?.schedulingRetryCount ?? 0) > 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {number} retryIndex
|
|
73
|
+
* @param {number | undefined} retryCount
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
function shouldSkipEfdRetry (retryIndex, retryCount) {
|
|
77
|
+
return retryCount !== undefined && retryIndex > retryCount
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const EMPTY_EFD_RETRY_POLICY = createEfdRetryPolicy()
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
EARLY_FLAKE_DETECTION_RETRY_BUCKETS,
|
|
84
|
+
EMPTY_EFD_RETRY_POLICY,
|
|
85
|
+
createEfdRetryPolicy,
|
|
86
|
+
getEfdRetryCountForDuration,
|
|
87
|
+
hasEfdRetries,
|
|
88
|
+
shouldSkipEfdRetry,
|
|
89
|
+
}
|
|
@@ -4,6 +4,7 @@ const { hostname: getHostname } = require('node:os')
|
|
|
4
4
|
const URL = require('url').URL
|
|
5
5
|
|
|
6
6
|
const { version: tracerVersion } = require('../../../../../package.json')
|
|
7
|
+
const { EMPTY_EFD_RETRY_POLICY, createEfdRetryPolicy } = require('../efd-retry-policy')
|
|
7
8
|
const { getLibraryConfiguration: getLibraryConfigurationRequest } = require('../requests/get-library-configuration')
|
|
8
9
|
const { getSkippableSuites: getSkippableSuitesRequest } = require('../intelligent-test-runner/get-skippable-suites')
|
|
9
10
|
const { getKnownTests: getKnownTestsRequest } = require('../early-flake-detection/get-known-tests')
|
|
@@ -23,6 +24,18 @@ const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata'
|
|
|
23
24
|
const hostname = getHostname()
|
|
24
25
|
const EMPTY_SETTINGS = Object.freeze({})
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Test session identity sent with every request. Fields are optional because the CI provider,
|
|
29
|
+
* the git repository or the runtime may not expose them.
|
|
30
|
+
*
|
|
31
|
+
* @typedef {{
|
|
32
|
+
* repositoryUrl?: string, sha?: string, branch?: string, tag?: string, testLevel?: string,
|
|
33
|
+
* osVersion?: string, osPlatform?: string, osArchitecture?: string,
|
|
34
|
+
* runtimeName?: string, runtimeVersion?: string, commitMessage?: string,
|
|
35
|
+
* pullRequestBaseSha?: string, commitHeadSha?: string, commitHeadMessage?: string,
|
|
36
|
+
* }} TestConfiguration
|
|
37
|
+
*/
|
|
38
|
+
|
|
26
39
|
function getTestConfigurationTags (tags) {
|
|
27
40
|
if (!tags) {
|
|
28
41
|
return {}
|
|
@@ -190,6 +203,7 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
190
203
|
const cachedSkippableSuites = this._testOptimizationHttpCache.readSkippableSuites({
|
|
191
204
|
testLevel: requestConfiguration.testLevel,
|
|
192
205
|
isCoverageReportUploadEnabled: requestConfiguration.isCoverageReportUploadEnabled,
|
|
206
|
+
isLineCoverageSupported: requestConfiguration.isLineCoverageSupported,
|
|
193
207
|
})
|
|
194
208
|
if (cachedSkippableSuites !== CACHE_MISS) {
|
|
195
209
|
const { skippableSuites, correlationId, coverage } = cachedSkippableSuites
|
|
@@ -238,6 +252,10 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
238
252
|
/**
|
|
239
253
|
* We can't request library configuration until we know whether we can use the
|
|
240
254
|
* CI Visibility Protocol, hence the this._canUseCiVisProtocol promise.
|
|
255
|
+
*
|
|
256
|
+
* @param {TestConfiguration} testConfiguration
|
|
257
|
+
* @param {(error: Error | null, libraryConfig?: Readonly<Record<string, unknown>>) => void} callback
|
|
258
|
+
* @returns {void}
|
|
241
259
|
*/
|
|
242
260
|
getLibraryConfiguration (testConfiguration, callback) {
|
|
243
261
|
const { repositoryUrl } = testConfiguration
|
|
@@ -321,21 +339,14 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
321
339
|
DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: configuredAttemptToFixRetries = 0,
|
|
322
340
|
DD_TEST_MANAGEMENT_ENABLED: isTestManagementAllowed,
|
|
323
341
|
} = testOptimization
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
? Object.freeze({
|
|
342
|
+
const earlyFlakeDetectionRetryPolicy = earlyFlakeDetectionRetryCount === undefined
|
|
343
|
+
? remoteConfiguration.earlyFlakeDetectionRetryPolicy ?? EMPTY_EFD_RETRY_POLICY
|
|
344
|
+
: createEfdRetryPolicy({
|
|
328
345
|
'5s': earlyFlakeDetectionRetryCount,
|
|
329
346
|
'10s': earlyFlakeDetectionRetryCount,
|
|
330
347
|
'30s': earlyFlakeDetectionRetryCount,
|
|
331
348
|
'5m': earlyFlakeDetectionRetryCount,
|
|
332
349
|
})
|
|
333
|
-
: Object.isFrozen(configuredSlowTestRetries)
|
|
334
|
-
? configuredSlowTestRetries
|
|
335
|
-
: Object.freeze({ ...configuredSlowTestRetries })
|
|
336
|
-
const earlyFlakeDetectionNumRetries = hasEarlyFlakeDetectionRetryCount
|
|
337
|
-
? earlyFlakeDetectionRetryCount
|
|
338
|
-
: remoteConfiguration.earlyFlakeDetectionNumRetries ?? 0
|
|
339
350
|
const testManagementAttemptToFixRetries =
|
|
340
351
|
remoteConfiguration.testManagementAttemptToFixRetries ?? configuredAttemptToFixRetries
|
|
341
352
|
|
|
@@ -346,8 +357,7 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
346
357
|
requireGit: remoteConfiguration.requireGit === true,
|
|
347
358
|
isEarlyFlakeDetectionEnabled:
|
|
348
359
|
remoteConfiguration.isEarlyFlakeDetectionEnabled === true && isEarlyFlakeDetectionAllowed === true,
|
|
349
|
-
|
|
350
|
-
earlyFlakeDetectionSlowTestRetries,
|
|
360
|
+
earlyFlakeDetectionRetryPolicy,
|
|
351
361
|
earlyFlakeDetectionFaultyThreshold: remoteConfiguration.earlyFlakeDetectionFaultyThreshold ?? 30,
|
|
352
362
|
isFlakyTestRetriesEnabled:
|
|
353
363
|
remoteConfiguration.isFlakyTestRetriesEnabled === true && isFlakyTestRetriesAllowed === true,
|
|
@@ -521,17 +531,21 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
521
531
|
* Uploads a single coverage report to the CI intake.
|
|
522
532
|
* @param {object} options - Upload options
|
|
523
533
|
* @param {string} options.filePath - Path to the coverage report file
|
|
534
|
+
* @param {bigint} options.fileDevice - Device containing the discovered report
|
|
535
|
+
* @param {bigint} options.fileInode - Inode of the discovered report
|
|
524
536
|
* @param {string} options.format - Format of the coverage report
|
|
525
537
|
* @param {object} options.testEnvironmentMetadata - Test environment metadata containing git/CI tags
|
|
526
538
|
* @param {(error: Error|null) => void} callback - Callback function
|
|
527
539
|
*/
|
|
528
|
-
uploadCoverageReport ({ filePath, format, testEnvironmentMetadata }, callback) {
|
|
540
|
+
uploadCoverageReport ({ filePath, fileDevice, fileInode, format, testEnvironmentMetadata }, callback) {
|
|
529
541
|
if (!this._codeCoverageReportUrl) {
|
|
530
542
|
return callback(new Error('Coverage report upload URL not configured'))
|
|
531
543
|
}
|
|
532
544
|
|
|
533
545
|
uploadCoverageReportRequest({
|
|
534
546
|
filePath,
|
|
547
|
+
fileDevice,
|
|
548
|
+
fileInode,
|
|
535
549
|
format,
|
|
536
550
|
flags: this._coverageReportFlags,
|
|
537
551
|
testEnvironmentMetadata,
|
|
@@ -32,6 +32,9 @@ const {
|
|
|
32
32
|
TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_BYTES,
|
|
33
33
|
} = require('../../../ci-visibility/telemetry')
|
|
34
34
|
|
|
35
|
+
/** @typedef {{ isEvpProxy: boolean, evpProxyPrefix: string }} EvpProxyConfiguration */
|
|
36
|
+
/** @typedef {EvpProxyConfiguration & { url: URL, repositoryUrl: string }} GitUploadTarget */
|
|
37
|
+
|
|
35
38
|
const isValidSha1 = (sha) => /^[0-9a-f]{40}$/.test(sha)
|
|
36
39
|
const isValidSha256 = (sha) => /^[0-9a-f]{64}$/.test(sha)
|
|
37
40
|
|
|
@@ -62,6 +65,10 @@ function getCommonRequestOptions (url) {
|
|
|
62
65
|
* This function posts the SHAs of the commits of the last month
|
|
63
66
|
* The response are the commits for which the backend already has information
|
|
64
67
|
* This response is used to know which commits can be ignored from there on
|
|
68
|
+
*
|
|
69
|
+
* @param {GitUploadTarget & { latestCommits: string[] }} options
|
|
70
|
+
* @param {(error: Error | null, commitsToUpload?: string[]) => void} callback
|
|
71
|
+
* @returns {void}
|
|
65
72
|
*/
|
|
66
73
|
function getCommitsToUpload ({ url, repositoryUrl, latestCommits, isEvpProxy, evpProxyPrefix }, callback) {
|
|
67
74
|
const commonOptions = getCommonRequestOptions(url)
|
|
@@ -127,6 +134,10 @@ function getCommitsToUpload ({ url, repositoryUrl, latestCommits, isEvpProxy, ev
|
|
|
127
134
|
|
|
128
135
|
/**
|
|
129
136
|
* This function uploads a git packfile
|
|
137
|
+
*
|
|
138
|
+
* @param {GitUploadTarget & { packFileToUpload: string, headCommit: string }} options
|
|
139
|
+
* @param {(error: Error | null, uploadSize?: number) => void} callback
|
|
140
|
+
* @returns {void}
|
|
130
141
|
*/
|
|
131
142
|
function uploadPackFile ({ url, isEvpProxy, evpProxyPrefix, packFileToUpload, repositoryUrl, headCommit }, callback) {
|
|
132
143
|
const form = new FormData()
|
|
@@ -245,6 +256,12 @@ function generateAndUploadPackFiles ({
|
|
|
245
256
|
|
|
246
257
|
/**
|
|
247
258
|
* This function uploads git metadata to CI Visibility's backend.
|
|
259
|
+
*
|
|
260
|
+
* @param {URL} url
|
|
261
|
+
* @param {EvpProxyConfiguration} evpProxyConfiguration
|
|
262
|
+
* @param {string | undefined} configRepositoryUrl repository URL from the configuration, if set
|
|
263
|
+
* @param {(error?: Error | null) => void} callback
|
|
264
|
+
* @returns {void}
|
|
248
265
|
*/
|
|
249
266
|
function sendGitMetadata (url, { isEvpProxy, evpProxyPrefix }, configRepositoryUrl, callback) {
|
|
250
267
|
if (!isGitAvailable()) {
|
|
@@ -5,12 +5,17 @@ const {
|
|
|
5
5
|
JEST_WORKER_TRACE_PAYLOAD_CODE,
|
|
6
6
|
JEST_WORKER_TELEMETRY_PAYLOAD_CODE,
|
|
7
7
|
CUCUMBER_WORKER_TRACE_PAYLOAD_CODE,
|
|
8
|
+
CUCUMBER_WORKER_TELEMETRY_PAYLOAD_CODE,
|
|
8
9
|
MOCHA_WORKER_LOGS_PAYLOAD_CODE,
|
|
10
|
+
MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE,
|
|
9
11
|
MOCHA_WORKER_TRACE_PAYLOAD_CODE,
|
|
10
12
|
JEST_WORKER_LOGS_PAYLOAD_CODE,
|
|
13
|
+
PLAYWRIGHT_WORKER_TELEMETRY_PAYLOAD_CODE,
|
|
11
14
|
PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE,
|
|
12
15
|
VITEST_WORKER_TRACE_PAYLOAD_CODE,
|
|
16
|
+
VITEST_WORKER_COVERAGE_PAYLOAD_CODE,
|
|
13
17
|
VITEST_WORKER_LOGS_PAYLOAD_CODE,
|
|
18
|
+
VITEST_WORKER_TELEMETRY_PAYLOAD_CODE,
|
|
14
19
|
} = require('../../../plugins/util/test')
|
|
15
20
|
const getConfig = require('../../../config')
|
|
16
21
|
const { getEnvironmentVariable } = require('../../../config/helper')
|
|
@@ -44,6 +49,9 @@ function getInterprocessCoverageCode () {
|
|
|
44
49
|
if (getEnvironmentVariable('JEST_WORKER_ID')) {
|
|
45
50
|
return JEST_WORKER_COVERAGE_PAYLOAD_CODE
|
|
46
51
|
}
|
|
52
|
+
if (getEnvironmentVariable('TINYPOOL_WORKER_ID') || getConfig().DD_VITEST_WORKER) {
|
|
53
|
+
return VITEST_WORKER_COVERAGE_PAYLOAD_CODE
|
|
54
|
+
}
|
|
47
55
|
return null
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -67,6 +75,19 @@ function getInterprocessTelemetryCode () {
|
|
|
67
75
|
if (getEnvironmentVariable('JEST_WORKER_ID')) {
|
|
68
76
|
return JEST_WORKER_TELEMETRY_PAYLOAD_CODE
|
|
69
77
|
}
|
|
78
|
+
if (getEnvironmentVariable('CUCUMBER_WORKER_ID')) {
|
|
79
|
+
return CUCUMBER_WORKER_TELEMETRY_PAYLOAD_CODE
|
|
80
|
+
}
|
|
81
|
+
if (getEnvironmentVariable('MOCHA_WORKER_ID')) {
|
|
82
|
+
return MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE
|
|
83
|
+
}
|
|
84
|
+
const { DD_PLAYWRIGHT_WORKER, DD_VITEST_WORKER } = getConfig()
|
|
85
|
+
if (DD_PLAYWRIGHT_WORKER) {
|
|
86
|
+
return PLAYWRIGHT_WORKER_TELEMETRY_PAYLOAD_CODE
|
|
87
|
+
}
|
|
88
|
+
if (getEnvironmentVariable('TINYPOOL_WORKER_ID') || DD_VITEST_WORKER) {
|
|
89
|
+
return VITEST_WORKER_TELEMETRY_PAYLOAD_CODE
|
|
90
|
+
}
|
|
70
91
|
return null
|
|
71
92
|
}
|
|
72
93
|
|
|
@@ -85,7 +106,6 @@ class TestWorkerCiVisibilityExporter {
|
|
|
85
106
|
this._writer = new Writer(interprocessTraceCode)
|
|
86
107
|
this._coverageWriter = new Writer(interprocessCoverageCode)
|
|
87
108
|
this._logsWriter = new Writer(interprocessLogsCode)
|
|
88
|
-
// TODO: add support for test workers other than Jest
|
|
89
109
|
if (interprocessTelemetryCode) {
|
|
90
110
|
this._telemetryWriter = new Writer(interprocessTelemetryCode)
|
|
91
111
|
this.exportTelemetry = function (telemetryEvent) {
|
|
@@ -25,6 +25,7 @@ function parseSkippableSuitesResponse (
|
|
|
25
25
|
{
|
|
26
26
|
testLevel = 'suite',
|
|
27
27
|
isCoverageReportUploadEnabled = false,
|
|
28
|
+
isLineCoverageSupported = true,
|
|
28
29
|
validateRequiredFields = false,
|
|
29
30
|
validationMode = false,
|
|
30
31
|
} = {}
|
|
@@ -34,8 +35,11 @@ function parseSkippableSuitesResponse (
|
|
|
34
35
|
validateSkippableTestsResponse(parsedResponse, { validationMode })
|
|
35
36
|
}
|
|
36
37
|
const coverage = {}
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const coverageByFilename = parsedResponse.meta?.coverage
|
|
39
|
+
if (coverageByFilename) {
|
|
40
|
+
for (const [filename, bitmap] of Object.entries(coverageByFilename)) {
|
|
41
|
+
coverage[filename.replaceAll('\\', '/')] = bitmap
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
const skippableItems = parsedResponse
|
|
@@ -51,7 +55,7 @@ function parseSkippableSuitesResponse (
|
|
|
51
55
|
},
|
|
52
56
|
} of skippableItems) {
|
|
53
57
|
// Only reject candidates without backend line coverage when we need that coverage to backfill reports.
|
|
54
|
-
if (isCoverageReportUploadEnabled && isMissingLineCodeCoverage) {
|
|
58
|
+
if (isCoverageReportUploadEnabled && isLineCoverageSupported && isMissingLineCodeCoverage) {
|
|
55
59
|
numExcludedByMissingLineCoverage++
|
|
56
60
|
continue
|
|
57
61
|
}
|
|
@@ -78,10 +82,11 @@ function parseSkippableSuitesResponse (
|
|
|
78
82
|
* numExcludedByMissingLineCoverage?: number
|
|
79
83
|
* }} result - Parsed skippable response.
|
|
80
84
|
* @param {'suite'|'test'} testLevel - Test optimization skipping level.
|
|
85
|
+
* @param {boolean} shouldConsiderLineCoverage - Whether line coverage can affect suite skipping.
|
|
81
86
|
* @returns {void}
|
|
82
87
|
*/
|
|
83
|
-
function logSkippableSuitesResponse (result, testLevel) {
|
|
84
|
-
if (result.numReceivedSkippableItems === undefined) {
|
|
88
|
+
function logSkippableSuitesResponse (result, testLevel, shouldConsiderLineCoverage) {
|
|
89
|
+
if (!shouldConsiderLineCoverage || result.numReceivedSkippableItems === undefined) {
|
|
85
90
|
log.debug('Number of received skippable %ss: %d', testLevel, result.skippableSuites.length)
|
|
86
91
|
return
|
|
87
92
|
}
|
|
@@ -122,10 +127,12 @@ function getSkippableSuites ({
|
|
|
122
127
|
custom,
|
|
123
128
|
testLevel = 'suite',
|
|
124
129
|
isCoverageReportUploadEnabled = false,
|
|
130
|
+
isLineCoverageSupported = true,
|
|
125
131
|
}, done) {
|
|
132
|
+
const shouldConsiderLineCoverage = isCoverageReportUploadEnabled && isLineCoverageSupported
|
|
126
133
|
const cacheKey = buildCacheKey('skippable', [
|
|
127
134
|
sha, service, env, repositoryUrl, osPlatform, osVersion, osArchitecture,
|
|
128
|
-
runtimeName, runtimeVersion, testLevel, custom,
|
|
135
|
+
runtimeName, runtimeVersion, testLevel, custom, shouldConsiderLineCoverage,
|
|
129
136
|
])
|
|
130
137
|
|
|
131
138
|
withCache(cacheKey, (activeCacheKey, cb) => {
|
|
@@ -146,11 +153,16 @@ function getSkippableSuites ({
|
|
|
146
153
|
custom,
|
|
147
154
|
testLevel,
|
|
148
155
|
isCoverageReportUploadEnabled,
|
|
156
|
+
isLineCoverageSupported,
|
|
149
157
|
cacheKey: activeCacheKey,
|
|
150
158
|
}, cb)
|
|
151
159
|
}, (err, data) => {
|
|
152
160
|
if (err) return done(err)
|
|
153
|
-
logSkippableSuitesResponse(
|
|
161
|
+
logSkippableSuitesResponse(
|
|
162
|
+
data,
|
|
163
|
+
testLevel,
|
|
164
|
+
shouldConsiderLineCoverage
|
|
165
|
+
)
|
|
154
166
|
done(null, data.skippableSuites, data.correlationId, data.coverage)
|
|
155
167
|
})
|
|
156
168
|
}
|
|
@@ -175,6 +187,7 @@ function getSkippableSuites ({
|
|
|
175
187
|
* @param {object} [params.custom]
|
|
176
188
|
* @param {string} [params.testLevel]
|
|
177
189
|
* @param {boolean} [params.isCoverageReportUploadEnabled]
|
|
190
|
+
* @param {boolean} [params.isLineCoverageSupported]
|
|
178
191
|
* @param {string | null} params.cacheKey
|
|
179
192
|
* @param {Function} done
|
|
180
193
|
*/
|
|
@@ -195,6 +208,7 @@ function fetchFromApi ({
|
|
|
195
208
|
custom,
|
|
196
209
|
testLevel,
|
|
197
210
|
isCoverageReportUploadEnabled,
|
|
211
|
+
isLineCoverageSupported,
|
|
198
212
|
cacheKey,
|
|
199
213
|
}, done) {
|
|
200
214
|
const options = {
|
|
@@ -258,6 +272,7 @@ function fetchFromApi ({
|
|
|
258
272
|
const result = parseSkippableSuitesResponse(parsedResponse, {
|
|
259
273
|
testLevel,
|
|
260
274
|
isCoverageReportUploadEnabled,
|
|
275
|
+
isLineCoverageSupported,
|
|
261
276
|
validateRequiredFields: true,
|
|
262
277
|
})
|
|
263
278
|
const skippableItems = parsedResponse.data.filter(({ type }) => type === testLevel)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const getConfig = require('../../config')
|
|
4
4
|
const id = require('../../id')
|
|
5
5
|
const log = require('../../log')
|
|
6
|
+
const { EARLY_FLAKE_DETECTION_RETRY_BUCKETS, createEfdRetryPolicy } = require('../efd-retry-policy')
|
|
6
7
|
const {
|
|
7
8
|
incrementCountMetric,
|
|
8
9
|
distributionMetric,
|
|
@@ -15,21 +16,20 @@ const { writeSettingsToCache } = require('../test-optimization-cache')
|
|
|
15
16
|
const { MAX_RETRIES, validateSettingsResponse } = require('../test-optimization-http-cache-schema')
|
|
16
17
|
const request = require('./request')
|
|
17
18
|
|
|
18
|
-
const DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES = 2
|
|
19
19
|
const DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES = Object.freeze({
|
|
20
20
|
'5s': 10,
|
|
21
21
|
'10s': 5,
|
|
22
22
|
'30s': 3,
|
|
23
23
|
'5m': 2,
|
|
24
24
|
})
|
|
25
|
+
const DEFAULT_EARLY_FLAKE_DETECTION_RETRY_POLICY =
|
|
26
|
+
createEfdRetryPolicy(DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES)
|
|
25
27
|
const DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD = 30
|
|
26
|
-
const EARLY_FLAKE_DETECTION_RETRY_BUCKETS = Object.keys(DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES)
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* @typedef {object} EarlyFlakeDetectionSettings
|
|
30
31
|
* @property {boolean} enabled
|
|
31
|
-
* @property {
|
|
32
|
-
* @property {Readonly<Record<string, number>>} slowTestRetries
|
|
32
|
+
* @property {import('../efd-retry-policy').EfdRetryPolicy} retryPolicy
|
|
33
33
|
* @property {number} faultyThreshold
|
|
34
34
|
*/
|
|
35
35
|
|
|
@@ -90,20 +90,17 @@ function parseEarlyFlakeDetectionSettings (value, isKnownTestsEnabled) {
|
|
|
90
90
|
if (!isRecord(value) || value.enabled !== true) {
|
|
91
91
|
return {
|
|
92
92
|
enabled: false,
|
|
93
|
-
|
|
94
|
-
slowTestRetries: DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES,
|
|
93
|
+
retryPolicy: DEFAULT_EARLY_FLAKE_DETECTION_RETRY_POLICY,
|
|
95
94
|
faultyThreshold: DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD,
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
let isValid = true
|
|
100
|
-
let
|
|
101
|
-
let slowTestRetries = DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES
|
|
99
|
+
let retryPolicy = DEFAULT_EARLY_FLAKE_DETECTION_RETRY_POLICY
|
|
102
100
|
if (Object.hasOwn(value, 'slow_test_retries')) {
|
|
103
101
|
const parsedSlowTestRetries = parseSlowTestRetries(value.slow_test_retries)
|
|
104
102
|
if (parsedSlowTestRetries) {
|
|
105
|
-
|
|
106
|
-
numRetries = parsedSlowTestRetries['5s'] ?? DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES
|
|
103
|
+
retryPolicy = createEfdRetryPolicy(parsedSlowTestRetries)
|
|
107
104
|
} else {
|
|
108
105
|
isValid = false
|
|
109
106
|
}
|
|
@@ -120,8 +117,7 @@ function parseEarlyFlakeDetectionSettings (value, isKnownTestsEnabled) {
|
|
|
120
117
|
|
|
121
118
|
return {
|
|
122
119
|
enabled: isKnownTestsEnabled && isValid,
|
|
123
|
-
|
|
124
|
-
slowTestRetries,
|
|
120
|
+
retryPolicy,
|
|
125
121
|
faultyThreshold,
|
|
126
122
|
}
|
|
127
123
|
}
|
|
@@ -180,8 +176,7 @@ function parseLibraryConfigurationResponse (rawJson, config = getConfig(), optio
|
|
|
180
176
|
isItrEnabled: attributes.itr_enabled === true,
|
|
181
177
|
requireGit: attributes.require_git === true,
|
|
182
178
|
isEarlyFlakeDetectionEnabled: earlyFlakeDetection.enabled,
|
|
183
|
-
|
|
184
|
-
earlyFlakeDetectionSlowTestRetries: earlyFlakeDetection.slowTestRetries,
|
|
179
|
+
earlyFlakeDetectionRetryPolicy: earlyFlakeDetection.retryPolicy,
|
|
185
180
|
earlyFlakeDetectionFaultyThreshold: earlyFlakeDetection.faultyThreshold,
|
|
186
181
|
isFlakyTestRetriesEnabled,
|
|
187
182
|
isDiEnabled: attributes.di_enabled === true && isFlakyTestRetriesEnabled,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { readFileSync } = require('node:fs')
|
|
3
|
+
const { closeSync, constants, fstatSync, openSync, readFileSync } = require('node:fs')
|
|
4
4
|
const { gzipSync } = require('node:zlib')
|
|
5
5
|
|
|
6
6
|
const getConfig = require('../../config')
|
|
@@ -17,12 +17,18 @@ const {
|
|
|
17
17
|
} = require('../telemetry')
|
|
18
18
|
|
|
19
19
|
const UPLOAD_TIMEOUT_MS = 30_000
|
|
20
|
+
const COVERAGE_FILE_OPEN_FLAGS = constants.O_RDONLY |
|
|
21
|
+
(constants.O_NOFOLLOW || 0) |
|
|
22
|
+
constants.O_NONBLOCK
|
|
23
|
+
const BIGINT_STAT_OPTIONS = { bigint: true }
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* Uploads a single coverage report to the Datadog CI intake.
|
|
23
27
|
* One file per request with field names 'coverage' and 'event'.
|
|
24
28
|
* @param {object} options - Upload options
|
|
25
29
|
* @param {string} options.filePath - Path to the coverage report file
|
|
30
|
+
* @param {bigint} options.fileDevice - Device containing the discovered report
|
|
31
|
+
* @param {bigint} options.fileInode - Inode of the discovered report
|
|
26
32
|
* @param {string} options.format - Format of the coverage report (e.g., 'lcov', 'cobertura')
|
|
27
33
|
* @param {string[]} [options.flags] - Optional coverage report grouping flags
|
|
28
34
|
* @param {object} options.testEnvironmentMetadata - Test environment metadata containing git/CI tags
|
|
@@ -32,7 +38,7 @@ const UPLOAD_TIMEOUT_MS = 30_000
|
|
|
32
38
|
* @param {(error: Error|null) => void} callback - Callback function
|
|
33
39
|
*/
|
|
34
40
|
function uploadCoverageReport (
|
|
35
|
-
{ filePath, format, flags, testEnvironmentMetadata, url, isEvpProxy, evpProxyPrefix },
|
|
41
|
+
{ filePath, fileDevice, fileInode, format, flags, testEnvironmentMetadata, url, isEvpProxy, evpProxyPrefix },
|
|
36
42
|
callback
|
|
37
43
|
) {
|
|
38
44
|
const { DD_API_KEY } = getConfig()
|
|
@@ -43,10 +49,20 @@ function uploadCoverageReport (
|
|
|
43
49
|
|
|
44
50
|
let compressedCoverage
|
|
45
51
|
try {
|
|
46
|
-
const
|
|
52
|
+
const fileDescriptor = openSync(filePath, COVERAGE_FILE_OPEN_FLAGS)
|
|
53
|
+
let coverageContent
|
|
54
|
+
try {
|
|
55
|
+
const fileStats = fstatSync(fileDescriptor, BIGINT_STAT_OPTIONS)
|
|
56
|
+
if (!fileStats.isFile() || fileStats.dev !== fileDevice || fileStats.ino !== fileInode) {
|
|
57
|
+
throw new Error('Coverage report changed after discovery')
|
|
58
|
+
}
|
|
59
|
+
coverageContent = readFileSync(fileDescriptor)
|
|
60
|
+
} finally {
|
|
61
|
+
closeSync(fileDescriptor)
|
|
62
|
+
}
|
|
47
63
|
compressedCoverage = gzipSync(coverageContent)
|
|
48
|
-
} catch (
|
|
49
|
-
return callback(new Error(`Failed to read coverage report at ${filePath}: ${
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return callback(new Error(`Failed to read coverage report at ${filePath}: ${error.message}`))
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
// Build the event payload with format, type, and all tags from test environment metadata
|
|
@@ -137,7 +137,7 @@ function validateSkippableTestsResponse (response, options = {}) {
|
|
|
137
137
|
if (response.meta !== undefined) {
|
|
138
138
|
assertObject(response.meta, 'Invalid skippable tests response: meta must be an object')
|
|
139
139
|
}
|
|
140
|
-
if (response.meta?.coverage !== undefined) {
|
|
140
|
+
if (response.meta?.coverage !== undefined && response.meta.coverage !== null) {
|
|
141
141
|
assertObject(response.meta.coverage, 'Invalid skippable tests response: meta.coverage must be an object')
|
|
142
142
|
}
|
|
143
143
|
if (response.meta?.correlation_id !== undefined && typeof response.meta.correlation_id !== 'string') {
|
|
@@ -145,7 +145,11 @@ class TestOptimizationHttpCache {
|
|
|
145
145
|
validationMode: Boolean(this._validationManifestPath),
|
|
146
146
|
})
|
|
147
147
|
const testLevel = options.testLevel || 'suite'
|
|
148
|
-
logSkippableSuitesResponse(
|
|
148
|
+
logSkippableSuitesResponse(
|
|
149
|
+
result,
|
|
150
|
+
testLevel,
|
|
151
|
+
options.isCoverageReportUploadEnabled && options.isLineCoverageSupported !== false
|
|
152
|
+
)
|
|
149
153
|
const skippableItems = parsedResponse.data.filter(({ type }) => type === testLevel)
|
|
150
154
|
incrementCountMetric(
|
|
151
155
|
testLevel === 'test'
|