dd-trace 6.3.0 → 6.5.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 (207) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/index.electron.js +3 -0
  52. package/init.js +18 -0
  53. package/initialize.mjs +1 -1
  54. package/loader-hook.mjs +28 -18
  55. package/openfeature.d.ts +1 -0
  56. package/openfeature.js +4 -0
  57. package/package.json +15 -8
  58. package/packages/datadog-instrumentations/src/ai.js +37 -26
  59. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  60. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  61. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  62. package/packages/datadog-instrumentations/src/cucumber.js +24 -10
  63. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  64. package/packages/datadog-instrumentations/src/express.js +20 -2
  65. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  66. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  68. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  69. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  70. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  71. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  72. package/packages/datadog-instrumentations/src/jest.js +36 -4
  73. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  74. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  75. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  76. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  77. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  78. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  79. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  80. package/packages/datadog-instrumentations/src/next.js +24 -0
  81. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  82. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  83. package/packages/datadog-instrumentations/src/pg.js +2 -2
  84. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  85. package/packages/datadog-instrumentations/src/process.js +3 -0
  86. package/packages/datadog-instrumentations/src/router.js +191 -106
  87. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  88. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  89. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  90. package/packages/datadog-instrumentations/src/ws.js +2 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  92. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  94. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  95. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  96. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
  97. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
  99. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  100. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  101. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  102. package/packages/datadog-plugin-graphql/src/execute.js +177 -59
  103. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  104. package/packages/datadog-plugin-graphql/src/request.js +32 -1
  105. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  106. package/packages/datadog-plugin-graphql/src/validate.js +15 -3
  107. package/packages/datadog-plugin-http2/src/client.js +1 -1
  108. package/packages/datadog-plugin-http2/src/server.js +38 -6
  109. package/packages/datadog-plugin-jest/src/util.js +21 -6
  110. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  111. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  112. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  113. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  114. package/packages/datadog-plugin-router/src/index.js +11 -2
  115. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  116. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  117. package/packages/datadog-plugin-ws/src/close.js +2 -1
  118. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  119. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  120. package/packages/datadog-plugin-ws/src/server.js +2 -1
  121. package/packages/dd-trace/index.electron.js +3 -0
  122. package/packages/dd-trace/index.js +2 -37
  123. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  125. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  126. package/packages/dd-trace/src/azure_metadata.js +2 -1
  127. package/packages/dd-trace/src/bootstrap.js +39 -0
  128. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  129. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  132. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  135. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  137. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  138. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
  139. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
  140. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  141. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  142. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  143. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  144. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  145. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
  146. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  147. package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
  148. package/packages/dd-trace/src/config/helper.js +1 -0
  149. package/packages/dd-trace/src/config/index.js +10 -0
  150. package/packages/dd-trace/src/config/supported-configurations.json +83 -0
  151. package/packages/dd-trace/src/constants.js +7 -0
  152. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  153. package/packages/dd-trace/src/exporter.js +2 -0
  154. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  155. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  156. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  157. package/packages/dd-trace/src/feature-registry.js +29 -0
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  159. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  160. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  161. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  162. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  163. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  164. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  165. package/packages/dd-trace/src/llmobs/index.js +10 -2
  166. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  167. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  168. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  169. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  170. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  171. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  172. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  173. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  174. package/packages/dd-trace/src/llmobs/tagger.js +79 -7
  175. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  176. package/packages/dd-trace/src/llmobs/util.js +32 -0
  177. package/packages/dd-trace/src/noop/proxy.js +5 -4
  178. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  179. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  180. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  181. package/packages/dd-trace/src/openfeature/index.js +0 -2
  182. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  183. package/packages/dd-trace/src/openfeature/register.js +35 -0
  184. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  185. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  186. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  187. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  188. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  189. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  190. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  191. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  192. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  193. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  194. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  195. package/packages/dd-trace/src/plugins/util/test.js +65 -8
  196. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  197. package/packages/dd-trace/src/proxy.js +99 -10
  198. package/packages/dd-trace/src/span_processor.js +5 -0
  199. package/packages/dd-trace/src/standalone/index.js +0 -36
  200. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  201. package/packages/dd-trace/src/util.js +26 -0
  202. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  203. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,369 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('node:crypto')
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+
7
+ const { MAX_OUTPUT_BYTES, msgpackToJson } = require('./msgpack-to-json')
8
+ const { projectTestCyclePayload } = require('./payload-projection')
9
+
10
+ const MAX_OUTPUT_FILES = 10_000
11
+ const MAX_SAMPLED_TESTS = 8
12
+ const MAX_SUMMARY_ERRORS = 20
13
+ const SUMMARY_PREFIX = 'DD_TEST_OPTIMIZATION_VALIDATION_V1 '
14
+
15
+ let payloadFileSequence = 0
16
+
17
+ class CiValidationSink {
18
+ #bytesWritten = 0
19
+ #captureMode
20
+ #completionDirectory
21
+ #errors = []
22
+ #eventsObserved = 0
23
+ #eventsRetained = 0
24
+ #fileCount = 0
25
+ #inputs = Object.create(null)
26
+ #outputRoot
27
+ #outputRootDirectory
28
+ #payloadsDirectory
29
+ #processId = crypto.randomBytes(16).toString('hex')
30
+ #sampledLifecycle = new Map()
31
+ #sampledTests = []
32
+ #summaryWritten = false
33
+ #testsDirectory
34
+ #testsObserved = 0
35
+ #testPayloadFileCount = 0
36
+
37
+ /**
38
+ * Creates a bounded payload-file sink under a validator-owned output root.
39
+ *
40
+ * @param {string} outputRoot absolute validator-owned output directory
41
+ * @param {object} [options] sink options
42
+ * @param {'sample'|'strict'} [options.captureMode] evidence retention mode
43
+ */
44
+ constructor (outputRoot, { captureMode = 'strict' } = {}) {
45
+ if (typeof outputRoot !== 'string' || !path.isAbsolute(outputRoot)) {
46
+ throw new Error('Offline Test Optimization validation output root must be absolute.')
47
+ }
48
+
49
+ if (!['sample', 'strict'].includes(captureMode)) {
50
+ throw new Error('Offline Test Optimization validation capture mode is invalid.')
51
+ }
52
+ this.#captureMode = captureMode
53
+ this.#outputRoot = outputRoot
54
+ this.#outputRootDirectory = captureDirectory(outputRoot, 'output root')
55
+ const payloadsRoot = path.join(outputRoot, 'payloads')
56
+ this.#payloadsDirectory = createDirectory(outputRoot, this.#outputRootDirectory, payloadsRoot, 'payloads')
57
+ this.#testsDirectory = createDirectory(
58
+ payloadsRoot,
59
+ this.#payloadsDirectory,
60
+ path.join(payloadsRoot, 'tests'),
61
+ 'tests'
62
+ )
63
+ this.#completionDirectory = createDirectory(
64
+ outputRoot,
65
+ this.#outputRootDirectory,
66
+ path.join(outputRoot, 'completions'),
67
+ 'completions'
68
+ )
69
+ }
70
+
71
+ /**
72
+ * Converts and writes one Test Optimization payload using the Bazel-compatible JSON layout.
73
+ *
74
+ * @param {Buffer} payload encoded Test Optimization payload
75
+ */
76
+ writeTestCycle (payload) {
77
+ let decoded
78
+ try {
79
+ decoded = JSON.parse(msgpackToJson(payload).toString('utf8'))
80
+ } catch {
81
+ this.#fail('output_payload_decode_failed')
82
+ return
83
+ }
84
+
85
+ let projected
86
+ try {
87
+ projected = projectTestCyclePayload(decoded)
88
+ } catch {
89
+ this.#fail('output_payload_projection_failed')
90
+ return
91
+ }
92
+
93
+ this.#eventsObserved += projected.events.length
94
+ if (this.#captureMode === 'sample') {
95
+ this.#retainSample(projected.events)
96
+ return
97
+ }
98
+
99
+ const json = Buffer.from(JSON.stringify(projected))
100
+ if (this.#writePayloadFile(json)) {
101
+ this.#eventsRetained += projected.events.length
102
+ this.#testPayloadFileCount++
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Records whether a control-plane fixture was loaded from the filesystem cache.
108
+ *
109
+ * @param {string} name fixed fixture name
110
+ * @param {Error|undefined|null} error cache-load error
111
+ */
112
+ writeInputResult (name, error) {
113
+ if (error) this.#addError(`invalid_${name}`)
114
+ this.#inputs[name] = { status: error ? 'error' : 'loaded' }
115
+ }
116
+
117
+ /**
118
+ * Records an exporter failure in the bounded validation summary.
119
+ *
120
+ * @param {string} code stable failure code
121
+ */
122
+ recordError (code) {
123
+ this.#fail(code)
124
+ }
125
+
126
+ /**
127
+ * Emits this process's single bounded stderr summary for validator aggregation.
128
+ */
129
+ writeSummary () {
130
+ if (this.#summaryWritten) return
131
+ this.#summaryWritten = true
132
+ if (this.#captureMode === 'sample') this.#writeSampledEvents()
133
+ const summary = {
134
+ events: this.#eventsRetained,
135
+ payloadFiles: this.#testPayloadFileCount,
136
+ input: 'filesystem-cache',
137
+ inputs: this.#inputs,
138
+ errors: this.#errors,
139
+ }
140
+ if (!this.#writeCompletionRecord()) this.#fail('completion_write_failed')
141
+ process.stderr.write(`${SUMMARY_PREFIX}${JSON.stringify(summary)}\n`)
142
+ }
143
+
144
+ /**
145
+ * Retains bounded first/late test evidence and the latest lifecycle event of each type.
146
+ *
147
+ * @param {object[]} events projected events
148
+ */
149
+ #retainSample (events) {
150
+ for (const event of events) {
151
+ if (event.type !== 'test') {
152
+ this.#sampledLifecycle.set(event.type, event)
153
+ continue
154
+ }
155
+ this.#testsObserved++
156
+ if (this.#sampledTests.length < MAX_SAMPLED_TESTS) {
157
+ this.#sampledTests.push(event)
158
+ } else {
159
+ const lateIndex = Math.floor(MAX_SAMPLED_TESTS / 2) +
160
+ (this.#testsObserved % Math.ceil(MAX_SAMPLED_TESTS / 2))
161
+ this.#sampledTests[lateIndex] = event
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Persists the final bounded CI-replay sample after all process-local events have been observed.
168
+ */
169
+ #writeSampledEvents () {
170
+ const events = [...this.#sampledTests, ...this.#sampledLifecycle.values()]
171
+ if (events.length === 0) return
172
+ const payload = Buffer.from(JSON.stringify({ version: 1, events }))
173
+ if (this.#writePayloadFile(payload)) {
174
+ this.#eventsRetained = events.length
175
+ this.#testPayloadFileCount++
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Atomically publishes this process's bounded completion evidence.
181
+ *
182
+ * @returns {boolean} whether the record was published
183
+ */
184
+ #writeCompletionRecord () {
185
+ const directoryPath = path.join(this.#outputRoot, 'completions')
186
+ const filename = path.join(directoryPath, `completion-${this.#processId}.json`)
187
+ const temporary = `${filename}.tmp`
188
+ const completion = Buffer.from(JSON.stringify({
189
+ version: 1,
190
+ processId: this.#processId,
191
+ captureMode: this.#captureMode,
192
+ counts: {
193
+ eventsObserved: this.#eventsObserved,
194
+ eventsRetained: this.#eventsRetained,
195
+ payloadFiles: this.#testPayloadFileCount,
196
+ },
197
+ inputs: this.#inputs,
198
+ errors: this.#errors,
199
+ }))
200
+
201
+ try {
202
+ assertDirectoryUnchanged(this.#outputRoot, this.#outputRootDirectory, 'output root')
203
+ assertDirectoryUnchanged(directoryPath, this.#completionDirectory, 'completions')
204
+ writeNewFile(temporary, completion)
205
+ assertDirectoryUnchanged(directoryPath, this.#completionDirectory, 'completions')
206
+ fs.renameSync(temporary, filename)
207
+ return true
208
+ } catch {
209
+ removePartialFile(temporary, directoryPath, this.#completionDirectory)
210
+ return false
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Writes one completed JSON payload to a unique file.
216
+ *
217
+ * @param {Buffer} payload JSON payload
218
+ * @returns {boolean} whether the payload was written
219
+ */
220
+ #writePayloadFile (payload) {
221
+ if (this.#fileCount >= MAX_OUTPUT_FILES) {
222
+ this.#fail('output_file_limit_exceeded')
223
+ return false
224
+ }
225
+ if (this.#bytesWritten + payload.length > MAX_OUTPUT_BYTES) {
226
+ this.#fail('output_byte_limit_exceeded')
227
+ return false
228
+ }
229
+
230
+ const directoryPath = path.join(this.#outputRoot, 'payloads', 'tests')
231
+ const sequence = ++payloadFileSequence
232
+ const timestamp = BigInt(Date.now()) * 1_000_000n + process.hrtime.bigint() % 1_000_000n
233
+ const filename = path.join(
234
+ directoryPath,
235
+ `tests-${this.#processId}-${timestamp}-${process.pid}-${sequence}.json`
236
+ )
237
+ let writeFailed = false
238
+ try {
239
+ assertDirectoryUnchanged(this.#outputRoot, this.#outputRootDirectory, 'output root')
240
+ assertDirectoryUnchanged(path.join(this.#outputRoot, 'payloads'), this.#payloadsDirectory, 'payloads')
241
+ assertDirectoryUnchanged(directoryPath, this.#testsDirectory, 'tests')
242
+ writeNewFile(filename, payload)
243
+ } catch {
244
+ writeFailed = true
245
+ }
246
+ if (writeFailed) {
247
+ this.#fail('output_write_failed')
248
+ removePartialFile(filename, directoryPath, this.#testsDirectory)
249
+ return false
250
+ }
251
+
252
+ this.#bytesWritten += payload.length
253
+ this.#fileCount++
254
+ return true
255
+ }
256
+
257
+ /**
258
+ * Records a sink failure and makes the process unsuccessful.
259
+ *
260
+ * @param {string} code stable failure code
261
+ */
262
+ #fail (code) {
263
+ this.#addError(code)
264
+ process.exitCode = 1
265
+ }
266
+
267
+ /**
268
+ * Adds one unique bounded summary error.
269
+ *
270
+ * @param {string} code stable failure code
271
+ */
272
+ #addError (code) {
273
+ if (this.#errors.length >= MAX_SUMMARY_ERRORS || this.#errors.includes(code)) return
274
+ this.#errors.push(code)
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Writes one new private regular file and rejects hard links or non-files.
280
+ *
281
+ * @param {string} filename output filename
282
+ * @param {Buffer} payload output bytes
283
+ */
284
+ function writeNewFile (filename, payload) {
285
+ const flags = fs.constants.O_WRONLY |
286
+ fs.constants.O_CREAT |
287
+ fs.constants.O_EXCL |
288
+ (fs.constants.O_NOFOLLOW || 0)
289
+ const file = fs.openSync(filename, flags, 0o600)
290
+ try {
291
+ const stat = fs.fstatSync(file)
292
+ if (!stat.isFile() || stat.nlink !== 1) {
293
+ throw new Error('Offline Test Optimization output is not a private regular file.')
294
+ }
295
+ fs.writeFileSync(file, payload)
296
+ } finally {
297
+ fs.closeSync(file)
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Captures the identity of one existing non-symbolic directory.
303
+ *
304
+ * @param {string} directory directory path
305
+ * @param {string} label directory label
306
+ * @returns {{dev: number, ino: number}} stable directory identity
307
+ */
308
+ function captureDirectory (directory, label) {
309
+ const stat = fs.lstatSync(directory)
310
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
311
+ throw new Error(`Offline Test Optimization validation ${label} must be a regular directory.`)
312
+ }
313
+ return { dev: stat.dev, ino: stat.ino }
314
+ }
315
+
316
+ /**
317
+ * Creates or validates one child directory without accepting symbolic links.
318
+ *
319
+ * @param {string} parent parent directory path
320
+ * @param {{dev: number, ino: number}} parentIdentity expected parent identity
321
+ * @param {string} directory child directory path
322
+ * @param {string} label directory label
323
+ * @returns {{dev: number, ino: number}} stable child identity
324
+ */
325
+ function createDirectory (parent, parentIdentity, directory, label) {
326
+ assertDirectoryUnchanged(parent, parentIdentity, 'parent output')
327
+ try {
328
+ fs.mkdirSync(directory, { mode: 0o700 })
329
+ } catch (error) {
330
+ if (error.code !== 'EEXIST') throw error
331
+ }
332
+ return captureDirectory(directory, `${label} output directory`)
333
+ }
334
+
335
+ /**
336
+ * Rejects a directory that changed after sink construction.
337
+ *
338
+ * @param {string} directory directory path
339
+ * @param {{dev: number, ino: number}} identity expected directory identity
340
+ * @param {string} label directory label
341
+ */
342
+ function assertDirectoryUnchanged (directory, identity, label) {
343
+ const current = captureDirectory(directory, label)
344
+ if (current.dev !== identity.dev || current.ino !== identity.ino) {
345
+ throw new Error(`Offline Test Optimization validation ${label} changed during execution.`)
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Removes a partially written final path without following symbolic links.
351
+ *
352
+ * @param {string} filename partial payload path
353
+ * @param {string} directory expected parent directory
354
+ * @param {{dev: number, ino: number}} identity expected parent identity
355
+ */
356
+ function removePartialFile (filename, directory, identity) {
357
+ try {
358
+ assertDirectoryUnchanged(directory, identity, 'partial-file parent')
359
+ const stat = fs.lstatSync(filename)
360
+ if (stat.isFile() || stat.isSymbolicLink()) fs.unlinkSync(filename)
361
+ } catch {}
362
+ }
363
+
364
+ module.exports = {
365
+ CiValidationSink,
366
+ MAX_OUTPUT_BYTES,
367
+ MAX_OUTPUT_FILES,
368
+ SUMMARY_PREFIX,
369
+ }
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const { AgentlessCiVisibilityEncoder } = require('../../../encode/agentless-ci-visibility')
4
+
5
+ class CiValidationWriter {
6
+ /**
7
+ * Creates an offline writer that preserves CI Visibility event encoding.
8
+ *
9
+ * @param {object} options writer options
10
+ * @param {object} options.sink bounded validation sink
11
+ * @param {object} options.tags tracer tags
12
+ */
13
+ constructor ({ sink, tags }) {
14
+ const { 'runtime-id': runtimeId, env, service } = tags
15
+ this._sink = sink
16
+ this._encoder = new AgentlessCiVisibilityEncoder(this, { runtimeId, env, service })
17
+ }
18
+
19
+ /**
20
+ * Encodes a CI Visibility trace without using an HTTP writer.
21
+ *
22
+ * @param {object[]} trace formatted trace
23
+ */
24
+ append (trace) {
25
+ this._encoder.encode(trace)
26
+ }
27
+
28
+ /**
29
+ * Flushes encoded events synchronously to an offline payload file.
30
+ *
31
+ * @param {Function} [done] completion callback
32
+ */
33
+ flush (done = () => {}) {
34
+ if (this._encoder.count() > 0) {
35
+ let payload
36
+ try {
37
+ payload = this._encoder.makePayload()
38
+ } catch (error) {
39
+ if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
40
+ this._encoder.reset()
41
+ this._sink.recordError('output_payload_too_large')
42
+ done()
43
+ return
44
+ }
45
+ this._sink.writeTestCycle(payload)
46
+ }
47
+ done()
48
+ }
49
+
50
+ /**
51
+ * Adds CI event metadata before the next payload is encoded.
52
+ *
53
+ * @param {object} tags metadata grouped by event type
54
+ */
55
+ addMetadataTags (tags) {
56
+ this._encoder.addMetadataTags(tags)
57
+ }
58
+ }
59
+
60
+ module.exports = CiValidationWriter
@@ -13,6 +13,7 @@ const { writeSettingsToCache } = require('../test-optimization-cache')
13
13
  const { CACHE_MISS, TestOptimizationHttpCache } = require('../test-optimization-http-cache')
14
14
  const { uploadCoverageReport: uploadCoverageReportRequest } = require('../requests/upload-coverage-report')
15
15
  const { uploadTestScreenshot: uploadTestScreenshotRequest } = require('../requests/upload-test-screenshot')
16
+ const { parsers } = require('../../config/parsers')
16
17
  const log = require('../../log')
17
18
  const BufferingExporter = require('../../exporters/common/buffering-exporter')
18
19
  const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags')
@@ -41,6 +42,7 @@ function getIsTestSessionTrace (trace) {
41
42
 
42
43
  const GIT_UPLOAD_TIMEOUT = 60_000 // 60 seconds
43
44
  const CAN_USE_CI_VIS_PROTOCOL_TIMEOUT = GIT_UPLOAD_TIMEOUT
45
+ const MAX_COVERAGE_REPORT_FLAGS = 32
44
46
 
45
47
  function appendLogTag (tags, key, value) {
46
48
  if (value !== undefined) {
@@ -71,13 +73,24 @@ function getLogTags (logMessage, { env, version }, gitRepositoryUrl, gitCommitSh
71
73
  }
72
74
 
73
75
  class CiVisibilityExporter extends BufferingExporter {
74
- constructor (config) {
76
+ constructor (config, options = {}) {
75
77
  super(config)
76
78
  this._timer = undefined
77
79
  this._coverageTimer = undefined
78
80
  this._logsTimer = undefined
79
81
  this._coverageBuffer = []
80
- this._testOptimizationHttpCache = new TestOptimizationHttpCache()
82
+ this._testOptimizationHttpCache = options.testOptimizationHttpCache || new TestOptimizationHttpCache()
83
+ this._isTestOptimizationCacheOnly = options.cacheOnly === true
84
+ const coverageReportFlags = parsers.ARRAY(config?.testOptimization?.DD_CODE_COVERAGE_FLAGS)
85
+ if (coverageReportFlags?.length > MAX_COVERAGE_REPORT_FLAGS) {
86
+ log.warn(
87
+ 'Maximum of %d coverage report flags allowed, but %d flags were provided. Omitting coverage report flags.',
88
+ MAX_COVERAGE_REPORT_FLAGS,
89
+ coverageReportFlags.length
90
+ )
91
+ } else if (coverageReportFlags?.length) {
92
+ this._coverageReportFlags = [...coverageReportFlags]
93
+ }
81
94
  // The library can use new features like ITR and test suite level visibility
82
95
  // AKA CI Vis Protocol
83
96
  this._canUseCiVisProtocol = false
@@ -181,6 +194,9 @@ class CiVisibilityExporter extends BufferingExporter {
181
194
  const { skippableSuites, correlationId, coverage } = cachedSkippableSuites
182
195
  return callback(null, skippableSuites, correlationId, coverage)
183
196
  }
197
+ if (this._isTestOptimizationCacheOnly) {
198
+ return callback(this._getCacheOnlyError('skippable tests'), [])
199
+ }
184
200
 
185
201
  this._gitUploadPromise.then(gitUploadError => {
186
202
  if (gitUploadError) {
@@ -198,6 +214,9 @@ class CiVisibilityExporter extends BufferingExporter {
198
214
  if (cachedKnownTests !== CACHE_MISS) {
199
215
  return callback(null, cachedKnownTests)
200
216
  }
217
+ if (this._isTestOptimizationCacheOnly) {
218
+ return callback(this._getCacheOnlyError('known tests'))
219
+ }
201
220
  getKnownTestsRequest(this.getRequestConfiguration(testConfiguration), callback)
202
221
  }
203
222
 
@@ -209,6 +228,9 @@ class CiVisibilityExporter extends BufferingExporter {
209
228
  if (cachedTestManagementTests !== CACHE_MISS) {
210
229
  return callback(null, cachedTestManagementTests)
211
230
  }
231
+ if (this._isTestOptimizationCacheOnly) {
232
+ return callback(this._getCacheOnlyError('test management tests'))
233
+ }
212
234
  getTestManagementTestsRequest(this.getRequestConfiguration(testConfiguration), callback)
213
235
  }
214
236
 
@@ -241,6 +263,10 @@ class CiVisibilityExporter extends BufferingExporter {
241
263
  return callback(null, this._libraryConfig)
242
264
  }
243
265
 
266
+ if (this._isTestOptimizationCacheOnly) {
267
+ return callback(this._getCacheOnlyError('settings'), {})
268
+ }
269
+
244
270
  this.sendGitMetadata(repositoryUrl)
245
271
  getLibraryConfigurationRequest(configuration, (err, libraryConfig) => {
246
272
  /**
@@ -270,6 +296,17 @@ class CiVisibilityExporter extends BufferingExporter {
270
296
  })
271
297
  }
272
298
 
299
+ /**
300
+ * Returns the deterministic cache error for offline exporters.
301
+ *
302
+ * @param {string} input required cache input
303
+ * @returns {Error} cache error
304
+ */
305
+ _getCacheOnlyError (input) {
306
+ return this._testOptimizationHttpCache.getLastError?.() ||
307
+ new Error(`Offline Test Optimization validation requires a valid ${input} cache fixture.`)
308
+ }
309
+
273
310
  // Takes into account potential kill switches
274
311
  filterConfiguration (remoteConfiguration) {
275
312
  if (!remoteConfiguration) {
@@ -293,6 +330,9 @@ class CiVisibilityExporter extends BufferingExporter {
293
330
  isCoverageReportUploadEnabled,
294
331
  } = remoteConfiguration
295
332
  const { testOptimization } = this._config
333
+ const earlyFlakeDetectionRetryCount =
334
+ testOptimization.DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT
335
+ const hasEarlyFlakeDetectionRetryCount = earlyFlakeDetectionRetryCount !== undefined
296
336
  return {
297
337
  isCodeCoverageEnabled,
298
338
  isSuitesSkippingEnabled,
@@ -300,8 +340,16 @@ class CiVisibilityExporter extends BufferingExporter {
300
340
  requireGit,
301
341
  isEarlyFlakeDetectionEnabled:
302
342
  isEarlyFlakeDetectionEnabled && testOptimization.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED,
303
- earlyFlakeDetectionNumRetries,
304
- earlyFlakeDetectionSlowTestRetries,
343
+ earlyFlakeDetectionNumRetries:
344
+ hasEarlyFlakeDetectionRetryCount ? earlyFlakeDetectionRetryCount : earlyFlakeDetectionNumRetries,
345
+ earlyFlakeDetectionSlowTestRetries: hasEarlyFlakeDetectionRetryCount
346
+ ? {
347
+ '5s': earlyFlakeDetectionRetryCount,
348
+ '10s': earlyFlakeDetectionRetryCount,
349
+ '30s': earlyFlakeDetectionRetryCount,
350
+ '5m': earlyFlakeDetectionRetryCount,
351
+ }
352
+ : earlyFlakeDetectionSlowTestRetries,
305
353
  earlyFlakeDetectionFaultyThreshold,
306
354
  isFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled && testOptimization.DD_CIVISIBILITY_FLAKY_RETRY_ENABLED,
307
355
  flakyTestRetriesCount: testOptimization.DD_CIVISIBILITY_FLAKY_RETRY_COUNT,
@@ -318,6 +366,7 @@ class CiVisibilityExporter extends BufferingExporter {
318
366
 
319
367
  sendGitMetadata (repositoryUrl) {
320
368
  if (!this._config.testOptimization.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED) {
369
+ this._resolveGit()
321
370
  return
322
371
  }
323
372
  this._canUseCiVisProtocolPromise.then((canUseCiVisProtocol) => {
@@ -475,7 +524,7 @@ class CiVisibilityExporter extends BufferingExporter {
475
524
  * @param {string} options.filePath - Path to the coverage report file
476
525
  * @param {string} options.format - Format of the coverage report
477
526
  * @param {object} options.testEnvironmentMetadata - Test environment metadata containing git/CI tags
478
- * @param {Function} callback - Callback function (err)
527
+ * @param {(error: Error|null) => void} callback - Callback function
479
528
  */
480
529
  uploadCoverageReport ({ filePath, format, testEnvironmentMetadata }, callback) {
481
530
  if (!this._codeCoverageReportUrl) {
@@ -485,6 +534,7 @@ class CiVisibilityExporter extends BufferingExporter {
485
534
  uploadCoverageReportRequest({
486
535
  filePath,
487
536
  format,
537
+ flags: this._coverageReportFlags,
488
538
  testEnvironmentMetadata,
489
539
  url: this._codeCoverageReportUrl,
490
540
  isEvpProxy: !!this._isUsingEvpProxy,
@@ -102,14 +102,30 @@ class TestWorkerCiVisibilityExporter {
102
102
  this._logsWriter.append({ testEnvironmentMetadata, logMessage })
103
103
  }
104
104
 
105
- // TODO: add to other writers
105
+ /**
106
+ * @param {() => void} [onDone]
107
+ */
106
108
  flush (onDone) {
107
- this._writer.flush(onDone)
108
- this._coverageWriter.flush()
109
- this._logsWriter.flush()
110
- if (this._telemetryWriter) {
111
- this._telemetryWriter.flush()
109
+ if (!onDone) {
110
+ this._writer.flush()
111
+ this._coverageWriter.flush()
112
+ this._logsWriter.flush()
113
+ if (this._telemetryWriter) {
114
+ this._telemetryWriter.flush()
115
+ }
116
+ return
117
+ }
118
+
119
+ let pendingWriters = this._telemetryWriter ? 4 : 3
120
+ const onWriterFlushed = () => {
121
+ pendingWriters--
122
+ if (pendingWriters === 0) onDone()
112
123
  }
124
+
125
+ this._writer.flush(onWriterFlushed)
126
+ this._coverageWriter.flush(onWriterFlushed)
127
+ this._logsWriter.flush(onWriterFlushed)
128
+ this._telemetryWriter?.flush(onWriterFlushed)
113
129
  }
114
130
  }
115
131
 
@@ -15,6 +15,9 @@ class Writer {
15
15
  this._interprocessCode = interprocessCode
16
16
  }
17
17
 
18
+ /**
19
+ * @param {() => void} [onDone]
20
+ */
18
21
  flush (onDone) {
19
22
  const count = this._encoder.count()
20
23
 
@@ -22,6 +25,8 @@ class Writer {
22
25
  const payload = this._encoder.makePayload()
23
26
 
24
27
  this._sendPayload(payload, onDone)
28
+ } else {
29
+ onDone?.()
25
30
  }
26
31
  }
27
32