dd-trace 5.110.0 → 5.112.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 (164) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/README.md +13 -8
  3. package/ci/init.js +21 -2
  4. package/ci/vitest-no-worker-init-setup.mjs +430 -0
  5. package/ext/tags.js +2 -0
  6. package/index.d.ts +34 -1
  7. package/initialize.mjs +5 -6
  8. package/loader-hook.mjs +76 -55
  9. package/package.json +37 -34
  10. package/packages/datadog-instrumentations/src/ai.js +45 -0
  11. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  12. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  13. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  14. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  15. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  16. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  17. package/packages/datadog-instrumentations/src/fs.js +8 -6
  18. package/packages/datadog-instrumentations/src/graphql.js +26 -495
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  20. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  25. package/packages/datadog-instrumentations/src/jest.js +713 -63
  26. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  27. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  28. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  29. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  30. package/packages/datadog-instrumentations/src/mongoose.js +13 -2
  31. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  32. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1297 -0
  33. package/packages/datadog-instrumentations/src/vitest-main.js +1594 -0
  34. package/packages/datadog-instrumentations/src/vitest-util.js +254 -0
  35. package/packages/datadog-instrumentations/src/vitest-worker.js +823 -0
  36. package/packages/datadog-instrumentations/src/vitest.js +11 -1855
  37. package/packages/datadog-plugin-ai/src/index.js +1 -1
  38. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  39. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  40. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +19 -10
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  43. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
  44. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  45. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  46. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  47. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  48. package/packages/datadog-plugin-cucumber/src/index.js +15 -24
  49. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +360 -14
  50. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  51. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  52. package/packages/datadog-plugin-cypress/src/support.js +45 -1
  53. package/packages/datadog-plugin-express/src/code_origin.js +1 -1
  54. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
  55. package/packages/datadog-plugin-graphql/src/execute.js +639 -12
  56. package/packages/datadog-plugin-graphql/src/index.js +37 -9
  57. package/packages/datadog-plugin-graphql/src/parse.js +24 -4
  58. package/packages/datadog-plugin-graphql/src/utils.js +9 -2
  59. package/packages/datadog-plugin-graphql/src/validate.js +17 -6
  60. package/packages/datadog-plugin-http/src/index.js +6 -8
  61. package/packages/datadog-plugin-jest/src/index.js +31 -15
  62. package/packages/datadog-plugin-mocha/src/index.js +40 -15
  63. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  64. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  65. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  66. package/packages/datadog-plugin-openai/src/services.js +2 -2
  67. package/packages/datadog-plugin-playwright/src/index.js +4 -3
  68. package/packages/datadog-plugin-vitest/src/index.js +120 -72
  69. package/packages/datadog-shimmer/src/shimmer.js +37 -16
  70. package/packages/dd-trace/src/aiguard/index.js +9 -14
  71. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  72. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  73. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  74. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  75. package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
  76. package/packages/dd-trace/src/appsec/channels.js +3 -1
  77. package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
  78. package/packages/dd-trace/src/appsec/graphql.js +14 -11
  79. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +23 -23
  80. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  81. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
  82. package/packages/dd-trace/src/appsec/index.js +10 -1
  83. package/packages/dd-trace/src/appsec/lambda.js +135 -0
  84. package/packages/dd-trace/src/appsec/reporter.js +49 -10
  85. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
  86. package/packages/dd-trace/src/appsec/waf/index.js +16 -4
  87. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
  88. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  89. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  90. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
  91. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +5 -2
  92. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  93. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  94. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +6 -2
  95. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  96. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +130 -20
  97. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
  98. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
  99. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
  100. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
  101. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
  102. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
  103. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  104. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
  105. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
  106. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
  107. package/packages/dd-trace/src/config/defaults.js +3 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +71 -57
  109. package/packages/dd-trace/src/config/helper.js +1 -0
  110. package/packages/dd-trace/src/config/index.js +35 -22
  111. package/packages/dd-trace/src/config/major-overrides.js +4 -2
  112. package/packages/dd-trace/src/config/remote_config.js +1 -1
  113. package/packages/dd-trace/src/config/supported-configurations.json +127 -56
  114. package/packages/dd-trace/src/constants.js +7 -0
  115. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  116. package/packages/dd-trace/src/datastreams/processor.js +11 -3
  117. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  118. package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
  119. package/packages/dd-trace/src/llmobs/index.js +1 -1
  120. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  121. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  122. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  123. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  124. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
  125. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  126. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  127. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  128. package/packages/dd-trace/src/llmobs/sdk.js +3 -3
  129. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  130. package/packages/dd-trace/src/llmobs/tagger.js +3 -3
  131. package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
  132. package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
  133. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
  134. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
  135. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
  136. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  137. package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
  138. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  139. package/packages/dd-trace/src/opentracing/tracer.js +6 -1
  140. package/packages/dd-trace/src/plugin_manager.js +23 -7
  141. package/packages/dd-trace/src/plugins/ci_plugin.js +159 -10
  142. package/packages/dd-trace/src/plugins/index.js +2 -0
  143. package/packages/dd-trace/src/plugins/util/llm.js +6 -1
  144. package/packages/dd-trace/src/plugins/util/test.js +15 -9
  145. package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
  146. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
  147. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
  148. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  149. package/packages/dd-trace/src/proxy.js +8 -8
  150. package/packages/dd-trace/src/span_processor.js +5 -6
  151. package/packages/dd-trace/src/span_stats.js +96 -78
  152. package/packages/dd-trace/src/startup-log.js +2 -1
  153. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  154. package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
  155. package/packages/dd-trace/src/telemetry/index.js +2 -2
  156. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  157. package/packages/dd-trace/src/telemetry/send-data.js +8 -8
  158. package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
  159. package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
  160. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  161. package/vendor/dist/protobufjs/index.js +1 -1
  162. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  163. package/vendor/dist/shell-quote/index.js +1 -1
  164. package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
