dd-trace 6.12.0 → 6.14.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 (251) hide show
  1. package/LICENSE-3rdparty.csv +3 -1
  2. package/ci/init.js +8 -3
  3. package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
  4. package/ci/test-optimization-validation/ci-remediation.js +6 -6
  5. package/ci/test-optimization-validation/command-blocker.js +7 -5
  6. package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
  7. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  8. package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
  9. package/ci/test-optimization-validation/package-check.js +1 -1
  10. package/ci/test-optimization-validation/report-writer.js +7 -5
  11. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  12. package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
  13. package/ci/vitest-no-worker-init-setup.mjs +13 -13
  14. package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
  15. package/index.d.ts +127 -26
  16. package/package.json +22 -19
  17. package/packages/datadog-esbuild/index.js +3 -1
  18. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  19. package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
  20. package/packages/datadog-instrumentations/src/fs.js +24 -20
  21. package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
  22. package/packages/datadog-instrumentations/src/graphql.js +16 -16
  23. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  24. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
  25. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  26. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +266 -0
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  33. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
  34. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +13 -0
  35. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +279 -0
  36. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +12 -1
  37. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
  38. package/packages/datadog-instrumentations/src/jest.js +165 -38
  39. package/packages/datadog-instrumentations/src/knex.js +76 -0
  40. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  41. package/packages/datadog-instrumentations/src/mariadb.js +13 -0
  42. package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
  43. package/packages/datadog-instrumentations/src/mocha/main.js +25 -15
  44. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  45. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
  46. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
  47. package/packages/datadog-instrumentations/src/net.js +14 -17
  48. package/packages/datadog-instrumentations/src/next.js +212 -62
  49. package/packages/datadog-instrumentations/src/openai.js +42 -40
  50. package/packages/datadog-instrumentations/src/pg.js +15 -0
  51. package/packages/datadog-instrumentations/src/pino.js +15 -4
  52. package/packages/datadog-instrumentations/src/playwright.js +137 -27
  53. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
  54. package/packages/datadog-instrumentations/src/sequelize.js +94 -2
  55. package/packages/datadog-instrumentations/src/undici.js +53 -4
  56. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +44 -7
  57. package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
  58. package/packages/datadog-instrumentations/src/vitest-worker.js +82 -14
  59. package/packages/datadog-instrumentations/src/webdriverio.js +986 -1
  60. package/packages/datadog-instrumentations/src/winston.js +6 -3
  61. package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
  62. package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
  63. package/packages/datadog-plugin-amqp10/src/util.js +2 -1
  64. package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
  65. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
  66. package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
  67. package/packages/datadog-plugin-cucumber/src/index.js +0 -2
  68. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
  69. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  70. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
  71. package/packages/datadog-plugin-graphql/src/execute.js +998 -196
  72. package/packages/datadog-plugin-graphql/src/index.js +27 -7
  73. package/packages/datadog-plugin-graphql/src/jit.js +136 -0
  74. package/packages/datadog-plugin-graphql/src/request.js +18 -27
  75. package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
  76. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
  77. package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
  78. package/packages/datadog-plugin-graphql/src/utils.js +208 -120
  79. package/packages/datadog-plugin-graphql/src/validate.js +10 -23
  80. package/packages/datadog-plugin-http/src/client.js +2 -15
  81. package/packages/datadog-plugin-http2/src/client.js +2 -15
  82. package/packages/datadog-plugin-jest/src/index.js +0 -6
  83. package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
  84. package/packages/datadog-plugin-mocha/src/index.js +403 -75
  85. package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
  86. package/packages/datadog-plugin-mysql/src/index.js +41 -15
  87. package/packages/datadog-plugin-net/src/tcp.js +3 -1
  88. package/packages/datadog-plugin-next/src/index.js +63 -29
  89. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  90. package/packages/datadog-plugin-playwright/src/index.js +176 -28
  91. package/packages/datadog-plugin-undici/src/index.js +29 -56
  92. package/packages/datadog-plugin-vitest/src/index.js +6 -12
  93. package/packages/dd-trace/src/aiguard/evaluation.js +96 -21
  94. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  95. package/packages/dd-trace/src/aiguard/messages/anthropic.js +29 -10
  96. package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
  97. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
  98. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  99. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  100. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  101. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  102. package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
  103. package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
  104. package/packages/dd-trace/src/appsec/graphql.js +6 -2
  105. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  106. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
  107. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
  108. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +11 -36
  109. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
  110. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  111. package/packages/dd-trace/src/appsec/lambda.js +44 -26
  112. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  113. package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
  114. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
  115. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
  116. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
  117. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +7 -150
  118. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
  119. package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
  121. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +397 -234
  122. package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
  124. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
  125. package/packages/dd-trace/src/ci-visibility/final-flush.js +21 -0
  126. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
  127. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
  128. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
  129. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +176 -0
  130. package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
  131. package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
  132. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  133. package/packages/dd-trace/src/config/index.js +75 -14
  134. package/packages/dd-trace/src/config/parsers.js +11 -1
  135. package/packages/dd-trace/src/config/stable.js +3 -3
  136. package/packages/dd-trace/src/config/supported-configurations.json +47 -2
  137. package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
  138. package/packages/dd-trace/src/debugger/config.js +12 -1
  139. package/packages/dd-trace/src/debugger/constants.js +1 -0
  140. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
  141. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +33 -0
  142. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -39
  143. package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
  144. package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
  145. package/packages/dd-trace/src/debugger/index.js +23 -3
  146. package/packages/dd-trace/src/dogstatsd.js +532 -71
  147. package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
  148. package/packages/dd-trace/src/encode/0.4.js +5 -5
  149. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
  150. package/packages/dd-trace/src/evp_proxy/direct.js +27 -23
  151. package/packages/dd-trace/src/exporters/agent/index.js +24 -3
  152. package/packages/dd-trace/src/exporters/agent/writer.js +28 -11
  153. package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
  154. package/packages/dd-trace/src/exporters/agentless/writer.js +100 -118
  155. package/packages/dd-trace/src/exporters/common/agents.js +28 -6
  156. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
  157. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
  158. package/packages/dd-trace/src/exporters/common/request.js +31 -11
  159. package/packages/dd-trace/src/exporters/common/url.js +40 -1
  160. package/packages/dd-trace/src/exporters/common/writer.js +31 -7
  161. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  162. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  163. package/packages/dd-trace/src/flush.js +105 -0
  164. package/packages/dd-trace/src/id.js +14 -0
  165. package/packages/dd-trace/src/index.js +5 -2
  166. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  167. package/packages/dd-trace/src/llmobs/experiments/dataset.js +52 -2
  168. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  169. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  170. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  171. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  172. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  173. package/packages/dd-trace/src/llmobs/index.js +111 -48
  174. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  175. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
  176. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
  177. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  178. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  179. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
  180. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  181. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  182. package/packages/dd-trace/src/llmobs/util.js +0 -33
  183. package/packages/dd-trace/src/llmobs/writers/base.js +106 -53
  184. package/packages/dd-trace/src/log/index.js +7 -1
  185. package/packages/dd-trace/src/log-submission/log-submission-plugin.js +249 -0
  186. package/packages/dd-trace/src/noop/span.js +5 -0
  187. package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
  188. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  189. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  190. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  191. package/packages/dd-trace/src/opentelemetry/logs/index.js +9 -25
  192. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  193. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
  194. package/packages/dd-trace/src/opentelemetry/metrics/index.js +18 -23
  195. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  196. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +18 -6
  197. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +14 -2
  198. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  199. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  200. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
  201. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
  202. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  203. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
  204. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  205. package/packages/dd-trace/src/opentracing/propagation/text_map.js +494 -351
  206. package/packages/dd-trace/src/opentracing/span.js +28 -0
  207. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  208. package/packages/dd-trace/src/payload-tagging/index.js +2 -1
  209. package/packages/dd-trace/src/plugin_manager.js +2 -0
  210. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
  211. package/packages/dd-trace/src/plugins/tracing.js +12 -10
  212. package/packages/dd-trace/src/plugins/util/ci.js +3 -1
  213. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
  214. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  215. package/packages/dd-trace/src/plugins/util/test.js +157 -41
  216. package/packages/dd-trace/src/plugins/util/url.js +2 -2
  217. package/packages/dd-trace/src/plugins/util/web.js +6 -74
  218. package/packages/dd-trace/src/profiling/config.js +1 -0
  219. package/packages/dd-trace/src/profiling/oom.js +11 -5
  220. package/packages/dd-trace/src/profiling/profiler.js +30 -13
  221. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  222. package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
  223. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  224. package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
  225. package/packages/dd-trace/src/proxy.js +108 -10
  226. package/packages/dd-trace/src/remote_config/index.js +319 -101
  227. package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
  228. package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
  229. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  230. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +10 -1
  231. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +22 -7
  232. package/packages/dd-trace/src/sampling_rule.js +36 -9
  233. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +51 -0
  234. package/packages/dd-trace/src/serverless/vercel.js +155 -0
  235. package/packages/dd-trace/src/serverless.js +45 -21
  236. package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
  237. package/packages/dd-trace/src/span_stats.js +64 -31
  238. package/packages/dd-trace/src/startup-log.js +3 -2
  239. package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
  240. package/packages/dd-trace/src/telemetry/send-data.js +18 -16
  241. package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
  242. package/packages/dd-trace/src/tracer.js +26 -5
  243. package/packages/dd-trace/src/tracer_metadata.js +11 -3
  244. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  245. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  246. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  247. package/vendor/dist/protobufjs/index.js +1 -1
  248. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  249. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
  250. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +0 -53
  251. package/packages/dd-trace/src/encode/agentless-json.js +0 -209
