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.
Files changed (179) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +9 -3
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  20. package/ci/test-optimization-validation/generated-files.js +38 -28
  21. package/ci/test-optimization-validation/generated-test-contract.js +8 -4
  22. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  23. package/ci/test-optimization-validation/literal-glob.js +52 -0
  24. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  25. package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
  26. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  27. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  28. package/ci/test-optimization-validation/package-check.js +94 -0
  29. package/ci/test-optimization-validation/plan-writer.js +183 -10
  30. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  31. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  32. package/ci/test-optimization-validation/redaction.js +18 -27
  33. package/ci/test-optimization-validation/report-writer.js +182 -48
  34. package/ci/test-optimization-validation/result-semantics.js +20 -3
  35. package/ci/test-optimization-validation/runner-command.js +40 -20
  36. package/ci/test-optimization-validation/runner-contract.js +79 -43
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  39. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  40. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  41. package/ci/test-optimization-validation/test-output.js +2 -1
  42. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  43. package/ci/vitest-no-worker-init-setup.mjs +30 -34
  44. package/index.d.ts +98 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +5 -3
  49. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  50. package/packages/datadog-esbuild/src/utils.js +16 -1
  51. package/packages/datadog-instrumentations/src/ai.js +61 -34
  52. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  53. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
  54. package/packages/datadog-instrumentations/src/cucumber.js +124 -47
  55. package/packages/datadog-instrumentations/src/dns.js +21 -2
  56. package/packages/datadog-instrumentations/src/electron.js +1 -0
  57. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  58. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  59. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  60. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  61. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +33 -9
  62. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +118 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
  66. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  67. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  68. package/packages/datadog-instrumentations/src/jest.js +979 -233
  69. package/packages/datadog-instrumentations/src/mocha/main.js +107 -9
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +64 -42
  71. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  72. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  73. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  74. package/packages/datadog-instrumentations/src/next.js +155 -23
  75. package/packages/datadog-instrumentations/src/openai.js +14 -0
  76. package/packages/datadog-instrumentations/src/pg.js +180 -6
  77. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  78. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  79. package/packages/datadog-instrumentations/src/promise.js +3 -3
  80. package/packages/datadog-instrumentations/src/router.js +195 -19
  81. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -10
  82. package/packages/datadog-instrumentations/src/vitest-main.js +360 -56
  83. package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
  84. package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
  85. package/packages/datadog-instrumentations/src/webdriverio.js +194 -18
  86. package/packages/datadog-instrumentations/src/when.js +3 -3
  87. package/packages/datadog-instrumentations/src/ws.js +5 -1
  88. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  89. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  90. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  91. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
  92. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  93. package/packages/datadog-plugin-electron/src/net.js +10 -4
  94. package/packages/datadog-plugin-http/src/client.js +1 -1
  95. package/packages/datadog-plugin-http2/src/client.js +1 -1
  96. package/packages/datadog-plugin-jest/src/index.js +1 -18
  97. package/packages/datadog-plugin-jest/src/util.js +2 -0
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
  99. package/packages/datadog-plugin-mocha/src/index.js +318 -1
  100. package/packages/datadog-plugin-next/src/index.js +57 -9
  101. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  102. package/packages/datadog-plugin-pg/src/index.js +65 -1
  103. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  104. package/packages/datadog-plugin-vitest/src/index.js +95 -7
  105. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  106. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  107. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  108. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  109. package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
  110. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  111. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  112. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  113. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  114. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  115. package/packages/dd-trace/src/appsec/index.js +48 -476
  116. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  117. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  118. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  120. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  121. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  122. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  123. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  124. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  127. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  128. package/packages/dd-trace/src/config/helper.js +2 -0
  129. package/packages/dd-trace/src/config/index.js +16 -5
  130. package/packages/dd-trace/src/config/remote_config.js +1 -1
  131. package/packages/dd-trace/src/config/supported-configurations.json +8 -0
  132. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  133. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  134. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  135. package/packages/dd-trace/src/guardrails/log.js +1 -10
  136. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  137. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  138. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  139. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  140. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  141. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  142. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  143. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  144. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  145. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  146. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  147. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  148. package/packages/dd-trace/src/log/channels.js +1 -9
  149. package/packages/dd-trace/src/log/levels.js +12 -0
  150. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  151. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  152. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  153. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  154. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  155. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  156. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  157. package/packages/dd-trace/src/plugins/index.js +1 -0
  158. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  159. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  160. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  161. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  162. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  163. package/packages/dd-trace/src/plugins/util/test.js +319 -75
  164. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  165. package/packages/dd-trace/src/plugins/util/web.js +23 -1
  166. package/packages/dd-trace/src/profiler.js +1 -1
  167. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  168. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  169. package/packages/dd-trace/src/proxy.js +1 -0
  170. package/packages/dd-trace/src/ritm.js +5 -0
  171. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  172. package/packages/dd-trace/src/sampling_rule.js +4 -2
  173. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  174. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  175. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  176. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  177. package/vendor/dist/pprof-format/index.js +1 -1
  178. package/version.js +10 -8
  179. /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}>} Array of discovered coverage reports
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
- const fullPath = path.join(rootDir, pattern.path)
73
+ let currentPath = resolvedRoot
53
74
 
54
75
  try {
55
- if (fs.existsSync(fullPath)) {
56
- const stats = fs.statSync(fullPath)
57
-
58
- // Only include regular files, not directories or symlinks
59
- if (stats.isFile()) {
60
- discoveredReports.push({
61
- filePath: fullPath,
62
- format: pattern.format,
63
- })
64
- log.debug('Found coverage report: %s (format: %s)', fullPath, pattern.format)
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 (err) {
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', fullPath, err.message)
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 hasEarlyFlakeDetectionRetryCount = earlyFlakeDetectionRetryCount !== undefined
325
- const configuredSlowTestRetries = remoteConfiguration.earlyFlakeDetectionSlowTestRetries ?? EMPTY_SETTINGS
326
- const earlyFlakeDetectionSlowTestRetries = hasEarlyFlakeDetectionRetryCount
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
- earlyFlakeDetectionNumRetries,
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
- for (const [filename, bitmap] of Object.entries(parsedResponse.meta?.coverage || {})) {
38
- coverage[filename.replaceAll('\\', '/')] = bitmap
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, isCoverageReportUploadEnabled,
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(data, testLevel)
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 {number} numRetries
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
- numRetries: DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES,
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 numRetries = DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES
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
- slowTestRetries = parsedSlowTestRetries
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
- numRetries,
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
- earlyFlakeDetectionNumRetries: earlyFlakeDetection.numRetries,
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 coverageContent = readFileSync(filePath)
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 (err) {
49
- return callback(new Error(`Failed to read coverage report at ${filePath}: ${err.message}`))
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(result, testLevel)
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'