dd-trace 6.4.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 (178) 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/init.js +18 -0
  52. package/initialize.mjs +1 -1
  53. package/loader-hook.mjs +28 -18
  54. package/openfeature.d.ts +1 -0
  55. package/openfeature.js +4 -0
  56. package/package.json +10 -7
  57. package/packages/datadog-instrumentations/src/ai.js +8 -2
  58. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  60. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  61. package/packages/datadog-instrumentations/src/cucumber.js +20 -7
  62. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  63. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  64. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  65. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  67. package/packages/datadog-instrumentations/src/jest.js +26 -0
  68. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  69. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  71. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  72. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  73. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  74. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  75. package/packages/datadog-instrumentations/src/next.js +24 -0
  76. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  77. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  78. package/packages/datadog-instrumentations/src/pg.js +2 -2
  79. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  80. package/packages/datadog-instrumentations/src/process.js +3 -0
  81. package/packages/datadog-instrumentations/src/router.js +18 -8
  82. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  83. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  84. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  85. package/packages/datadog-instrumentations/src/ws.js +2 -1
  86. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  87. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  88. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  89. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  90. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  91. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  92. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  93. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  94. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  95. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  96. package/packages/datadog-plugin-http2/src/client.js +1 -1
  97. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  98. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  99. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  100. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  101. package/packages/datadog-plugin-router/src/index.js +11 -2
  102. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  103. package/packages/datadog-plugin-ws/src/close.js +2 -1
  104. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  105. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  106. package/packages/datadog-plugin-ws/src/server.js +2 -1
  107. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  110. package/packages/dd-trace/src/azure_metadata.js +2 -1
  111. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  112. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  113. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  114. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  115. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  116. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  117. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  119. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  120. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  121. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  122. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  123. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  124. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  127. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  128. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  129. package/packages/dd-trace/src/config/helper.js +1 -0
  130. package/packages/dd-trace/src/config/index.js +10 -0
  131. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  132. package/packages/dd-trace/src/exporter.js +2 -0
  133. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  134. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  135. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  136. package/packages/dd-trace/src/feature-registry.js +10 -3
  137. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  138. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  139. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  140. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  141. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  142. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  143. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  146. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  147. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  149. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  150. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  151. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  153. package/packages/dd-trace/src/llmobs/util.js +32 -0
  154. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  155. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  156. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  157. package/packages/dd-trace/src/openfeature/index.js +0 -2
  158. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  159. package/packages/dd-trace/src/openfeature/register.js +16 -27
  160. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  161. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  162. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  163. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  164. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  165. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  166. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  167. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  168. package/packages/dd-trace/src/proxy.js +84 -7
  169. package/packages/dd-trace/src/span_processor.js +5 -0
  170. package/packages/dd-trace/src/standalone/index.js +0 -14
  171. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  172. package/packages/dd-trace/src/util.js +26 -0
  173. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  174. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  175. package/vendor/dist/protobufjs/index.js +1 -1
  176. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  177. package/vendor/dist/shell-quote/index.js +1 -1
  178. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,707 @@