@@ -1,31 +1,56 @@
1
1
  'use strict'
2
2
 
3
+ const fs = require('node:fs')
3
4
  const { hostname: getHostname } = require('node:os')
4
5
  const URL = require('url').URL
5
6
 
6
7
  const { version: tracerVersion } = require('../../../../../package.json')
7
8
  const { EMPTY_EFD_RETRY_POLICY, createEfdRetryPolicy } = require('../efd-retry-policy')
8
9
  const { getLibraryConfiguration: getLibraryConfigurationRequest } = require('../requests/get-library-configuration')
10
+ const { getCachePath, withCache, writeToCache } = require('../requests/fs-cache')
9
11
  const { getSkippableSuites: getSkippableSuitesRequest } = require('../intelligent-test-runner/get-skippable-suites')
10
12
  const { getKnownTests: getKnownTestsRequest } = require('../early-flake-detection/get-known-tests')
11
13
  const { getTestManagementTests: getTestManagementTestsRequest } =
12
14
  require('../test-management/get-test-management-tests')
13
15
  const { writeSettingsToCache } = require('../test-optimization-cache')
14
16
  const { CACHE_MISS, TestOptimizationHttpCache } = require('../test-optimization-http-cache')
15
- const { incrementCountMetric, TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION } = require('../telemetry')
17
+ const { MAX_RETRIES } = require('../test-optimization-http-cache-schema')
16
18
  const { uploadCoverageReport: uploadCoverageReportRequest } = require('../requests/upload-coverage-report')
17
- const { uploadTestScreenshot: uploadTestScreenshotRequest } = require('../requests/upload-test-screenshot')
19
+ const {
20
+ uploadTestScreenshot: uploadTestScreenshotRequest,
21
+ uploadTestSuiteVideo: uploadTestSuiteVideoRequest,
22
+ uploadTestVideo: uploadTestVideoRequest,
23
+ } = require('../requests/upload-test-screenshot')
18
24
  const { parsers } = require('../../config/parsers')
19
25
  const log = require('../../log')
20
- const spanFormat = require('../../span_format')
21
26
  const { getSegment } = require('../../util')
22
27
  const BufferingExporter = require('../../exporters/common/buffering-exporter')
23
28
  const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags')
24
- const { TEST_STATUS } = require('../../plugins/util/test')
29
+ const {
30
+ createFinalFlushTimeoutError,
31
+ FINAL_FLUSH_FALLBACK_DELAY,
32
+ FINAL_FLUSH_TIMEOUT,
33
+ } = require('../final-flush')
34
+ const { incrementCountMetric, TELEMETRY_ENDPOINT_PAYLOAD_DROPPED } = require('../telemetry')
25
35
  const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata')
36
+ const buildSettingsCacheKey = require('./settings-cache-key')
26
37
 
27
38
  const hostname = getHostname()
28
39
  const EMPTY_SETTINGS = Object.freeze({})
40
+ const SETTINGS_CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
41
+ const SETTINGS_BOOLEAN_FIELDS = Object.freeze([
42
+ 'isCodeCoverageEnabled',
43
+ 'isSuitesSkippingEnabled',
44
+ 'isItrEnabled',
45
+ 'requireGit',
46
+ 'isEarlyFlakeDetectionEnabled',
47
+ 'isFlakyTestRetriesEnabled',
48
+ 'isDiEnabled',
49
+ 'isKnownTestsEnabled',
50
+ 'isTestManagementEnabled',
51
+ 'isImpactedTestsEnabled',
52
+ 'isCoverageReportUploadEnabled',
53
+ ])
29
54
 