@@ -50,7 +50,7 @@ function getCommonRequestOptions (url) {
50
50
  return {
51
51
  method: 'POST',
52
52
  headers: {
53
- 'dd-api-key': getConfig().apiKey,
53
+ 'dd-api-key': getConfig().DD_API_KEY,
54
54
  },
55
55
  timeout: 15_000,
56
56
  url,
@@ -287,7 +287,7 @@ function sendGitMetadata (url, { isEvpProxy, evpProxyPrefix }, configRepositoryU
287
287
  }
288
288
  // Otherwise we unshallow and get commits to upload again
289
289
  log.debug('It is shallow clone, unshallowing...')
290
- if (getConfig().DD_CIVISIBILITY_GIT_UNSHALLOW_ENABLED) {
290
+ if (getConfig().testOptimization.DD_CIVISIBILITY_GIT_UNSHALLOW_ENABLED) {
291
291
  unshallowRepository(false)
292
292
  }
293
293
 
@@ -3,6 +3,11 @@ const { JSONEncoder } = require('../../encode/json-encoder')
3
3
  const { getEnvironmentVariable } = require('../../../config/helper')
4
4
  const log = require('../../../log')
5
5
 
6
+ function getVitestWorkerPort () {
7
+ const port = globalThis.__vitest_worker__?.ctx?.port
8
+ return typeof port?.postMessage === 'function' ? port : undefined
9
+ }
10
+
6
11
  class Writer {
7
12
  constructor (interprocessCode) {
8
13
  this._encoder = new JSONEncoder()
@@ -37,6 +42,18 @@ class Writer {
37
42
  ? { __tinypool_worker_message__: true, interprocessCode: this._interprocessCode, data }
38
43
  : [this._interprocessCode, data]
39
44
 
45
+ const vitestWorkerPort = getVitestWorkerPort()
46
+ if (vitestWorkerPort) {
47
+ try {
48
+ vitestWorkerPort.postMessage(payload)
49
+ } catch (error) {
50
+ log.error('Error posting message to vitest worker port', error)
51
+ } finally {
52
+ onDone()
53
+ }
54
+ return
55
+ }
56
+
40
57
  // child_process workers (jest default, cucumber)
41
58
  if (process.send) {
42
59
  process.send(payload, () => {
@@ -14,6 +14,42 @@ const {
14
14
  TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES,
15
15
  } = require('../../ci-visibility/telemetry')
16
16
  const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
17
+ const { validateSkippableTestsResponse } = require('../test-optimization-http-cache-schema')
18
+
19
+ function parseJsonResponse (rawJson) {
20
+ return typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson
21
+ }
22
+
23
+ function parseSkippableSuitesResponse (
24
+ rawJson,
25
+ { testLevel = 'suite', isCoverageReportUploadEnabled = false, validateRequiredFields = false } = {}
26
+ ) {
27
+ const parsedResponse = parseJsonResponse(rawJson)
28
+ if (validateRequiredFields) {
29
+ validateSkippableTestsResponse(parsedResponse)
30
+ }
31
+ const coverage = parsedResponse.meta?.coverage || {}
32
+
33
+ const skippableItems = parsedResponse
34
+ .data
35
+ .filter(({ type }) => type === testLevel)
36
+ const skippableSuites = []
37
+ for (const {
38
+ attributes: {
39
+ suite,
40
+ name,
41
+ _is_missing_line_code_coverage: isMissingLineCodeCoverage,
42
+ },
43
+ } of skippableItems) {
44
+ // Only reject candidates without backend line coverage when we need that coverage to backfill reports.
45
+ if (isCoverageReportUploadEnabled && isMissingLineCodeCoverage) continue
46
+
47
+ skippableSuites.push(testLevel === 'suite' ? suite : { suite, name })
48
+ }
49
+ const correlationId = parsedResponse.meta?.correlation_id
50
+
51
+ return { skippableSuites, correlationId, coverage }
52
+ }
17
53
 
18
54
  function getSkippableSuites ({
19
55
  url,
@@ -124,12 +160,12 @@ function fetchFromApi ({
124
160
  options.path = `${evpProxyPrefix}/api/v2/ci/tests/skippable`
125
161
  options.headers['X-Datadog-EVP-Subdomain'] = 'api'
126
162
  } else {
127
- const { apiKey } = getConfig()
128
- if (!apiKey) {
163
+ const { DD_API_KEY } = getConfig()
164
+ if (!DD_API_KEY) {
129
165
  return done(new Error('Skippable suites were not fetched because Datadog API key is not defined.'))
130
166
  }
131
167
 
132
- options.headers['dd-api-key'] = apiKey
168
+ options.headers['dd-api-key'] = DD_API_KEY
133
169
  }
134
170
 
135
171
  const data = JSON.stringify({
@@ -163,26 +199,12 @@ function fetchFromApi ({
163
199
  done(err)
164
200
  } else {
165
201
  try {
166
- const parsedResponse = JSON.parse(res)
167
- const coverage = parsedResponse.meta?.coverage || {}
168
-
169
- const skippableItems = parsedResponse
170
- .data
171
- .filter(({ type }) => type === testLevel)
172
- const skippableSuites = []
173
- for (const {
174
- attributes: {
175
- suite,
176
- name,
177
- _is_missing_line_code_coverage: isMissingLineCodeCoverage,
178
- },
179
- } of skippableItems) {
180
- // Only reject candidates without backend line coverage when we need that coverage to backfill reports.
181
- if (isCoverageReportUploadEnabled && isMissingLineCodeCoverage) continue
182
-
183
- skippableSuites.push(testLevel === 'suite' ? suite : { suite, name })
184
- }
185
- const correlationId = parsedResponse.meta?.correlation_id
202
+ const parsedResponse = parseJsonResponse(res)
203
+ const result = parseSkippableSuitesResponse(parsedResponse, {
204
+ testLevel,
205
+ isCoverageReportUploadEnabled,
206
+ })
207
+ const skippableItems = parsedResponse.data.filter(({ type }) => type === testLevel)
186
208
  incrementCountMetric(
187
209
  testLevel === 'test'
188
210
  ? TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS
@@ -191,9 +213,8 @@ function fetchFromApi ({
191
213
  skippableItems.length
192
214
  )
193
215
  distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, {}, res.length)
194
- log.debug('Number of received skippable %ss:', testLevel, skippableSuites.length)
216
+ log.debug('Number of received skippable %ss:', testLevel, result.skippableSuites.length)
195
217
 
196
- const result = { skippableSuites, correlationId, coverage }
197
218
  writeToCache(cacheKey, result)
198
219
 
199
220
  done(null, result)
@@ -204,4 +225,4 @@ function fetchFromApi ({
204
225
  })
205
226
  }
206
227
 
207
- module.exports = { getSkippableSuites }
228
+ module.exports = { getSkippableSuites, parseSkippableSuitesResponse }
@@ -4,14 +4,14 @@ const Plugin = require('../../plugins/plugin')
4
4
  const log = require('../../log')
5
5
 
6
6
  function getWinstonLogSubmissionParameters (config) {
7
- const { site, service, apiKey, DD_AGENTLESS_LOG_SUBMISSION_URL } = config
7
+ const { site, service, DD_API_KEY, DD_AGENTLESS_LOG_SUBMISSION_URL } = config
8
8
 
9
9
  const defaultParameters = {
10
10
  host: `http-intake.logs.${site}`,
11
11
  path: `/api/v2/logs?ddsource=winston&service=${service}`,
12
12
  ssl: true,
13
13
  headers: {
14
- 'DD-API-KEY': apiKey,
14
+ 'DD-API-KEY': DD_API_KEY,
15
15
  },
16
16
  }
17
17
 
@@ -12,12 +12,72 @@ const {
12
12
  TELEMETRY_GIT_REQUESTS_SETTINGS_RESPONSE,
13
13
  } = require('../telemetry')
14
14
  const { writeSettingsToCache } = require('../test-optimization-cache')
15
+ const { validateSettingsResponse } = require('../test-optimization-http-cache-schema')
15
16
  const request = require('./request')
16
17
 
17
18
  const DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES = 2
18
19
  const DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES = { '5s': 10, '10s': 5, '30s': 3, '5m': 2 }
19
20
  const DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD = 30
20
21
 
22
+ function parseJsonResponse (rawJson) {
23
+ return typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson
24
+ }
25
+
26
+ function parseLibraryConfigurationResponse (rawJson, config = getConfig(), options = {}) {
27
+ const parsedResponse = parseJsonResponse(rawJson)
28
+ if (options.validateRequiredFields) {
29
+ validateSettingsResponse(parsedResponse)
30
+ }
31
+ const {
32
+ code_coverage: isCodeCoverageEnabled,
33
+ tests_skipping: isSuitesSkippingEnabled,
34
+ itr_enabled: isItrEnabled,
35
+ require_git: requireGit,
36
+ early_flake_detection: earlyFlakeDetectionConfig,
37
+ flaky_test_retries_enabled: isFlakyTestRetriesEnabled,
38
+ di_enabled: isDiEnabled,
39
+ known_tests_enabled: isKnownTestsEnabled,
40
+ test_management: testManagementConfig,
41
+ impacted_tests_enabled: isImpactedTestsEnabled,
42
+ coverage_report_upload_enabled: isCoverageReportUploadEnabled,
43
+ } = parsedResponse?.data?.attributes ?? parsedResponse
44
+
45
+ const settings = {
46
+ isCodeCoverageEnabled,
47
+ isSuitesSkippingEnabled,
48
+ isItrEnabled,
49
+ requireGit,
50
+ isEarlyFlakeDetectionEnabled: isKnownTestsEnabled && (earlyFlakeDetectionConfig?.enabled ?? false),
51
+ earlyFlakeDetectionNumRetries:
52
+ earlyFlakeDetectionConfig?.slow_test_retries?.['5s'] || DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES,
53
+ earlyFlakeDetectionSlowTestRetries:
54
+ earlyFlakeDetectionConfig?.slow_test_retries ?? DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES,
55
+ earlyFlakeDetectionFaultyThreshold:
56
+ earlyFlakeDetectionConfig?.faulty_session_threshold ?? DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD,
57
+ isFlakyTestRetriesEnabled,
58
+ isDiEnabled: isDiEnabled && isFlakyTestRetriesEnabled,
59
+ isKnownTestsEnabled,
60
+ isTestManagementEnabled: (testManagementConfig?.enabled ?? false),
61
+ testManagementAttemptToFixRetries:
62
+ testManagementConfig?.attempt_to_fix_retries,
63
+ isImpactedTestsEnabled,
64
+ isCoverageReportUploadEnabled: isCoverageReportUploadEnabled ?? false,
65
+ }
66
+
67
+ log.debug('Remote settings: %j', settings)
68
+
69
+ if (config.testOptimization.DD_CIVISIBILITY_DANGEROUSLY_FORCE_COVERAGE) {
70
+ settings.isCodeCoverageEnabled = true
71
+ log.debug('Dangerously set code coverage to true')
72
+ }
73
+ if (config.testOptimization.DD_CIVISIBILITY_DANGEROUSLY_FORCE_TEST_SKIPPING) {
74
+ settings.isSuitesSkippingEnabled = true
75
+ log.debug('Dangerously set test skipping to true')
76
+ }
77
+
78
+ return settings
79
+ }
80
+
21
81
  function getLibraryConfiguration ({
22
82
  url,
23
83
  isEvpProxy,
@@ -51,10 +111,10 @@ function getLibraryConfiguration ({
51
111
  options.path = `${evpProxyPrefix}/api/v2/libraries/tests/services/setting`
52
112
  options.headers['X-Datadog-EVP-Subdomain'] = 'api'
53
113
  } else {
54
- if (!config.apiKey) {
114
+ if (!config.DD_API_KEY) {
55
115
  return done(new Error('Request to settings endpoint was not done because Datadog API key is not defined.'))
56
116
  }
57
- options.headers['dd-api-key'] = config.apiKey
117
+ options.headers['dd-api-key'] = config.DD_API_KEY
58
118
  }
59
119
 
60
120
  const data = JSON.stringify({
@@ -90,56 +150,7 @@ function getLibraryConfiguration ({
90
150
  done(err)
91
151
  } else {
92
152
  try {
93
- const {
94
- data: {
95
- attributes: {
96
- code_coverage: isCodeCoverageEnabled,
97
- tests_skipping: isSuitesSkippingEnabled,
98
- itr_enabled: isItrEnabled,
99
- require_git: requireGit,
100
- early_flake_detection: earlyFlakeDetectionConfig,
101
- flaky_test_retries_enabled: isFlakyTestRetriesEnabled,
102
- di_enabled: isDiEnabled,
103
- known_tests_enabled: isKnownTestsEnabled,
104
- test_management: testManagementConfig,
105
- impacted_tests_enabled: isImpactedTestsEnabled,
106
- coverage_report_upload_enabled: isCoverageReportUploadEnabled,
107
- },
108
- },
109
- } = JSON.parse(res)
110
-
111
- const settings = {
112
- isCodeCoverageEnabled,
113
- isSuitesSkippingEnabled,
114
- isItrEnabled,
115
- requireGit,
116
- isEarlyFlakeDetectionEnabled: isKnownTestsEnabled && (earlyFlakeDetectionConfig?.enabled ?? false),
117
- earlyFlakeDetectionNumRetries:
118
- earlyFlakeDetectionConfig?.slow_test_retries?.['5s'] || DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES,
119
- earlyFlakeDetectionSlowTestRetries:
120
- earlyFlakeDetectionConfig?.slow_test_retries ?? DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES,
121
- earlyFlakeDetectionFaultyThreshold:
122
- earlyFlakeDetectionConfig?.faulty_session_threshold ?? DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD,
123
- isFlakyTestRetriesEnabled,
124
- isDiEnabled: isDiEnabled && isFlakyTestRetriesEnabled,
125
- isKnownTestsEnabled,
126
- isTestManagementEnabled: (testManagementConfig?.enabled ?? false),
127
- testManagementAttemptToFixRetries:
128
- testManagementConfig?.attempt_to_fix_retries,
129
- isImpactedTestsEnabled,
130
- isCoverageReportUploadEnabled: isCoverageReportUploadEnabled ?? false,
131
- }
132
-
133
- log.debug('Remote settings: %j', settings)
134
-
135
- if (config.DD_CIVISIBILITY_DANGEROUSLY_FORCE_COVERAGE) {
136
- settings.isCodeCoverageEnabled = true
137
- log.debug('Dangerously set code coverage to true')
138
- }
139
- if (config.DD_CIVISIBILITY_DANGEROUSLY_FORCE_TEST_SKIPPING) {
140
- settings.isSuitesSkippingEnabled = true
141
- log.debug('Dangerously set test skipping to true')
142
- }
153
+ const settings = parseLibraryConfigurationResponse(res, config)
143
154
 
144
155
  incrementCountMetric(TELEMETRY_GIT_REQUESTS_SETTINGS_RESPONSE, settings)
145
156
 
@@ -153,4 +164,4 @@ function getLibraryConfiguration ({
153
164
  })
154
165
  }
155
166
 
156
- module.exports = { getLibraryConfiguration }
167
+ module.exports = { getLibraryConfiguration, parseLibraryConfigurationResponse }
@@ -34,9 +34,9 @@ function uploadCoverageReport (
34
34
  { filePath, format, testEnvironmentMetadata, url, isEvpProxy, evpProxyPrefix },
35
35
  callback
36
36
  ) {
37
- const apiKey = getConfig().apiKey
37
+ const { DD_API_KEY } = getConfig()
38
38
 
39
- if (!apiKey && !isEvpProxy) {
39
+ if (!DD_API_KEY && !isEvpProxy) {
40
40
  return callback(new Error('DD_API_KEY is required for coverage report upload'))
41
41
  }
42
42
 
@@ -82,7 +82,7 @@ function uploadCoverageReport (
82
82
  options.headers['X-Datadog-EVP-Subdomain'] = 'ci-intake'
83
83
  } else {
84
84
  options.path = '/api/v2/cicovreprt'
85
- options.headers['dd-api-key'] = apiKey
85
+ options.headers['dd-api-key'] = DD_API_KEY
86
86
  }
87
87
 
88
88
  log.debug('Uploading coverage report %s to %s%s', filePath, url, options.path)
@@ -0,0 +1,156 @@
1
+ 'use strict'
2
+
3
+ const { readFileSync } = require('node:fs')
4
+ const { extname } = require('node:path')
5
+
6
+ const getConfig = require('../../config')
7
+ const request = require('../../exporters/common/request')
8
+ const log = require('../../log')
9
+
10
+ const UPLOAD_TIMEOUT_MS = 30_000
11
+ const TEST_SCREENSHOT_ENDPOINT_PREFIX = '/api/v2/ci/test-runs/'
12
+ const TEST_SCREENSHOT_ENDPOINT_SUFFIX = '/media'
13
+ const UINT64_MAX = 18_446_744_073_709_551_615n
14
+
15
+ function getContentType (filePath) {
16
+ const extension = extname(filePath).toLowerCase()
17
+ if (extension === '.gif') {
18
+ return 'image/gif'
19
+ }
20
+ if (extension === '.jpg' || extension === '.jpeg') {
21
+ return 'image/jpeg'
22
+ }
23
+ if (extension === '.webp') {
24
+ return 'image/webp'
25
+ }
26
+ return 'image/png'
27
+ }
28
+
29
+ function isValidTraceId (traceId) {
30
+ if (!/^[1-9]\d*$/.test(traceId)) {
31
+ return false
32
+ }
33
+ return BigInt(traceId) <= UINT64_MAX
34
+ }
35
+
36
+ /**
37
+ * Renders the idempotency key (`${traceId}:${basename(filePath)}`) into a value safe to carry in
38
+ * the upload's query string. The Agent's evp_proxy validates the forwarded query against a
39
+ * restrictive charset and rejects a raw Cypress filename (spaces, parens, non-ASCII), so the
40
+ * filename part is hex-encoded to [0-9a-f]; the decimal trace id and ':' separator are already in
41
+ * the allowed set and stay readable. Deterministic, so a retried upload reproduces the same key
42
+ * and the backend's UUIDv5 overwrite-on-retry holds.
43
+ *
44
+ * @param {string} idempotencyKey - The raw idempotency key (`<traceId>:<filename>`)
45
+ * @returns {string} A query-safe, deterministic representation of the key
46
+ */
47
+ function toIdempotencyQueryValue (idempotencyKey) {
48
+ const separatorIndex = idempotencyKey.indexOf(':')
49
+ if (separatorIndex === -1) {
50
+ return Buffer.from(idempotencyKey, 'utf8').toString('hex')
51
+ }
52
+ const traceIdPart = idempotencyKey.slice(0, separatorIndex)
53
+ const filenamePart = idempotencyKey.slice(separatorIndex + 1)
54
+ return `${traceIdPart}:${Buffer.from(filenamePart, 'utf8').toString('hex')}`
55
+ }
56
+
57
+ /**
58
+ * Uploads a single test screenshot to the Test Optimization media intake.
59
+ * The trace id is included in the request path and the body is the raw image bytes.
60
+ *
61
+ * The media service requires two values from the tracer, sent as query params so they
62
+ * survive the Agent's evp_proxy (which forwards only an allow-listed header set and would
63
+ * otherwise strip the metadata, leaving the backend with an empty idempotency key):
64
+ * - `idempotencyKey`: stable per artifact and reused on retry, so a retried
65
+ * upload overwrites the same stored object instead of creating a duplicate.
66
+ * - `capturedAtMs`: the capture time in epoch milliseconds, stamped once at
67
+ * capture and resent unchanged on retry (it is part of the stored object key).
68
+ *
69
+ * @param {object} options - Upload options
70
+ * @param {string} options.filePath - Path to the screenshot file
71
+ * @param {string} options.traceId - Test trace id used as the screenshot key
72
+ * @param {string} options.idempotencyKey - Stable per-artifact key, reused on retry
73
+ * @param {number} options.capturedAtMs - Capture time in epoch milliseconds
74
+ * @param {URL} options.url - The base URL for the screenshot upload
75
+ * @param {boolean} [options.isEvpProxy] - Whether to upload through the Agent's evp_proxy
76
+ * @param {string} [options.evpProxyPrefix] - The evp_proxy path prefix (e.g. '/evp_proxy/v4')
77
+ * @param {Function} callback - Callback function (err)
78
+ */
79
+ function uploadTestScreenshot (
80
+ { filePath, traceId, idempotencyKey, capturedAtMs, url, isEvpProxy, evpProxyPrefix },
81
+ callback
82
+ ) {
83
+ const { DD_API_KEY } = getConfig()
84
+
85
+ if (!isValidTraceId(traceId)) {
86
+ return callback(new Error('A non-zero decimal uint64 trace_id is required for test screenshot upload'))
87
+ }
88
+ if (!DD_API_KEY && !isEvpProxy) {
89
+ return callback(new Error('DD_API_KEY is required for test screenshot upload'))
90
+ }
91
+ if (!idempotencyKey) {
92
+ return callback(new Error('An idempotency key is required for test screenshot upload'))
93
+ }
94
+ if (!Number.isInteger(capturedAtMs) || capturedAtMs <= 0) {
95
+ return callback(new Error('A positive captured-at timestamp (epoch ms) is required for test screenshot upload'))
96
+ }
97
+
98
+ let screenshotContent
99
+ try {
100
+ screenshotContent = readFileSync(filePath)
101
+ } catch (err) {
102
+ return callback(new Error(`Failed to read screenshot at ${filePath}: ${err.message}`))
103
+ }
104
+ if (screenshotContent.length === 0) {
105
+ return callback(new Error(`Screenshot at ${filePath} is empty`))
106
+ }
107
+
108
+ // Metadata rides the query string, not X-Dd-* headers: the Agent's evp_proxy strips
109
+ // non-allow-listed headers, so header-borne metadata reached the backend empty. The key is
110
+ // rendered proxy-safe (see toIdempotencyQueryValue) because evp_proxy also validates the
111
+ // forwarded query against a restrictive charset. capturedAtMs is a plain integer and part of the
112
+ // stored object key, so it (and the key) must stay stable across retries.
113
+ const query = new URLSearchParams({
114
+ idempotency_key: toIdempotencyQueryValue(idempotencyKey),
115
+ captured_at_ms: String(capturedAtMs),
116
+ }).toString()
117
+ const basePath = `${TEST_SCREENSHOT_ENDPOINT_PREFIX}${traceId}${TEST_SCREENSHOT_ENDPOINT_SUFFIX}`
118
+
119
+ const contentType = getContentType(filePath)
120
+ const options = {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': contentType,
124
+ },
125
+ path: `${basePath}?${query}`,
126
+ timeout: UPLOAD_TIMEOUT_MS,
127
+ url,
128
+ }
129
+
130
+ if (isEvpProxy) {
131
+ // Agent mode: prefix the evp_proxy path, tell the proxy which subdomain to forward to, and
132
+ // drop the API key — the Agent injects it. The query params survive the proxy.
133
+ options.path = `${evpProxyPrefix}${basePath}?${query}`
134
+ options.headers['X-Datadog-EVP-Subdomain'] = 'api'
135
+ } else {
136
+ options.headers['DD-API-KEY'] = DD_API_KEY
137
+ }
138
+
139
+ log.debug('Uploading test screenshot %s to %s', filePath, new URL(options.path, url).href)
140
+
141
+ request(screenshotContent, options, (err, res, statusCode) => {
142
+ if (err) {
143
+ log.error('Error uploading test screenshot: %s', err.message)
144
+ return callback(err)
145
+ }
146
+ if (statusCode === undefined) {
147
+ const uploadError = new Error('Test screenshot upload request was dropped before it was sent')
148
+ log.error('Error uploading test screenshot: %s', uploadError.message)
149
+ return callback(uploadError)
150
+ }
151
+ log.debug('Test screenshot uploaded successfully (status: %d)', statusCode)
152
+ callback(null)
153
+ })
154
+ }
155
+
156
+ module.exports = { TEST_SCREENSHOT_ENDPOINT_PREFIX, TEST_SCREENSHOT_ENDPOINT_SUFFIX, uploadTestScreenshot }
@@ -16,6 +16,7 @@ const {
16
16
  } = require('../telemetry')
17
17
 
18
18
  const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
19
+ const { validateTestManagementTestsResponse } = require('../test-optimization-http-cache-schema')
19
20
 
20
21
  // Calculate the number of tests from the test management tests response, which has a shape like:
21
22
  // { module: { suites: { suite: { tests: { testName: { properties: {...} } } } } } }
@@ -39,6 +40,19 @@ function getNumFromTestManagementTests (testManagementTests) {
39
40
  return totalNumTests
40
41
  }
41
42
 
43
+ function parseJsonResponse (rawJson) {
44
+ return typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson
45
+ }
46
+
47
+ function parseTestManagementTestsResponse (rawJson, options = {}) {
48
+ const parsedResponse = parseJsonResponse(rawJson)
49
+ if (options.validateRequiredFields) {
50
+ validateTestManagementTestsResponse(parsedResponse)
51
+ }
52
+ const { data: { attributes: { modules: testManagementTests } } } = parsedResponse
53
+ return testManagementTests
54
+ }
55
+
42
56
  function getTestManagementTests ({
43
57
  url,
44
58
  isEvpProxy,
@@ -121,12 +135,12 @@ function fetchFromApi ({
121
135
  options.path = `${evpProxyPrefix}/api/v2/test/libraries/test-management/tests`
122
136
  options.headers['X-Datadog-EVP-Subdomain'] = 'api'
123
137
  } else {
124
- const { apiKey } = getConfig()
125
- if (!apiKey) {
138
+ const { DD_API_KEY } = getConfig()
139
+ if (!DD_API_KEY) {
126
140
  return done(new Error('Test management tests were not fetched because Datadog API key is not defined.'))
127
141
  }
128
142
 
129
- options.headers['dd-api-key'] = apiKey
143
+ options.headers['dd-api-key'] = DD_API_KEY
130
144
  }
131
145
 
132
146
  const data = JSON.stringify({
@@ -155,7 +169,7 @@ function fetchFromApi ({
155
169
  done(err)
156
170
  } else {
157
171
  try {
158
- const { data: { attributes: { modules: testManagementTests } } } = JSON.parse(res)
172
+ const testManagementTests = parseTestManagementTestsResponse(res)
159
173
 
160
174
  const numTests = getNumFromTestManagementTests(testManagementTests)
161
175
 
@@ -174,4 +188,8 @@ function fetchFromApi ({
174
188
  })
175
189
  }
176
190
 
177
- module.exports = { getTestManagementTests }
191
+ module.exports = {
192
+ getNumFromTestManagementTests, // Exported for later use in the cache
193
+ getTestManagementTests,
194
+ parseTestManagementTestsResponse,
195
+ }