1
+ 'use strict'
2
+
3
+ /* eslint-disable no-console, eslint-rules/eslint-process-env */
4
+
5
+ const path = require('path')
6
+ const { spawn } = require('child_process')
7
+
8
+ const { cleanupCommandOutputs, prepareCommandOutputs } = require('./command-output-policy')
9
+ const {
10
+ getExecutableForSpawn,
11
+ isEnvExecutable,
12
+ parseArgv,
13
+ } = require('./executable')
14
+ const { sanitizeConsoleText, sanitizeString } = require('./redaction')
15
+ const { ensureSafeDirectory, writeFileSafely } = require('./safe-files')
16
+
17
+ const INIT_PATH = path.resolve(__dirname, '..', 'init.js')
18
+ const REGISTER_PATH = path.resolve(__dirname, '..', '..', 'register.js')
19
+ const VALIDATION_MODE_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_MODE'
20
+ const VALIDATION_MANIFEST_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_MANIFEST_FILE'
21
+ const VALIDATION_OUTPUT_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_OUTPUT_DIR'
22
+ const VALIDATION_CAPTURE_MODE_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_CAPTURE_MODE'
23
+ const APM_AGENTLESS_ENV = '_DD_APM_TRACING_AGENTLESS_ENABLED'
24
+ const VALIDATION_RESERVED_ENV_NAMES = [
25
+ 'NODE_OPTIONS',
26
+ VALIDATION_MANIFEST_ENV,
27
+ VALIDATION_MODE_ENV,
28
+ VALIDATION_OUTPUT_ENV,
29
+ VALIDATION_CAPTURE_MODE_ENV,
30
+ APM_AGENTLESS_ENV,
31
+ ]
32
+ const CLEAN_ENV_ALLOWLIST = new Set([
33
+ 'COMSPEC',
34
+ 'ComSpec',
35
+ 'HOME',
36
+ 'LOGNAME',
37
+ 'PATH',
38
+ 'Path',
39
+ 'PATHEXT',
40
+ 'SHELL',
41
+ 'SystemRoot',
42
+ 'TEMP',
43
+ 'TMP',
44
+ 'TMPDIR',
45
+ 'USER',
46
+ 'USERNAME',
47
+ 'VOLTA_HOME',
48
+ 'WINDIR',
49
+ 'windir',
50
+ ])
51
+ const VALIDATION_SUPPRESSION_ENV = {
52
+ DD_AGENTLESS_LOG_SUBMISSION_ENABLED: 'false',
53
+ DD_APPSEC_ENABLED: 'false',
54
+ DD_CRASHTRACKING_ENABLED: 'false',
55
+ DD_DATA_STREAMS_ENABLED: 'false',
56
+ DD_DYNAMIC_INSTRUMENTATION_ENABLED: 'false',
57
+ DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED: 'false',
58
+ DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'false',
59
+ DD_HEAP_SNAPSHOT_COUNT: '0',
60
+ DD_IAST_ENABLED: 'false',
61
+ DD_INSTRUMENTATION_TELEMETRY_ENABLED: 'false',
62
+ DD_LLMOBS_ENABLED: 'false',
63
+ DD_LOGS_OTEL_ENABLED: 'false',
64
+ DD_METRICS_OTEL_ENABLED: 'false',
65
+ DD_PROFILING_ENABLED: 'false',
66
+ DD_REMOTE_CONFIGURATION_ENABLED: 'false',
67
+ DD_RUNTIME_METRICS_ENABLED: 'false',
68
+ DD_TRACE_OTEL_ENABLED: 'false',
69
+ DD_TRACE_SPAN_LEAK_DEBUG: '0',
70
+ // Offline validation only needs test-cycle events and explicitly configured filesystem inputs.
71
+ DD_CIVISIBILITY_GIT_UPLOAD_ENABLED: 'false',
72
+ DD_CIVISIBILITY_GIT_UNSHALLOW_ENABLED: 'false',
73
+ DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED: 'false',
74
+ DD_EXPERIMENTAL_TEST_REQUESTS_FS_CACHE: 'false',
75
+ DD_TEST_FAILED_TEST_REPLAY_ENABLED: 'false',
76
+ }
77
+ const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024
78
+ const EARLY_STOP_KILL_GRACE_MS = 500
79
+ const TIMEOUT_KILL_GRACE_MS = 5000
80
+ const TIMEOUT_FINALIZE_GRACE_MS = 1000
81
+
82
+ function runCommand (command, options = {}) {
83
+ const {
84
+ env = {},
85
+ envMode = 'inherit',
86
+ outDir,
87
+ label,
88
+ repositoryRoot,
89
+ requireExecutableApproval = false,
90
+ stopWhen,
91
+ verbose = false,
92
+ } = options
93
+ const artifactRoot = options.artifactRoot || path.dirname(outDir)
94
+ const startedAt = Date.now()
95
+ const {
96
+ maxOutputBytes,
97
+ timeoutFinalizeGraceMs,
98
+ timeoutKillGraceMs,
99
+ timeoutMs,
100
+ } = getCommandExecutionSettings(command)
101
+ const result = {
102
+ label,
103
+ command: serializeCommand(command),
104
+ displayCommand: serializeDisplayCommand(command),
105
+ commandDetails: getCommandDetails(command),
106
+ cwd: command.cwd,
107
+ exitCode: null,
108
+ signal: null,
109
+ stoppedEarly: false,
110
+ durationMs: 0,
111
+ timedOut: false,
112
+ stdout: '',
113
+ stdoutTruncated: false,
114
+ stderr: '',
115
+ stderrTruncated: false,
116
+ artifacts: {},
117
+ }
118
+
119
+ ensureSafeDirectory(artifactRoot, outDir, 'command artifact directory')
120
+ try {
121
+ assertNoInlineValidationEnvOverrides(command, env)
122
+ } catch (error) {
123
+ return Promise.reject(error)
124
+ }
125
+ const outputStates = prepareCommandOutputs({ command, artifactRoot, outDir, repositoryRoot })
126
+
127
+ return new Promise((resolve) => {
128
+ let finalized = false
129
+ let processGroupCleanupPending = false
130
+ let pendingCloseResult
131
+ const childEnv = {
132
+ ...getBaseEnv(envMode),
133
+ ...command.env,
134
+ ...env,
135
+ }
136
+ if (command.env?.NODE_OPTIONS && env.NODE_OPTIONS) {
137
+ childEnv.NODE_OPTIONS = mergeNodeOptions(env.NODE_OPTIONS, command.env.NODE_OPTIONS)
138
+ }
139
+ for (const [name, value] of Object.entries(childEnv)) {
140
+ if (value === undefined) delete childEnv[name]
141
+ }
142
+
143
+ const useProcessGroup = shouldUseProcessGroup()
144
+ let child
145
+ try {
146
+ const executable = getExecutableForSpawn(command, { requireApproval: requireExecutableApproval })
147
+ const argv0 = process.platform === 'win32' ? {} : { argv0: executable.argv0 }
148
+ child = command.usesShell
149
+ ? spawn(command.shellCommand, {
150
+ ...argv0,
151
+ cwd: command.cwd,
152
+ detached: useProcessGroup,
153
+ env: childEnv,
154
+ shell: executable.path,
155
+ stdio: ['ignore', 'pipe', 'pipe'],
156
+ })
157
+ : spawn(executable.path, command.argv.slice(1), {
158
+ ...argv0,
159
+ cwd: command.cwd,
160
+ detached: useProcessGroup,
161
+ env: childEnv,
162
+ shell: false,
163
+ stdio: ['ignore', 'pipe', 'pipe'],
164
+ })
165
+ } catch (error) {
166
+ result.stderr = `${error.stack || error}\n`
167
+ result.durationMs = Date.now() - startedAt
168
+ try {
169
+ result.commandOutputPaths = cleanupCommandOutputs(outputStates)
170
+ } catch (cleanupError) {
171
+ result.outputCleanupError = cleanupError?.message || String(cleanupError)
172
+ }
173
+ resolve(result)
174
+ return
175
+ }
176
+
177
+ if (verbose) {
178
+ console.log(sanitizeConsoleText(
179
+ `[test-optimization-validator] running ${label || serializeCommand(command)}`
180
+ ))
181
+ }
182
+
183
+ let killTimer
184
+ let finalizeTimer
185
+ let stopTimer
186
+ const timeout = setTimeout(() => {
187
+ result.timedOut = true
188
+ processGroupCleanupPending = useProcessGroup
189
+ signalChild(child, 'SIGTERM', useProcessGroup)
190
+ killTimer = setTimeout(() => {
191
+ signalChild(child, 'SIGKILL', useProcessGroup)
192
+ finishProcessGroupCleanup('SIGKILL')
193
+ }, timeoutKillGraceMs)
194
+ }, timeoutMs)
195
+
196
+ if (stopWhen) {
197
+ stopTimer = setInterval(() => {
198
+ let shouldStop = false
199
+ try {
200
+ shouldStop = stopWhen()
201
+ } catch {}
202
+ if (!shouldStop) return
203
+
204
+ clearInterval(stopTimer)
205
+ result.stoppedEarly = true
206
+ processGroupCleanupPending = useProcessGroup
207
+ signalChild(child, 'SIGTERM', useProcessGroup)
208
+ killTimer = setTimeout(() => {
209
+ signalChild(child, 'SIGKILL', useProcessGroup)
210
+ finishProcessGroupCleanup('SIGKILL')
211
+ }, EARLY_STOP_KILL_GRACE_MS)
212
+ }, 25)
213
+ }
214
+
215
+ child.stdout.on('data', chunk => {
216
+ const capture = appendCapturedOutput(result.stdout, chunk, maxOutputBytes)
217
+ result.stdout = capture.output
218
+ result.stdoutTruncated = result.stdoutTruncated || capture.truncated
219
+ })
220
+ child.stderr.on('data', chunk => {
221
+ const capture = appendCapturedOutput(result.stderr, chunk, maxOutputBytes)
222
+ result.stderr = capture.output
223
+ result.stderrTruncated = result.stderrTruncated || capture.truncated
224
+ })
225
+ child.on('error', err => {
226
+ result.stderr += `${err.stack || err}\n`
227
+ finalize(null, null)
228
+ })
229
+ child.on('close', (code, signal) => {
230
+ if (processGroupCleanupPending) {
231
+ pendingCloseResult = { code, signal }
232
+ return
233
+ }
234
+ finalize(code, signal)
235
+ })
236
+
237
+ function finishProcessGroupCleanup (fallbackSignal) {
238
+ processGroupCleanupPending = false
239
+ if (pendingCloseResult) {
240
+ finalize(pendingCloseResult.code, pendingCloseResult.signal || fallbackSignal)
241
+ return
242
+ }
243
+ finalizeTimer = setTimeout(() => finalize(null, fallbackSignal), timeoutFinalizeGraceMs)
244
+ }
245
+
246
+ function finalize (code, signal) {
247
+ if (finalized) return
248
+ finalized = true
249
+ clearTimeout(timeout)
250
+ clearTimeout(killTimer)
251
+ clearTimeout(finalizeTimer)
252
+ clearInterval(stopTimer)
253
+ result.exitCode = code
254
+ result.signal = signal
255
+ result.durationMs = Date.now() - startedAt
256
+
257
+ try {
258
+ result.commandOutputPaths = cleanupCommandOutputs(outputStates)
259
+ } catch (err) {
260
+ result.outputCleanupError = err && err.message ? err.message : String(err)
261
+ result.stderr += '\n[test-optimization-validator] could not clean up command outputs: ' +
262
+ `${result.outputCleanupError}\n`
263
+ if (result.exitCode === 0) result.exitCode = 1
264
+ }
265
+
266
+ result.artifacts.stdout = path.join(outDir, 'stdout.txt')
267
+ result.artifacts.stderr = path.join(outDir, 'stderr.txt')
268
+ result.artifacts.command = path.join(outDir, 'command.json')
269
+
270
+ try {
271
+ writeFileSafely(
272
+ artifactRoot,
273
+ result.artifacts.stdout,
274
+ sanitizeString(formatCapturedOutput(result.stdout, result.stdoutTruncated, maxOutputBytes)),
275
+ 'command stdout artifact'
276
+ )
277
+ writeFileSafely(
278
+ artifactRoot,
279
+ result.artifacts.stderr,
280
+ sanitizeString(formatCapturedOutput(result.stderr, result.stderrTruncated, maxOutputBytes)),
281
+ 'command stderr artifact'
282
+ )
283
+ writeFileSafely(artifactRoot, result.artifacts.command, `${JSON.stringify({
284
+ command: sanitizeString(result.command),
285
+ displayCommand: sanitizeString(result.displayCommand),
286
+ commandDetails: result.commandDetails,
287
+ cwd: result.cwd,
288
+ exitCode: result.exitCode,
289
+ signal: result.signal,
290
+ durationMs: result.durationMs,
291
+ timedOut: result.timedOut,
292
+ stoppedEarly: result.stoppedEarly,
293
+ stdoutTruncated: result.stdoutTruncated,
294
+ stderrTruncated: result.stderrTruncated,
295
+ maxOutputBytes,
296
+ commandOutputPaths: result.commandOutputPaths,
297
+ outputCleanupError: result.outputCleanupError,
298
+ }, null, 2)}\n`, 'command metadata artifact')
299
+ } catch (error) {
300
+ result.artifactWriteError = error?.message || String(error)
301
+ result.stderr += '\n[test-optimization-validator] could not write command artifacts: ' +
302
+ `${result.artifactWriteError}\n`
303
+ if (!Number.isInteger(result.exitCode) || result.exitCode === 0) result.exitCode = 1
304
+ }
305
+
306
+ resolve(result)
307
+ }
308
+ })
309
+ }
310
+
311
+ /**
312
+ * Returns the effective bounded execution settings used for one project command.
313
+ *
314
+ * @param {object} command structured command
315
+ * @returns {{maxOutputBytes: number, timeoutFinalizeGraceMs: number, timeoutKillGraceMs: number, timeoutMs: number}}
316
+ * execution settings
317
+ */
318
+ function getCommandExecutionSettings (command) {
319
+ return {
320
+ maxOutputBytes: command.maxOutputBytes || DEFAULT_MAX_OUTPUT_BYTES,
321
+ timeoutFinalizeGraceMs: command.timeoutFinalizeGraceMs || TIMEOUT_FINALIZE_GRACE_MS,
322
+ timeoutKillGraceMs: command.timeoutKillGraceMs || TIMEOUT_KILL_GRACE_MS,
323
+ timeoutMs: command.timeoutMs || 300_000,
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Appends output while retaining only the latest bytes for diagnostic artifacts.
329
+ *
330
+ * @param {string} current currently captured output
331
+ * @param {Buffer|string} chunk new output chunk
332
+ * @param {number} maxBytes maximum retained bytes
333
+ * @returns {{output: string, truncated: boolean}} retained output and truncation flag
334
+ */
335
+ function appendCapturedOutput (current, chunk, maxBytes) {
336
+ const next = Buffer.concat([
337
+ Buffer.from(current),
338
+ Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)),
339
+ ])
340
+
341
+ if (next.length <= maxBytes) {
342
+ return {
343
+ output: next.toString('utf8'),
344
+ truncated: false,
345
+ }
346
+ }
347
+
348
+ return {
349
+ output: next.subarray(next.length - maxBytes).toString('utf8'),
350
+ truncated: true,
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Adds truncation context to a captured command output artifact.
356
+ *
357
+ * @param {string} output captured output
358
+ * @param {boolean} truncated whether earlier output was omitted
359
+ * @param {number} maxBytes maximum retained bytes
360
+ * @returns {string} output artifact content
361
+ */
362
+ function formatCapturedOutput (output, truncated, maxBytes) {
363
+ if (!truncated) return output
364
+ return `[test-optimization-validator] output truncated to last ${maxBytes} bytes\n${output}`
365
+ }
366
+
367
+ function shouldUseProcessGroup () {
368
+ return process.platform !== 'win32'
369
+ }
370
+
371
+ function signalChild (child, signal, useProcessGroup) {
372
+ try {
373
+ if (useProcessGroup) {
374
+ process.kill(-child.pid, signal)
375
+ return
376
+ }
377
+ } catch {}
378
+
379
+ child.kill(signal)
380
+ }
381
+
382
+ function getBaseEnv (envMode) {
383
+ if (envMode !== 'clean') return process.env
384
+
385
+ const cleanEnv = {}
386
+ for (const name of CLEAN_ENV_ALLOWLIST) {
387
+ if (process.env[name] !== undefined) cleanEnv[name] = process.env[name]
388
+ }
389
+ return cleanEnv
390
+ }
391
+
392
+ function buildDatadogEnv ({ fixture, outputRoot, scenario, framework }) {
393
+ const offline = buildOfflineValidationEnv({ fixture, outputRoot })
394
+ return {
395
+ ...offline,
396
+ DD_CIVISIBILITY_AGENTLESS_ENABLED: '0',
397
+ DD_CIVISIBILITY_ENABLED: '1',
398
+ DD_TRACE_ENABLED: 'true',
399
+ ...VALIDATION_SUPPRESSION_ENV,
400
+ DD_SERVICE: 'dd-test-optimization-validation',
401
+ DD_ENV: 'local-validation',
402
+ DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '2',
403
+ DD_TAGS: `test_optimization.validation.scenario:${scenario}`,
404
+ NODE_OPTIONS: withCiPreloads('', framework),
405
+ }
406
+ }
407
+
408
+ function buildCiWiringEnv ({ fixture, outputRoot }) {
409
+ return {
410
+ ...buildOfflineValidationEnv({ fixture, outputRoot }),
411
+ [VALIDATION_CAPTURE_MODE_ENV]: 'sample',
412
+ DD_TRACE_DEBUG: '1',
413
+ DD_TRACE_LOG_LEVEL: 'debug',
414
+ ...VALIDATION_SUPPRESSION_ENV,
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Builds the private environment used by the filesystem-only validation exporter.
420
+ *
421
+ * @param {object} input offline validation inputs
422
+ * @param {{manifestPath: string}} input.fixture authoritative cache fixture
423
+ * @param {string} input.outputRoot pre-created payload output root
424
+ * @returns {NodeJS.ProcessEnv} validation transport environment
425
+ */
426
+ function buildOfflineValidationEnv ({ fixture, outputRoot }) {
427
+ return {
428
+ DD_AGENT_HOST: undefined,
429
+ DD_API_KEY: undefined,
430
+ DD_APP_KEY: undefined,
431
+ DATADOG_API_KEY: undefined,
432
+ [APM_AGENTLESS_ENV]: undefined,
433
+ DD_CIVISIBILITY_AGENTLESS_ENABLED: '0',
434
+ DD_CIVISIBILITY_AGENTLESS_URL: undefined,
435
+ DD_EXPERIMENTAL_TEST_OPT_SETTINGS_CACHE: undefined,
436
+ DD_TRACE_AGENT_HOSTNAME: undefined,
437
+ DD_TRACE_AGENT_PORT: undefined,
438
+ DD_TRACE_AGENT_UNIX_DOMAIN_SOCKET: undefined,
439
+ DD_TRACE_AGENT_URL: undefined,
440
+ OTEL_LOGS_EXPORTER: undefined,
441
+ OTEL_METRICS_EXPORTER: undefined,
442
+ OTEL_TRACES_EXPORTER: undefined,
443
+ [VALIDATION_MANIFEST_ENV]: fixture.manifestPath,
444
+ [VALIDATION_MODE_ENV]: '1',
445
+ [VALIDATION_OUTPUT_ENV]: outputRoot,
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Rejects command-local assignments that can bypass validator-controlled offline routing.
451
+ *
452
+ * @param {object} command command to execute
453
+ * @param {NodeJS.ProcessEnv} env validator environment overrides
454
+ */
455
+ function assertNoInlineValidationEnvOverrides (command, env) {
456
+ if (!env[VALIDATION_MODE_ENV]) return
457
+ const reservedEnvNames = new Set([
458
+ ...VALIDATION_RESERVED_ENV_NAMES,
459
+ ...Object.keys(env).filter(name => {
460
+ return name.startsWith('DD_') || name.startsWith('_DD_') || name.startsWith('OTEL_')
461
+ }),
462
+ ])
463
+
464
+ if (command.usesShell) {
465
+ rejectReservedShellAssignments(command.shellCommand, reservedEnvNames)
466
+ return
467
+ }
468
+
469
+ const parsed = parseArgv(command.argv)
470
+ rejectReservedEnvSplitStrings(command.argv, reservedEnvNames)
471
+ if (parsed.ignoreEnvironment) throwEnvironmentReset()
472
+ if (parsed.unsupportedEnvOption) throwUnsupportedEnvOption(parsed.unsupportedEnvOption)
473
+ for (const name of Object.keys(parsed.prefixEnv)) {
474
+ if (reservedEnvNames.has(name)) throwReservedEnvOverride(name)
475
+ }
476
+ for (const name of parsed.unsetEnvNames) {
477
+ if (reservedEnvNames.has(name)) throwReservedEnvOverride(name)
478
+ }
479
+
480
+ if (isPosixShellExecutable(command.argv[parsed.commandIndex])) {
481
+ for (let index = parsed.commandIndex + 1; index < command.argv.length - 1; index++) {
482
+ const value = command.argv[index]
483
+ if (isShellCommandFlag(value) && typeof command.argv[index + 1] === 'string') {
484
+ rejectReservedShellAssignments(command.argv[index + 1], reservedEnvNames)
485
+ }
486
+ }
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Rejects reserved variable assignments and removals in shell source.
492
+ *
493
+ * @param {string} shellCommand shell source
494
+ * @param {Set<string>} reservedEnvNames validator-controlled environment names
495
+ */
496
+ function rejectReservedShellAssignments (shellCommand, reservedEnvNames) {
497
+ const source = String(shellCommand || '')
498
+ const environmentReset =
499
+ /\benv(?:\.exe)?\s+(?:(?![;&|()]).)*?(?:-(?=\s|$)|-i\b|--ignore-environment\b)/i
500
+
501
+ if (environmentReset.test(source)) throwEnvironmentReset()
502
+
503
+ for (const name of reservedEnvNames) {
504
+ const escapedName = escapeRegExp(name)
505
+ const assignment = new RegExp(
506
+ String.raw`(?:^|[\s;&|()'"])(?:export\s+|set\s+)?(?:\$env:)?${escapedName}\s*\+?=`,
507
+ 'i'
508
+ )
509
+ const removal = new RegExp(
510
+ String.raw`(?:\bunset(?:\s+(?:-[A-Za-z]+|[A-Za-z_][A-Za-z0-9_]*))*\s+|` +
511
+ String.raw`\benv(?:\.exe)?\s+(?:(?![;&|()]).)*?(?:-u\s*|--unset(?:=|\s+))|` +
512
+ String.raw`\bRemove-Item\s+(?:[^;&|]*\s)?env:)${escapedName}\b`,
513
+ 'i'
514
+ )
515
+
516
+ if (assignment.test(source) || removal.test(source)) throwReservedEnvOverride(name)
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Rejects reserved environment changes hidden inside env --split-string arguments.
522
+ *
523
+ * @param {string[]} argv structured command arguments
524
+ * @param {Set<string>} reservedEnvNames validator-controlled environment names
525
+ * @returns {void}
526
+ */
527
+ function rejectReservedEnvSplitStrings (argv, reservedEnvNames) {
528
+ if (!Array.isArray(argv) || !isEnvExecutable(argv[0])) return
529
+
530
+ for (let index = 1; index < argv.length; index++) {
531
+ const argument = argv[index]
532
+ if (argument === '-S' || argument === '--split-string') {
533
+ if (typeof argv[index + 1] === 'string') {
534
+ rejectReservedShellAssignments(`env ${argv[index + 1]}`, reservedEnvNames)
535
+ }
536
+ index++
537
+ continue
538
+ }
539
+
540
+ const splitString = /^(?:-S|--split-string=)(.+)$/.exec(argument)?.[1]
541
+ if (splitString !== undefined) rejectReservedShellAssignments(`env ${splitString}`, reservedEnvNames)
542
+ }
543
+ }
544
+
545
+ function isShellCommandFlag (value) {
546
+ return /^-[A-Za-z]*c[A-Za-z]*$/.test(value)
547
+ }
548
+
549
+ function isPosixShellExecutable (value) {
550
+ return /^(?:a|ba|da|k|z)?sh$/.test(path.basename(String(value || '')).toLowerCase())
551
+ }
552
+
553
+ /**
554
+ * Throws a customer-facing error for unsafe inline validation environment changes.
555
+ *
556
+ * @param {string} name reserved environment variable
557
+ */
558
+ function throwReservedEnvOverride (name) {
559
+ throw new Error(
560
+ `Refusing inline ${name} changes during live validation because they can bypass the offline validation mode. ` +
561
+ 'Record CI-provided values in command.env so the validator can apply its private diagnostic settings.'
562
+ )
563
+ }
564
+
565
+ function throwEnvironmentReset () {
566
+ throw new Error(
567
+ 'Refusing to clear the command environment during live validation because this would remove the offline ' +
568
+ 'validation and Datadog initialization settings.'
569
+ )
570
+ }
571
+
572
+ function throwUnsupportedEnvOption (option) {
573
+ throw new Error(
574
+ `Refusing unsupported env option ${option} during live validation because its environment effects cannot be ` +
575
+ 'verified safely.'
576
+ )
577
+ }
578
+
579
+ /**
580
+ * Escapes a literal for use in a regular expression.
581
+ *
582
+ * @param {string} value literal value
583
+ * @returns {string} escaped value
584
+ */
585
+ function escapeRegExp (value) {
586
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
587
+ }
588
+
589
+ function withCiPreloads (nodeOptions = '', framework) {
590
+ let result = nodeOptions.trim()
591
+
592
+ if (framework?.framework === 'vitest' && !hasRegister(result)) {
593
+ result = `--import ${formatNodeRequire(REGISTER_PATH)}${result ? ` ${result}` : ''}`
594
+ }
595
+
596
+ if (!hasCiInit(result)) {
597
+ result = `${result ? `${result} ` : ''}-r ${formatNodeRequire(INIT_PATH)}`
598
+ }
599
+
600
+ return result
601
+ }
602
+
603
+ function mergeNodeOptions (...nodeOptions) {
604
+ return nodeOptions
605
+ .map(value => String(value || '').trim())
606
+ .filter(Boolean)
607
+ .join(' ')
608
+ }
609
+
610
+ function hasCiInit (nodeOptions) {
611
+ return nodeOptions.includes('dd-trace/ci/init') || nodeOptions.includes(INIT_PATH)
612
+ }
613
+
614
+ function hasRegister (nodeOptions) {
615
+ return nodeOptions.includes('dd-trace/register.js') || nodeOptions.includes(REGISTER_PATH)
616
+ }
617
+
618
+ function formatNodeRequire (filename) {
619
+ if (!/\s/.test(filename)) return filename
620
+ return JSON.stringify(filename)
621
+ }
622
+
623
+ function serializeCommand (command) {
624
+ return command.usesShell ? command.shellCommand : command.argv.join(' ')
625
+ }
626
+
627
+ /**
628
+ * Renders the command that will actually execute without trusting display-only manifest fields.
629
+ *
630
+ * @param {object} command command to render
631
+ * @returns {string} unambiguous customer-facing command
632
+ */
633
+ function serializeApprovalCommand (command) {
634
+ if (command.usesShell) return command.shellCommand
635
+ return command.argv.map(formatApprovalArgument).join(' ')
636
+ }
637
+
638
+ /**
639
+ * Quotes arguments whose boundaries would otherwise be ambiguous in an approval plan.
640
+ *
641
+ * @param {string} value argument value
642
+ * @returns {string} visible argument
643
+ */
644
+ function formatApprovalArgument (value) {
645
+ const argument = String(value)
646
+ if (/^[A-Za-z0-9_@%+=:,./\\-]+$/.test(argument)) return argument
647
+ if (process.platform === 'win32') return JSON.stringify(argument)
648
+ return `'${argument.replaceAll('\'', '\'"\'"\'')}'`
649
+ }
650
+
651
+ function serializeDisplayCommand (command) {
652
+ if (typeof command.displayCommand === 'string' && command.displayCommand.trim()) {
653
+ return command.displayCommand.trim()
654
+ }
655
+
656
+ if (command.usesShell) return command.shellCommand
657
+
658
+ return getDisplayArgv(command.argv).join(' ')
659
+ }
660
+
661
+ function getCommandDetails (command) {
662
+ if (command.usesShell) return
663
+
664
+ const details = getDisplayDetails(command.argv)
665
+ if (!details.exactCommandCollapsed) return
666
+
667
+ return details
668
+ }
669
+
670
+ function getDisplayArgv (argv) {
671
+ const { prefixAssignments, commandIndex, corepackIndex } = parseArgv(argv)
672
+ if (corepackIndex !== -1) return [...prefixAssignments, ...argv.slice(corepackIndex + 1)]
673
+ return [...prefixAssignments, ...argv.slice(commandIndex)]
674
+ }
675
+
676
+ function getDisplayDetails (argv) {
677
+ const { commandIndex, corepackIndex, pathAdjusted } = parseArgv(argv)
678
+ const displayArgv = getDisplayArgv(argv)
679
+ const details = {
680
+ exactCommandCollapsed: displayArgv.join(' ') !== argv.join(' '),
681
+ }
682
+
683
+ if (pathAdjusted) details.pathAdjusted = true
684
+
685
+ if (corepackIndex !== -1) {
686
+ details.runtimeWrapper = 'node/corepack'
687
+ details.packageManager = argv[corepackIndex + 1]
688
+ } else if (commandIndex > 0) {
689
+ details.runtimeWrapper = 'env'
690
+ }
691
+
692
+ return details
693
+ }
694
+
695
+ module.exports = {
696
+ runCommand,
697
+ buildCiWiringEnv,
698
+ buildDatadogEnv,
699
+ getBaseEnv,
700
+ getCommandDetails,
701
+ getCommandExecutionSettings,
702
+ serializeApprovalCommand,
703
+ serializeCommand,
704
+ serializeDisplayCommand,
705
+ withCiPreloads,
706
+ mergeNodeOptions,
707
+ }