30
55
  /**
31
56
  * Test session identity sent with every request. Fields are optional because the CI provider,
@@ -51,61 +76,131 @@ function getTestConfigurationTags (tags) {
51
76
  }, {})
52
77
  }
53
78
 
79
+ function isTestSessionEvent (span) {
80
+ return span.type === 'test_session_end' || span.type === 'test_suite_end' || span.type === 'test_module_end'
81
+ }
82
+
54
83
  function getIsTestSessionTrace (trace) {
55
- return trace.some(span =>
56
- span.type === 'test_session_end' || span.type === 'test_suite_end' || span.type === 'test_module_end'
57
- )
84
+ return trace.some(isTestSessionEvent)
85
+ }
86
+
87
+ /**
88
+ * Checks whether a value is a non-negative safe integer.
89
+ *
90
+ * @param {unknown} value - Candidate integer.
91
+ * @returns {value is number}
92
+ */
93
+ function isNonNegativeSafeInteger (value) {
94
+ return Number.isSafeInteger(value) && value >= 0
95
+ }
96
+
97
+ /**
98
+ * Checks whether a value is a retry count accepted by the settings parser.
99
+ *
100
+ * @param {unknown} value - Candidate retry count.
101
+ * @returns {value is number}
102
+ */
103
+ function isValidRetryCount (value) {
104
+ return isNonNegativeSafeInteger(value) && value <= MAX_RETRIES
105
+ }
106
+
107
+ /**
108
+ * Checks whether a cached EFD retry policy has the complete parsed shape.
109
+ *
110
+ * @param {unknown} retryPolicy - Candidate retry policy.
111
+ * @returns {boolean}
112
+ */
113
+ function isValidCachedEfdRetryPolicy (retryPolicy) {
114
+ if (retryPolicy === null || typeof retryPolicy !== 'object' || Array.isArray(retryPolicy)) return false
115
+ if (!isValidRetryCount(retryPolicy.schedulingRetryCount)) return false
116
+ if (!Array.isArray(retryPolicy.durationRetryCounts) ||
117
+ retryPolicy.durationRetryCounts.length !== EMPTY_EFD_RETRY_POLICY.durationRetryCounts.length) {
118
+ return false
119
+ }
120
+
121
+ let schedulingRetryCount = 0
122
+ for (let index = 0; index < EMPTY_EFD_RETRY_POLICY.durationRetryCounts.length; index++) {
123
+ const durationRetryCount = retryPolicy.durationRetryCounts[index]
124
+ if (durationRetryCount === null || typeof durationRetryCount !== 'object' ||
125
+ Array.isArray(durationRetryCount) ||
126
+ durationRetryCount.durationLimitMs !==
127
+ EMPTY_EFD_RETRY_POLICY.durationRetryCounts[index].durationLimitMs ||
128
+ !isValidRetryCount(durationRetryCount.retryCount)) {
129
+ return false
130
+ }
131
+ if (durationRetryCount.retryCount > schedulingRetryCount) {
132
+ schedulingRetryCount = durationRetryCount.retryCount
133
+ }
134
+ }
135
+ return retryPolicy.schedulingRetryCount === schedulingRetryCount
136
+ }
137
+
138
+ /**
139
+ * Checks whether a filesystem cache value has the complete shape produced by
140
+ * parseLibraryConfigurationResponse.
141
+ *
142
+ * @param {unknown} settings - Cached settings value.
143
+ * @returns {settings is Record<string, unknown>}
144
+ */
145
+ function isValidCachedSettings (settings) {
146
+ if (settings === null || typeof settings !== 'object' || Array.isArray(settings)) return false
147
+ for (const field of SETTINGS_BOOLEAN_FIELDS) {
148
+ if (typeof settings[field] !== 'boolean') return false
149
+ }
150
+ if (settings.isDiEnabled && !settings.isFlakyTestRetriesEnabled) return false
151
+ if (settings.isEarlyFlakeDetectionEnabled && !settings.isKnownTestsEnabled) return false
152
+ if (!isValidCachedEfdRetryPolicy(settings.earlyFlakeDetectionRetryPolicy)) return false
153
+ if (!isNonNegativeSafeInteger(settings.earlyFlakeDetectionFaultyThreshold) ||
154
+ settings.earlyFlakeDetectionFaultyThreshold > 100) {
155
+ return false
156
+ }
157
+ return settings.testManagementAttemptToFixRetries === undefined ||
158
+ isValidRetryCount(settings.testManagementAttemptToFixRetries)
58
159
  }
59
160
 
60
161
  const GIT_UPLOAD_TIMEOUT = 60_000 // 60 seconds
61
162
  const CAN_USE_CI_VIS_PROTOCOL_TIMEOUT = GIT_UPLOAD_TIMEOUT
62
163
  const MAX_COVERAGE_REPORT_FLAGS = 32
63
- const FINAL_FLUSH_TIMEOUT = 10_000
64
- const FINAL_FLUSH_FALLBACK_DELAY = 100
164
+ // Give a 200 MiB background video five minutes; final flush still caps shutdown at 60 seconds.
165
+ const BACKGROUND_VIDEO_UPLOAD_TIMEOUT = 5 * FINAL_FLUSH_TIMEOUT
65
166
 
66
167
  /**
67
- * @returns {Error}
168
+ * @param {string} tags
169
+ * @param {string} key
170
+ * @param {string | undefined} value
68
171
  */
69
- function createFinalFlushTimeoutError () {
70
- const error = new Error('Timed out waiting for Test Optimization to flush')
71
- error.code = 'ERR_DD_TEST_OPTIMIZATION_FLUSH_TIMEOUT'
72
- return error
73
- }
74
-
75
172
  function appendLogTag (tags, key, value) {
76
- if (value !== undefined) {
77
- tags.push(`${key}:${value}`)
78
- }
173
+ if (value === undefined) return tags
174
+
175
+ const tag = `${key}:${value}`
176
+ return tags ? `${tags},${tag}` : tag
79
177
  }
80
178
 
81
179
  function getLogTags (logMessage, { env, version }, gitRepositoryUrl, gitCommitSha) {
82
- const tags = []
180
+ let tags = ''
83
181
  if (Array.isArray(logMessage.ddtags)) {
84
- for (const tag of logMessage.ddtags) {
85
- tags.push(tag)
86
- }
182
+ tags = logMessage.ddtags.join(',')
87
183
  } else if (logMessage.ddtags) {
88
- for (const tag of logMessage.ddtags.split(',')) {
89
- tags.push(tag)
90
- }
184
+ tags = logMessage.ddtags
91
185
  }
92
186
 
93
- appendLogTag(tags, 'env', env)
94
- appendLogTag(tags, 'version', version)
95
- appendLogTag(tags, 'debugger_version', tracerVersion)
96
- appendLogTag(tags, 'host_name', hostname)
97
- appendLogTag(tags, GIT_COMMIT_SHA, gitCommitSha)
98
- appendLogTag(tags, GIT_REPOSITORY_URL, gitRepositoryUrl)
187
+ tags = appendLogTag(tags, 'env', env)
188
+ tags = appendLogTag(tags, 'version', version)
189
+ tags = appendLogTag(tags, 'debugger_version', tracerVersion)
190
+ tags = appendLogTag(tags, 'host_name', hostname)
191
+ tags = appendLogTag(tags, GIT_COMMIT_SHA, gitCommitSha)
192
+ tags = appendLogTag(tags, GIT_REPOSITORY_URL, gitRepositoryUrl)
99
193
 
100
- return tags.join(',')
194
+ return tags
101
195
  }
102
196
 
103
197
  class CiVisibilityExporter extends BufferingExporter {
104
198
  #finalFlush
105
199
  #deferredTestSessionTraces = []
106
- #pendingScreenshotUploads = new Set()
107
- #screenshotFlushWaiters = new Set()
200
+ #pendingMediaUploads = new Set()
201
+ #mediaFlushWaiters = new Set()
108
202
  #deferredTestSuiteSpans = new Map()
203
+ #gitUploadTimeoutId
109
204
 
110
205
  constructor (config, options = {}) {
111
206
  super(config)
@@ -131,11 +226,8 @@ class CiVisibilityExporter extends BufferingExporter {
131
226
 
132
227
  this._isTestFailureScreenshotsEnabled =
133
228
  Boolean(config?.testOptimization?.DD_TEST_FAILURE_SCREENSHOTS_ENABLED)
134
-
135
- const gitUploadTimeoutId = setTimeout(() => {
136
- this._resolveGit(new Error('Timeout while uploading git metadata'))
137
- }, GIT_UPLOAD_TIMEOUT)
138
- gitUploadTimeoutId.unref?.()
229
+ this._isTestFailureVideosEnabled =
230
+ Boolean(config?.testOptimization?.DD_TEST_FAILURE_VIDEOS_ENABLED)
139
231
 
140
232
  const canUseCiVisProtocolTimeoutId = setTimeout(() => {
141
233
  this._resolveCanUseCiVisProtocol(false)
@@ -144,7 +236,8 @@ class CiVisibilityExporter extends BufferingExporter {
144
236
 
145
237
  this._gitUploadPromise = new Promise(resolve => {
146
238
  this._resolveGit = (err) => {
147
- clearTimeout(gitUploadTimeoutId)
239
+ clearTimeout(this.#gitUploadTimeoutId)
240
+ this.#gitUploadTimeoutId = null
148
241
  resolve(err)
149
242
  }
150
243
  })
@@ -276,51 +369,142 @@ class CiVisibilityExporter extends BufferingExporter {
276
369
  const cachedLibraryConfig = this._testOptimizationHttpCache.readSettings()
277
370
  if (cachedLibraryConfig !== CACHE_MISS) {
278
371
  log.debug('Test Optimization HTTP cache settings found, skipping settings request')
279
- writeSettingsToCache(cachedLibraryConfig)
280
- this._libraryConfig = this.filterConfiguration(cachedLibraryConfig)
281
- const canUseCachedSkippableSuites = !this.shouldRequestSkippableSuites() ||
282
- this._testOptimizationHttpCache.hasValidSkippableSuites({
283
- testLevel: configuration.testLevel,
284
- isCoverageReportUploadEnabled: configuration.isCoverageReportUploadEnabled,
285
- })
286
- if (this._libraryConfig.requireGit && !canUseCachedSkippableSuites) {
287
- this.sendGitMetadata(repositoryUrl)
288
- } else {
289
- this._resolveGit()
290
- }
291
- return callback(null, this._libraryConfig)
372
+ return this._applyCachedSettings(cachedLibraryConfig, configuration, repositoryUrl, false, callback)
292
373
  }
293
374
 
294
375
  if (this._isTestOptimizationCacheOnly) {
295
376
  return callback(this._getCacheOnlyError('settings'), {})
296
377
  }
297
378
 
298
- this.sendGitMetadata(repositoryUrl)
299
- getLibraryConfigurationRequest(configuration, (err, libraryConfig) => {
379
+ // The settings request has a two-phase shape: when the backend returns
380
+ // `require_git`, we upload git metadata and re-request settings. Only the
381
+ // final (post-git-upload) configuration is cached, so the cross-process
382
+ // filesystem cache never serves a pre-git-upload response that would skip
383
+ // the git upload in other processes.
384
+ //
385
+ // The live-fetch and cache-hit paths resolve the git upload promise
386
+ // differently, so they must not share a single apply path:
387
+ // - Live fetch: `sendGitMetadata` is started in `_fetchLibraryConfigurationFromBackend`
388
+ // and resolves `_gitUploadPromise` itself (with the upload result). We must NOT call
389
+ // `_resolveGit()` here, or we would race the in-flight upload and mask its error.
390
+ // - Filesystem cache hit: no upload was started in this process, so `_applyCachedSettings`
391
+ // resolves the git promise locally (starting an upload only if the cached config still
392
+ // requires git and we lack valid cached skippable suites).
393
+ let liveFetchStarted = false
394
+ const fsCacheKey = buildSettingsCacheKey(configuration)
395
+ const fetchSettings = (activeCacheKey, done) => {
396
+ liveFetchStarted = true
397
+ this._fetchLibraryConfigurationFromBackend(configuration, repositoryUrl, activeCacheKey, done)
398
+ }
399
+ const applySettings = (err, libraryConfig) => {
300
400
  /**
301
401
  * **Important**: this._libraryConfig remains empty in testing frameworks
302
402
  * where the tests run in a subprocess, like Jest,
303
403
  * because `getLibraryConfiguration` is called only once in the main process.
304
404
  */
305
- this._libraryConfig = this.filterConfiguration(libraryConfig)
306
-
307
405
  if (err) {
308
- callback(err, {})
309
- } else if (libraryConfig?.requireGit) {
310
- // If the backend requires git, we'll wait for the upload to finish and request settings again
311
- this._gitUploadPromise.then(gitUploadError => {
312
- if (gitUploadError) {
313
- return callback(gitUploadError, {})
406
+ return callback(err, {})
407
+ }
408
+ if (liveFetchStarted) {
409
+ // Live fetch: the git upload was already started and resolves
410
+ // `_gitUploadPromise` itself. Do not call `_resolveGit()`.
411
+ writeSettingsToCache(libraryConfig)
412
+ this._libraryConfig = this.filterConfiguration(libraryConfig)
413
+ return callback(null, this._libraryConfig)
414
+ }
415
+ // Filesystem cache hit: no git upload was started in this process.
416
+ if (!isValidCachedSettings(libraryConfig)) {
417
+ // A syntactically valid cache file with an invalid settings payload is corrupt.
418
+ // Settings never writes such a value, so this is external corruption. Remove the
419
+ // file and fall back to the backend so we never serve a garbage config that would
420
+ // crash filterConfiguration or silently disable every feature.
421
+ try {
422
+ fs.unlinkSync(getCachePath(fsCacheKey))
423
+ } catch (err) {
424
+ if (err.code !== 'ENOENT') {
425
+ return fetchSettings(null, applySettings)
314
426
  }
315
- getLibraryConfigurationRequest(configuration, (err, finalLibraryConfig) => {
316
- this._libraryConfig = this.filterConfiguration(finalLibraryConfig)
317
- callback(err, this._libraryConfig)
318
- })
319
- })
320
- } else {
321
- callback(null, this._libraryConfig)
427
+ }
428
+ liveFetchStarted = false
429
+ return withCache(fsCacheKey, fetchSettings, applySettings, SETTINGS_CACHE_TTL_MS)
322
430
  }
431
+ this._applyCachedSettings(libraryConfig, configuration, repositoryUrl, true, callback)
432
+ }
433
+ withCache(fsCacheKey, fetchSettings, applySettings, SETTINGS_CACHE_TTL_MS)
434
+ })
435
+ }
436
+
437
+ /**
438
+ * Applies a resolved (cached or freshly fetched) library configuration: writes it to the
439
+ * shared settings cache, filters it through local kill switches, and resolves the git
440
+ * upload promise according to whether git metadata still needs to be uploaded.
441
+ *
442
+ * @param {Record<string, unknown>} settings - Resolved library configuration.
443
+ * @param {object} configuration - Request configuration used to evaluate cached skippable suites.
444
+ * @param {string} repositoryUrl - Repository URL for git metadata upload.
445
+ * @param {boolean} isFilesystemCache - Whether settings came from the cross-process cache.
446
+ * @param {Function} callback - Completion callback.
447
+ * @returns {void}
448
+ */
449
+ _applyCachedSettings (settings, configuration, repositoryUrl, isFilesystemCache, callback) {
450
+ writeSettingsToCache(settings)
451
+ this._libraryConfig = this.filterConfiguration(settings)
452
+ const canUseCachedSkippableSuites = !this.shouldRequestSkippableSuites() ||
453
+ this._testOptimizationHttpCache.hasValidSkippableSuites({
454
+ testLevel: configuration.testLevel,
455
+ isCoverageReportUploadEnabled: configuration.isCoverageReportUploadEnabled,
323
456
  })
457
+ if (!canUseCachedSkippableSuites && (this._libraryConfig.requireGit || isFilesystemCache)) {
458
+ this.sendGitMetadata(repositoryUrl)
459
+ } else {
460
+ this._resolveGit()
461
+ }
462
+ callback(null, this._libraryConfig)
463
+ }
464
+
465
+ /**
466
+ * Fetches library configuration from the backend, performing the two-phase
467
+ * `require_git` re-request, and writes only the final configuration to the
468
+ * filesystem cache when this process owns the cache lock.
469
+ *
470
+ * @param {object} configuration - Request configuration for the settings endpoint.
471
+ * @param {string} repositoryUrl - Repository URL for git metadata upload.
472
+ * @param {string|null} cacheKey - Filesystem cache key when this process owns the lock, null otherwise.
473
+ * @param {Function} done - Completion callback.
474
+ * @returns {void}
475
+ */
476
+ _fetchLibraryConfigurationFromBackend (configuration, repositoryUrl, cacheKey, done) {
477
+ this.sendGitMetadata(repositoryUrl)
478
+ getLibraryConfigurationRequest(configuration, (err, libraryConfig) => {
479
+ // Mirror the original live path: keep the phase-1 config on `_libraryConfig` even
480
+ // before the git upload resolves, so `shouldRequestSkippableSuites()` and the
481
+ // skippable path's `_gitUploadPromise` await behave identically to the uncached flow.
482
+ this._libraryConfig = this.filterConfiguration(libraryConfig)
483
+ if (err) {
484
+ return done(err, libraryConfig)
485
+ }
486
+ if (libraryConfig?.requireGit) {
487
+ // If the backend requires git, wait for the upload to finish and request settings again
488
+ this._gitUploadPromise.then(gitUploadError => {
489
+ if (gitUploadError) {
490
+ return done(gitUploadError, libraryConfig)
491
+ }
492
+ getLibraryConfigurationRequest(configuration, (finalErr, finalLibraryConfig) => {
493
+ if (finalErr) {
494
+ // Match the original live path: reset `_libraryConfig` from the final
495
+ // (failed) response so stale phase-1 feature flags don't stay installed.
496
+ // On error `finalLibraryConfig` is undefined, so this resolves to empty settings.
497
+ this._libraryConfig = this.filterConfiguration(finalLibraryConfig)
498
+ return done(finalErr, finalLibraryConfig)
499
+ }
500
+ writeToCache(cacheKey, finalLibraryConfig)
501
+ done(null, finalLibraryConfig)
502
+ })
503
+ })
504
+ } else {
505
+ writeToCache(cacheKey, libraryConfig)
506
+ done(null, libraryConfig)
507
+ }
324
508
  })
325
509
  }
326
510
 
@@ -387,6 +571,13 @@ class CiVisibilityExporter extends BufferingExporter {
387
571
  this._resolveGit()
388
572
  return
389
573
  }
574
+ if (this.#gitUploadTimeoutId === null) return
575
+ if (this.#gitUploadTimeoutId === undefined) {
576
+ this.#gitUploadTimeoutId = setTimeout(() => {
577
+ this._resolveGit(new Error('Timeout while uploading git metadata'))
578
+ }, GIT_UPLOAD_TIMEOUT)
579
+ this.#gitUploadTimeoutId.unref?.()
580
+ }
390
581
  this._canUseCiVisProtocolPromise.then((canUseCiVisProtocol) => {
391
582
  if (!canUseCiVisProtocol) {
392
583
  return
@@ -409,34 +600,7 @@ class CiVisibilityExporter extends BufferingExporter {
409
600
 
410
601
  export (trace) {
411
602
  this.#resetFinalFlush()
412
-
413
- if (this.#deferredTestSuiteSpans.size === 0) {
414
- this.#exportTrace(trace)
415
- return
416
- }
417
-
418
- let hasDeferredTestSuiteSpan = false
419
- let immediateTrace
420
- for (let index = 0; index < trace.length; index++) {
421
- const formattedSpan = trace[index]
422
- const spanId = formattedSpan.span_id?.toString()
423
- const deferredTestSuiteSpan = this.#deferredTestSuiteSpans.get(spanId)
424
- if (deferredTestSuiteSpan) {
425
- if (!hasDeferredTestSuiteSpan && index > 0) immediateTrace = trace.slice(0, index)
426
- hasDeferredTestSuiteSpan = true
427
- if (deferredTestSuiteSpan.emitted || (this._isInitialized && !this.canReportSessionTraces())) {
428
- this.#deferredTestSuiteSpans.delete(spanId)
429
- } else {
430
- deferredTestSuiteSpan.formattedSpan = formattedSpan
431
- }
432
- } else if (hasDeferredTestSuiteSpan) {
433
- immediateTrace ??= []
434
- immediateTrace.push(formattedSpan)
435
- }
436
- }
437
-
438
- if (!hasDeferredTestSuiteSpan) this.#exportTrace(trace)
439
- else if (immediateTrace) this.#exportTrace(immediateTrace)
603
+ this.#exportTrace(trace)
440
604
  }
441
605
 
442
606
  /**
@@ -453,6 +617,11 @@ class CiVisibilityExporter extends BufferingExporter {
453
617
  }
454
618
  const isTestSessionTrace = getIsTestSessionTrace(trace)
455
619
  if (!this.canReportSessionTraces() && isTestSessionTrace) {
620
+ const testTrace = []
621
+ for (const span of trace) {
622
+ if (!isTestSessionEvent(span)) testTrace.push(span)
623
+ }
624
+ if (testTrace.length > 0) this._export(testTrace)
456
625
  return
457
626
  }
458
627
  if (this._export(trace, undefined, undefined, isTestSessionTrace) === false && isTestSessionTrace) {
@@ -460,115 +629,6 @@ class CiVisibilityExporter extends BufferingExporter {
460
629
  }
461
630
  }
462
631
 
463
- /**
464
- * Retains a completed suite until finalization so a later framework error can still update it.
465
- *
466
- * @param {import('../../opentracing/span')} testSuiteSpan
467
- * @returns {void}
468
- */
469
- deferTestSuiteSpan (testSuiteSpan) {
470
- this.#resetFinalFlush()
471
- this.#deferredTestSuiteSpans.set(testSuiteSpan.context()._spanId.toString(), {
472
- span: testSuiteSpan,
473
- formattedSpan: undefined,
474
- emitted: false,
475
- })
476
- }
477
-
478
- /**
479
- * Retains formatted test suite events received from test framework workers until finalization.
480
- *
481
- * @param {Array<object>} trace
482
- * @returns {void}
483
- */
484
- exportTraceWithDeferredTestSuite (trace) {
485
- for (const formattedSpan of trace) {
486
- if (formattedSpan.type !== 'test_suite_end') continue
487
-
488
- this.#deferredTestSuiteSpans.set(formattedSpan.span_id.toString(), {
489
- span: undefined,
490
- formattedSpan,
491
- emitted: false,
492
- })
493
- }
494
- this.export(trace)
495
- }
496
-
497
- /**
498
- * Applies a late framework error to every completed suite retained by the current finalization boundary.
499
- *
500
- * @param {Error} error
501
- * @returns {void}
502
- */
503
- setDeferredTestSuiteError (error) {
504
- for (const { span, formattedSpan, emitted } of this.#deferredTestSuiteSpans.values()) {
505
- if (emitted) continue
506
- if (span) {
507
- span.setTag(TEST_STATUS, 'fail')
508
- span.setTag('error', error)
509
- } else if (formattedSpan) {
510
- formattedSpan.meta[TEST_STATUS] = 'fail'
511
- spanFormat.addError(formattedSpan, error)
512
- }
513
- }
514
- }
515
-
516
- /**
517
- * Appends a completed suite while preserving the ordinary writer call shape outside finalization.
518
- *
519
- * @param {object} formattedSpan
520
- * @param {{ deadline?: number }} [options]
521
- * @returns {boolean|undefined}
522
- */
523
- #appendDeferredTestSuiteSpan (formattedSpan, options) {
524
- const appended = options === undefined
525
- ? this._writer.append([formattedSpan])
526
- : this._writer.append([formattedSpan], options)
527
-
528
- if (appended !== false && this._config.isCiVisibility) {
529
- incrementCountMetric(TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION)
530
- }
531
- return appended
532
- }
533
-
534
- /**
535
- * Appends completed suites before their module and session parents are exported.
536
- *
537
- * @param {{ deadline?: number }} [options]
538
- * @returns {void}
539
- */
540
- exportDeferredTestSuiteSpans (options) {
541
- if (!this._writer || !this.canReportSessionTraces()) return
542
-
543
- for (const [spanId, deferredTestSuiteSpan] of this.#deferredTestSuiteSpans) {
544
- const { span, formattedSpan, emitted } = deferredTestSuiteSpan
545
- if (emitted) continue
546
- const updatedSpan = span && spanFormat(span)
547
- if (span && formattedSpan) {
548
- formattedSpan.error = updatedSpan.error
549
- Object.assign(formattedSpan.meta, updatedSpan.meta)
550
- Object.assign(formattedSpan.metrics, updatedSpan.metrics)
551
- if (this.#appendDeferredTestSuiteSpan(formattedSpan, options) === false) continue
552
- this.#deferredTestSuiteSpans.delete(spanId)
553
- } else if (formattedSpan) {
554
- if (this.#appendDeferredTestSuiteSpan(formattedSpan, options) === false) continue
555
- this.#deferredTestSuiteSpans.delete(spanId)
556
- } else {
557
- if (this.#appendDeferredTestSuiteSpan(updatedSpan, options) === false) continue
558
- deferredTestSuiteSpan.emitted = true
559
- }
560
- }
561
- }
562
-
563
- /**
564
- * Discards completed suites that the selected transport cannot deliver.
565
- *
566
- * @returns {void}
567
- */
568
- resetDeferredTestSuiteSpans () {
569
- this.#deferredTestSuiteSpans.clear()
570
- }
571
-
572
632
  /**
573
633
  * Retries session, module, and suite traces rejected by writer backpressure within the final deadline.
574
634
  *
@@ -636,11 +696,17 @@ class CiVisibilityExporter extends BufferingExporter {
636
696
  }
637
697
 
638
698
  this.#resetFinalFlush()
639
- this._export(
699
+ const appended = this._export(
640
700
  this.formatLogMessage(testEnvironmentMetadata, logMessage),
641
701
  this._logsWriter,
642
702
  '_logsTimer'
643
703
  )
704
+ if (appended === false) {
705
+ incrementCountMetric(
706
+ TELEMETRY_ENDPOINT_PAYLOAD_DROPPED,
707
+ { endpoint: 'di_logs', statusCode: undefined, errorType: 'encoder_limit' }
708
+ )
709
+ }
644
710
  }
645
711
 
646
712
  flush (done) {
@@ -657,7 +723,8 @@ class CiVisibilityExporter extends BufferingExporter {
657
723
  if (isFinalFlush && !this._isInitialized &&
658
724
  this._traceBuffer.length === 0 && this._coverageBuffer.length === 0 &&
659
725
  this.#deferredTestSuiteSpans.size === 0 && this.#deferredTestSessionTraces.length === 0 &&
660
- this.#pendingScreenshotUploads.size === 0) {
726
+ this.#pendingMediaUploads.size === 0) {
727
+ this._initializationRequest?.controller.abort()
661
728
  onDone()
662
729
  return
663
730
  }
@@ -674,6 +741,9 @@ class CiVisibilityExporter extends BufferingExporter {
674
741
  const deadline = isFinalFlush ? Date.now() + FINAL_FLUSH_TIMEOUT : undefined
675
742
  let hasCompleted = false
676
743
  let initializationTimeoutId
744
+ let mediaTimeoutId
745
+ let writersFlushed = false
746
+ let writerFlushError
677
747
 
678
748
  const fallbackTimeoutId = isFinalFlush
679
749
  ? setTimeout(() => {
@@ -686,7 +756,8 @@ class CiVisibilityExporter extends BufferingExporter {
686
756
  hasCompleted = true
687
757
  clearTimeout(fallbackTimeoutId)
688
758
  clearTimeout(initializationTimeoutId)
689
- this.#screenshotFlushWaiters.delete(flushWriters)
759
+ clearTimeout(mediaTimeoutId)
760
+ this.#mediaFlushWaiters.delete(completeAfterMedia)
690
761
  if (error) log.error('Error flushing Test Optimization data', error)
691
762
  if (!isFinalFlush) {
692
763
  onDone(error)
@@ -706,15 +777,23 @@ class CiVisibilityExporter extends BufferingExporter {
706
777
  }
707
778
  }
708
779
 
709
- const flushWriters = () => {
710
- if (isFinalFlush && this.#pendingScreenshotUploads.size !== 0) {
711
- this.#screenshotFlushWaiters.add(flushWriters)
780
+ const completeAfterMedia = () => {
781
+ if (writersFlushed) complete(writerFlushError)
782
+ }
783
+
784
+ const onWritersFlushed = (error) => {
785
+ writersFlushed = true
786
+ writerFlushError = error
787
+ if (isFinalFlush && this.#pendingMediaUploads.size !== 0) {
788
+ this.#mediaFlushWaiters.add(completeAfterMedia)
712
789
  return
713
790
  }
791
+ complete(error)
792
+ }
714
793
 
794
+ const flushWriters = () => {
715
795
  const options = deadline === undefined ? undefined : { deadline }
716
796
  if (isFinalFlush) {
717
- this.exportDeferredTestSuiteSpans(options)
718
797
  this.#exportDeferredTestSessionTraces(options)
719
798
  }
720
799
 
@@ -728,24 +807,31 @@ class CiVisibilityExporter extends BufferingExporter {
728
807
  let flushError
729
808
 
730
809
  if (remaining === 0) {
731
- complete()
810
+ onWritersFlushed()
732
811
  return
733
812
  }
734
813
 
735
814
  const onFlushComplete = (error) => {
736
815
  flushError ||= error
737
816
  remaining -= 1
738
- if (remaining === 0) complete(flushError)
817
+ if (remaining === 0) onWritersFlushed(flushError)
739
818
  }
740
819
 
741
820
  for (const writer of writers) writer.flush(onFlushComplete, options)
742
821
  }
743
822
 
823
+ if (isFinalFlush && this.#pendingMediaUploads.size !== 0) {
824
+ mediaTimeoutId = setTimeout(() => {
825
+ const error = createFinalFlushTimeoutError()
826
+ for (const controller of this.#pendingMediaUploads) controller.abort(error)
827
+ }, Math.max(0, deadline - Date.now()))
828
+ mediaTimeoutId.unref?.()
829
+ }
830
+
744
831
  if (isFinalFlush && this._initializationRequest) {
745
832
  const initializationRequest = this._initializationRequest
746
- const { controller, options } = initializationRequest
833
+ const { controller } = initializationRequest
747
834
  initializationRequest.finalFlush = finalFlush
748
- options.deadline = deadline
749
835
  initializationTimeoutId = setTimeout(() => {
750
836
  const error = createFinalFlushTimeoutError()
751
837
  if (initializationRequest.finalFlush === finalFlush) controller.abort(error)
@@ -853,25 +939,104 @@ class CiVisibilityExporter extends BufferingExporter {
853
939
  return Boolean(this._testScreenshotUploadUrl) && this._isTestFailureScreenshotsEnabled
854
940
  }
855
941
 
942
+ /**
943
+ * Returns whether the exporter can upload test failure videos.
944
+ *
945
+ * @returns {boolean}
946
+ */
947
+ canUploadTestVideos () {
948
+ return Boolean(this._testScreenshotUploadUrl) && this._isTestFailureVideosEnabled
949
+ }
950
+
856
951
  /**
857
952
  * Uploads a single test screenshot to the Test Optimization media intake.
858
953
  *
859
954
  * @param {object} options - Upload options
860
- * @param {string} options.filePath - Path to the screenshot file
955
+ * @param {string} [options.filePath] - Path to the screenshot file
956
+ * @param {Buffer} [options.content] - In-memory screenshot content
861
957
  * @param {string} options.traceId - Test trace id used as the screenshot key
862
958
  * @param {string} options.idempotencyKey - Stable per-artifact key, reused on retry
863
959
  * @param {number} options.capturedAtMs - Capture time in epoch milliseconds
864
960
  * @param {AbortSignal} [options.signal] - Additional signal used to cancel the upload
865
961
  * @param {Function} callback - Callback function (err)
866
962
  */
867
- uploadTestScreenshot ({ filePath, traceId, idempotencyKey, capturedAtMs, signal }, callback) {
963
+ uploadTestScreenshot ({ filePath, content, traceId, idempotencyKey, capturedAtMs, signal }, callback) {
868
964
  if (!this._testScreenshotUploadUrl) {
869
965
  return callback(new Error('Test screenshot upload URL not configured'))
870
966
  }
871
967
 
968
+ this.#uploadTestMedia(uploadTestScreenshotRequest, {
969
+ filePath,
970
+ content,
971
+ traceId,
972
+ idempotencyKey,
973
+ capturedAtMs,
974
+ signal,
975
+ }, callback)
976
+ }
977
+
978
+ /**
979
+ * Uploads a test-scoped failure video to the Test Optimization media intake.
980
+ *
981
+ * @param {object} options - Upload options
982
+ * @param {string} options.filePath - Path to the video file
983
+ * @param {string} options.traceId - Test trace id used as the media key
984
+ * @param {string} options.idempotencyKey - Stable per-artifact key, reused on retry
985
+ * @param {number} options.capturedAtMs - Capture time in epoch milliseconds
986
+ * @param {AbortSignal} [options.signal] - Additional signal used to cancel the upload
987
+ * @param {Function} callback - Callback function (err)
988
+ * @returns {void}
989
+ */
990
+ uploadTestVideo (options, callback) {
991
+ if (!this._testScreenshotUploadUrl) {
992
+ return callback(new Error('Test video upload URL not configured'))
993
+ }
994
+
995
+ this.#uploadTestMedia(uploadTestVideoRequest, options, callback, BACKGROUND_VIDEO_UPLOAD_TIMEOUT)
996
+ }
997
+
998
+ /**
999
+ * Uploads a suite-scoped failure video to the Test Optimization media intake.
1000
+ *
1001
+ * @param {object} options - Upload options
1002
+ * @param {string} options.filePath - Path to the video file
1003
+ * @param {string} options.testSessionId - Test session id used as the media key
1004
+ * @param {string} options.testSuiteId - Test suite id used as the media key
1005
+ * @param {string} options.idempotencyKey - Stable per-artifact key, reused on retry
1006
+ * @param {number} options.capturedAtMs - Capture time in epoch milliseconds
1007
+ * @param {AbortSignal} [options.signal] - Additional signal used to cancel the upload
1008
+ * @param {Function} callback - Callback function (err)
1009
+ * @returns {void}
1010
+ */
1011
+ uploadTestSuiteVideo (options, callback) {
1012
+ if (!this._testScreenshotUploadUrl) {
1013
+ return callback(new Error('Test suite video upload URL not configured'))
1014
+ }
1015
+
1016
+ this.#uploadTestMedia(uploadTestSuiteVideoRequest, options, callback, BACKGROUND_VIDEO_UPLOAD_TIMEOUT)
1017
+ }
1018
+
1019
+ /**
1020
+ * Starts and tracks one media upload so final flush waits for it.
1021
+ *
1022
+ * @param {Function} uploadRequest - Media request function
1023
+ * @param {object} uploadOptions - Media request options
1024
+ * @param {AbortSignal} [uploadOptions.signal] - Additional signal used to cancel the upload
1025
+ * @param {Function} callback - Callback function (err)
1026
+ * @param {number} [timeoutMs] - Maximum background upload duration
1027
+ * @returns {void}
1028
+ */
1029
+ #uploadTestMedia (uploadRequest, uploadOptions, callback, timeoutMs = FINAL_FLUSH_TIMEOUT) {
1030
+ const { signal } = uploadOptions
1031
+
1032
+ if (signal?.aborted) {
1033
+ const error = signal.reason || Object.assign(new Error('Test screenshot upload aborted'), { code: 'ABORT_ERR' })
1034
+ callback(error)
1035
+ return
1036
+ }
872
1037
  this.#resetFinalFlush()
1038
+ const deadline = Date.now() + timeoutMs
873
1039
  const controller = new AbortController()
874
- const deadline = Date.now() + FINAL_FLUSH_TIMEOUT
875
1040
  let settled = false
876
1041
  const complete = (error) => {
877
1042
  if (settled) return
@@ -882,10 +1047,10 @@ class CiVisibilityExporter extends BufferingExporter {
882
1047
  try {
883
1048
  callback(error)
884
1049
  } finally {
885
- this.#pendingScreenshotUploads.delete(controller)
886
- if (this.#pendingScreenshotUploads.size === 0) {
887
- const waiters = [...this.#screenshotFlushWaiters]
888
- this.#screenshotFlushWaiters.clear()
1050
+ this.#pendingMediaUploads.delete(controller)
1051
+ if (this.#pendingMediaUploads.size === 0) {
1052
+ const waiters = [...this.#mediaFlushWaiters]
1053
+ this.#mediaFlushWaiters.clear()
889
1054
  for (const waiter of waiters) queueMicrotask(waiter)
890
1055
  }
891
1056
  }
@@ -896,25 +1061,23 @@ class CiVisibilityExporter extends BufferingExporter {
896
1061
  complete(error)
897
1062
  }
898
1063
 
899
- this.#pendingScreenshotUploads.add(controller)
900
- signal?.addEventListener('abort', onAbort, { once: true })
901
1064
  const timeoutId = setTimeout(() => {
902
1065
  const error = createFinalFlushTimeoutError()
903
1066
  controller.abort(error)
904
1067
  complete(error)
905
- }, FINAL_FLUSH_TIMEOUT)
1068
+ }, Math.max(0, deadline - Date.now()))
906
1069
  timeoutId.unref?.()
907
1070
 
1071
+ this.#pendingMediaUploads.add(controller)
1072
+ signal?.addEventListener('abort', onAbort, { once: true })
1073
+
908
1074
  if (signal?.aborted) {
909
1075
  onAbort()
910
1076
  return
911
1077
  }
912
1078
 
913
- uploadTestScreenshotRequest({
914
- filePath,
915
- traceId,
916
- idempotencyKey,
917
- capturedAtMs,
1079
+ uploadRequest({
1080
+ ...uploadOptions,
918
1081
  url: this._testScreenshotUploadUrl,
919
1082
  isEvpProxy: !!this._isUsingEvpProxy,
920
1083
  evpProxyPrefix: this.evpProxyPrefix,