dd-trace 6.4.0 → 6.6.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 (185) 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 +11 -8
  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 +31 -19
  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/channel.js +74 -0
  65. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  66. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  68. package/packages/datadog-instrumentations/src/jest.js +44 -17
  69. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  70. package/packages/datadog-instrumentations/src/mocha/main.js +79 -32
  71. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  72. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  73. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  74. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  75. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  76. package/packages/datadog-instrumentations/src/next.js +24 -0
  77. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  78. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  79. package/packages/datadog-instrumentations/src/pg.js +2 -2
  80. package/packages/datadog-instrumentations/src/playwright.js +162 -46
  81. package/packages/datadog-instrumentations/src/process.js +3 -0
  82. package/packages/datadog-instrumentations/src/router.js +18 -8
  83. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  84. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +3 -3
  85. package/packages/datadog-instrumentations/src/vitest-main.js +14 -28
  86. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  87. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  88. package/packages/datadog-instrumentations/src/ws.js +2 -1
  89. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  90. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  91. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  92. package/packages/datadog-plugin-cucumber/src/index.js +3 -2
  93. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  94. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  95. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  96. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  97. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  98. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  99. package/packages/datadog-plugin-http2/src/client.js +1 -1
  100. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  101. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  102. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  103. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  104. package/packages/datadog-plugin-playwright/src/index.js +181 -65
  105. package/packages/datadog-plugin-router/src/index.js +11 -2
  106. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  107. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  108. package/packages/datadog-plugin-ws/src/close.js +2 -1
  109. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  110. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  111. package/packages/datadog-plugin-ws/src/server.js +2 -1
  112. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  113. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  114. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  115. package/packages/dd-trace/src/azure_metadata.js +2 -1
  116. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  117. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  121. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  122. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  124. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  125. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  126. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  127. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  128. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  129. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  131. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  132. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  133. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  134. package/packages/dd-trace/src/config/helper.js +1 -0
  135. package/packages/dd-trace/src/config/index.js +10 -0
  136. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  137. package/packages/dd-trace/src/exporter.js +2 -0
  138. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  139. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  140. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  141. package/packages/dd-trace/src/feature-registry.js +10 -3
  142. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  143. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  144. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  145. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  146. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  147. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  148. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  149. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  150. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  151. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  152. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  153. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  154. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  155. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  156. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  157. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  158. package/packages/dd-trace/src/llmobs/util.js +32 -0
  159. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  160. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  161. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  162. package/packages/dd-trace/src/openfeature/index.js +0 -2
  163. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  164. package/packages/dd-trace/src/openfeature/register.js +16 -27
  165. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  166. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  167. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  168. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  169. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  170. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  171. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  172. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  173. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  174. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  175. package/packages/dd-trace/src/proxy.js +84 -7
  176. package/packages/dd-trace/src/span_processor.js +5 -0
  177. package/packages/dd-trace/src/standalone/index.js +0 -14
  178. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  179. package/packages/dd-trace/src/util.js +26 -0
  180. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  181. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  182. package/vendor/dist/protobufjs/index.js +1 -1
  183. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  184. package/vendor/dist/shell-quote/index.js +1 -1
  185. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,246 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const {
7
+ mergeNodeOptions,
8
+ runCommand,
9
+ } = require('./command-runner')
10
+ const { inheritApprovedExecutable } = require('./executable-approval')
11
+ const { sanitizeForReport } = require('./redaction')
12
+ const { createFileSafely, ensureSafeDirectory, writeFileSafely } = require('./safe-files')
13
+
14
+ const PROBE_PRELOAD = path.join(__dirname, 'init-probe-preload.js')
15
+ const PROBE_FILE_ENV = 'DD_TEST_OPTIMIZATION_INIT_PROBE_FILE'
16
+ const MAX_PROBE_RECORD_BYTES = 1024 * 1024
17
+
18
+ /**
19
+ * Runs a CI-shaped command with a lightweight NODE_OPTIONS preload that records process reachability.
20
+ *
21
+ * @param {object} options probe options
22
+ * @param {object} options.command manifest command to run
23
+ * @param {object} options.framework manifest framework entry
24
+ * @param {string} options.outDir scenario output directory
25
+ * @param {object} options.options CLI options
26
+ * @returns {Promise<{ artifacts: object, summary: object }>} probe artifacts and summary
27
+ */
28
+ async function runInitializationProbe ({ command, framework, outDir, options }) {
29
+ const probeOutDir = path.join(outDir, 'initialization-probe')
30
+ const recordsPath = path.join(probeOutDir, 'records.ndjson')
31
+ const rawRecordsPath = path.join(probeOutDir, '.records.raw.ndjson')
32
+ const probeCommand = getProbeCommand(command)
33
+
34
+ ensureSafeDirectory(outDir, probeOutDir, 'initialization probe artifact directory')
35
+ createFileSafely(outDir, rawRecordsPath, '', 'raw initialization probe records')
36
+
37
+ let result
38
+ let records
39
+ try {
40
+ result = await runCommand(probeCommand, {
41
+ artifactRoot: outDir,
42
+ env: {
43
+ [PROBE_FILE_ENV]: rawRecordsPath,
44
+ NODE_OPTIONS: mergeNodeOptions(
45
+ `-r ${formatNodeRequire(PROBE_PRELOAD)}`
46
+ ),
47
+ },
48
+ envMode: 'clean',
49
+ outDir: probeOutDir,
50
+ repositoryRoot: options.repositoryRoot,
51
+ requireExecutableApproval: options.requireExecutableApproval,
52
+ label: `${framework.id}:ci-wiring:init-probe`,
53
+ stopWhen: () => probeReachedFramework(rawRecordsPath, framework.framework),
54
+ verbose: options.verbose,
55
+ })
56
+ records = readProbeRecords(rawRecordsPath)
57
+ writeFileSafely(
58
+ outDir,
59
+ recordsPath,
60
+ records.map(record => JSON.stringify(sanitizeForReport(record))).join('\n') + '\n',
61
+ 'initialization probe records'
62
+ )
63
+ } finally {
64
+ fs.rmSync(rawRecordsPath, { force: true })
65
+ }
66
+
67
+ return {
68
+ artifacts: {
69
+ command: result.artifacts.command,
70
+ records: recordsPath,
71
+ stderr: result.artifacts.stderr,
72
+ stdout: result.artifacts.stdout,
73
+ },
74
+ summary: summarizeProbeResult({ framework, result, records, recordsPath }),
75
+ }
76
+ }
77
+
78
+ function getProbeCommand (command) {
79
+ if (!command.env?.NODE_OPTIONS) return command
80
+
81
+ const env = { ...command.env }
82
+ const nodeOptions = removeDatadogPreloads(env.NODE_OPTIONS)
83
+ if (nodeOptions) {
84
+ env.NODE_OPTIONS = nodeOptions
85
+ } else {
86
+ delete env.NODE_OPTIONS
87
+ }
88
+
89
+ return inheritApprovedExecutable(command, {
90
+ ...command,
91
+ env,
92
+ })
93
+ }
94
+
95
+ function removeDatadogPreloads (nodeOptions) {
96
+ const tokens = tokenizeNodeOptions(nodeOptions)
97
+ const kept = []
98
+
99
+ for (let index = 0; index < tokens.length; index++) {
100
+ const token = tokens[index]
101
+ if (isSeparatedPreloadFlag(token) && isDatadogPreload(tokens[index + 1])) {
102
+ index++
103
+ continue
104
+ }
105
+ if (isInlineDatadogPreload(token)) continue
106
+ kept.push(token)
107
+ }
108
+
109
+ return kept.join(' ')
110
+ }
111
+
112
+ function tokenizeNodeOptions (nodeOptions) {
113
+ return String(nodeOptions || '').match(/"[^"]*"|'[^']*'|\S+/g) || []
114
+ }
115
+
116
+ function isSeparatedPreloadFlag (token) {
117
+ return token === '-r' || token === '--require' || token === '--import'
118
+ }
119
+
120
+ function isInlineDatadogPreload (token) {
121
+ return (token.startsWith('--require=') && isDatadogPreload(token.slice('--require='.length))) ||
122
+ (token.startsWith('--import=') && isDatadogPreload(token.slice('--import='.length))) ||
123
+ (token.startsWith('-r') && token.length > 2 && isDatadogPreload(token.slice(2)))
124
+ }
125
+
126
+ function isDatadogPreload (value) {
127
+ const normalized = String(value || '').replaceAll(/^['"]|['"]$/g, '')
128
+ return normalized.includes('dd-trace/ci/init') || normalized.includes('dd-trace/register')
129
+ }
130
+
131
+ function summarizeProbeResult ({ framework, result, records, recordsPath }) {
132
+ const processRecords = records.filter(record => record.type === 'process-start')
133
+ const moduleLoadRecords = records.filter(record => record.type === 'module-load')
134
+ const testRunnerSignals = getToolSignals(records, 'test-runner')
135
+ .filter(signal => signal.name === framework.framework)
136
+ const wrapperSignals = getToolSignals(records, 'wrapper', { processStartsOnly: true })
137
+ const packageManagerSignals = getToolSignals(records, 'package-manager', { processStartsOnly: true })
138
+
139
+ return {
140
+ ran: true,
141
+ commandExitCode: result.exitCode,
142
+ commandTimedOut: result.timedOut,
143
+ processCount: processRecords.length,
144
+ moduleLoadCount: moduleLoadRecords.length,
145
+ reachedAnyNodeProcess: processRecords.length > 0,
146
+ reachedTestRunnerProcess: testRunnerSignals.length > 0,
147
+ stoppedAfterRunnerReached: result.stoppedEarly === true && testRunnerSignals.length > 0,
148
+ testRunnerSignals,
149
+ wrapperSignals,
150
+ packageManagerSignals,
151
+ recordsPath,
152
+ }
153
+ }
154
+
155
+ function probeReachedFramework (recordsPath, frameworkName) {
156
+ return readProbeRecords(recordsPath).some(record => {
157
+ return getRecordTools(record).some(tool => tool.kind === 'test-runner' && tool.name === frameworkName)
158
+ })
159
+ }
160
+
161
+ function getToolSignals (records, kind, { processStartsOnly = false } = {}) {
162
+ const signals = []
163
+ const signalsByLocation = new Map()
164
+
165
+ for (const record of records) {
166
+ if (processStartsOnly && record.type !== 'process-start') continue
167
+ for (const tool of getRecordTools(record)) {
168
+ if (tool.kind !== kind) continue
169
+
170
+ const key = `${tool.name}:${record.cwd}`
171
+ let signal = signalsByLocation.get(key)
172
+ if (!signal) {
173
+ signal = {
174
+ name: tool.name,
175
+ kind: tool.kind,
176
+ pid: record.pid,
177
+ ppid: record.ppid,
178
+ source: record.type,
179
+ argv: Array.isArray(record.argv) ? record.argv : undefined,
180
+ cwd: record.cwd,
181
+ request: record.request,
182
+ processCount: 0,
183
+ processIds: new Set(),
184
+ }
185
+ signalsByLocation.set(key, signal)
186
+ signals.push(signal)
187
+ }
188
+ signal.processIds.add(record.pid)
189
+ }
190
+ }
191
+
192
+ return signals.map(signal => {
193
+ signal.processCount = signal.processIds.size
194
+ delete signal.processIds
195
+ return signal
196
+ })
197
+ }
198
+
199
+ function getRecordTools (record) {
200
+ if (record.tool) return [record.tool]
201
+ if (Array.isArray(record.detectedTools)) return record.detectedTools
202
+ return []
203
+ }
204
+
205
+ function readProbeRecords (recordsPath) {
206
+ let file
207
+ try {
208
+ file = fs.openSync(recordsPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0))
209
+ const stat = fs.fstatSync(file)
210
+ if (!stat.isFile() || stat.size > MAX_PROBE_RECORD_BYTES) return []
211
+ const content = fs.readFileSync(file, 'utf8')
212
+ return parseProbeRecords(content)
213
+ } catch {
214
+ return []
215
+ } finally {
216
+ if (file !== undefined) fs.closeSync(file)
217
+ }
218
+ }
219
+
220
+ function parseProbeRecords (content) {
221
+ const records = []
222
+ for (const line of content.split(/\r?\n/)) {
223
+ if (!line) continue
224
+ try {
225
+ const record = JSON.parse(line)
226
+ if (isProbeRecord(record)) records.push(sanitizeForReport(record))
227
+ } catch {}
228
+ }
229
+ return records
230
+ }
231
+
232
+ function isProbeRecord (record) {
233
+ return record && typeof record === 'object' && !Array.isArray(record) &&
234
+ (record.type === 'process-start' || record.type === 'module-load') &&
235
+ Number.isInteger(record.pid) && Number.isInteger(record.ppid) &&
236
+ typeof record.cwd === 'string' && Array.isArray(record.argv)
237
+ }
238
+
239
+ function formatNodeRequire (filename) {
240
+ if (!/\s/.test(filename)) return filename
241
+ return JSON.stringify(filename)
242
+ }
243
+
244
+ module.exports = {
245
+ runInitializationProbe,
246
+ }
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const MAX_FILE_BYTES = 512 * 1024
7
+ const SCRIPT_LITERAL_PATTERN = /['"]([^'"]+\.[cm]?[jt]sx?)['"]/g
8
+
9
+ /**
10
+ * Finds Test Optimization initialization loaded from a Vitest setup file.
11
+ *
12
+ * @param {object} manifest normalized manifest
13
+ * @param {object} framework manifest framework
14
+ * @returns {{configFile: string, setupFile: string}[]} late initialization evidence
15
+ */
16
+ function findLateInitialization (manifest, framework) {
17
+ if (framework.framework !== 'vitest') return []
18
+
19
+ const findings = []
20
+ const seen = new Set()
21
+ for (const configFile of framework.project?.configFiles || []) {
22
+ const config = readSmallFile(configFile)
23
+ if (!config || !/\bsetupFiles\b/.test(config)) continue
24
+
25
+ if (/setupFiles[\s\S]{0,1000}?dd-trace\/ci\/init/.test(config)) {
26
+ addFinding(findings, seen, configFile, 'dd-trace/ci/init')
27
+ }
28
+
29
+ for (const match of config.matchAll(SCRIPT_LITERAL_PATTERN)) {
30
+ for (const candidate of getSetupFileCandidates(manifest.repository.root, configFile, match[1])) {
31
+ const setup = readSmallFile(candidate)
32
+ if (!setup || !/dd-trace\/ci\/init/.test(setup)) continue
33
+ addFinding(findings, seen, configFile, candidate)
34
+ }
35
+ }
36
+ }
37
+ return findings
38
+ }
39
+
40
+ function getSetupFileCandidates (repositoryRoot, configFile, filename) {
41
+ if (path.isAbsolute(filename)) return [filename]
42
+ return [
43
+ path.resolve(path.dirname(configFile), filename),
44
+ path.resolve(repositoryRoot, filename),
45
+ ]
46
+ }
47
+
48
+ function readSmallFile (filename) {
49
+ try {
50
+ const stat = fs.statSync(filename)
51
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return
52
+ return fs.readFileSync(filename, 'utf8')
53
+ } catch {}
54
+ }
55
+
56
+ function addFinding (findings, seen, configFile, setupFile) {
57
+ const key = `${configFile}:${setupFile}`
58
+ if (seen.has(key)) return
59
+ seen.add(key)
60
+ findings.push({ configFile, setupFile })
61
+ }
62
+
63
+ module.exports = { findLateInitialization }
@@ -0,0 +1,163 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+
5
+ const { inheritApprovedExecutable } = require('./executable-approval')
6
+
7
+ const JEST_NO_WATCHMAN_ADJUSTMENT = 'Disable Watchman for local validation to avoid home-directory writes.'
8
+ const INLINE_DATADOG_ENV_PATTERN = /(?:^|[\s;&|()'"])(?:export\s+|set\s+)?["']?(?:\$env:)?DD_[A-Z0-9_]+\s*\+?=/i
9
+ const INIT_PATH = path.resolve(__dirname, '..', 'init.js').replaceAll('\\', '/')
10
+ const REGISTER_PATH = path.resolve(__dirname, '..', '..', 'register.js').replaceAll('\\', '/')
11
+
12
+ /**
13
+ * Applies semantics-preserving local validation adjustments to a command.
14
+ *
15
+ * @param {object} framework manifest framework entry
16
+ * @param {object} command command to adjust
17
+ * @returns {object} adjusted command
18
+ */
19
+ function getLocalValidationCommand (framework, command) {
20
+ if (framework.framework !== 'jest' || command.usesShell || command.argv?.includes('--no-watchman') ||
21
+ !isJestCommand(command.argv)) {
22
+ return command
23
+ }
24
+
25
+ const argv = [...command.argv]
26
+ const executable = path.basename(argv[0]).replace(/\.cmd$/i, '')
27
+ if (executable === 'npm' && !argv.includes('--')) argv.push('--')
28
+ argv.push('--no-watchman')
29
+
30
+ return inheritApprovedExecutable(command, {
31
+ ...command,
32
+ argv,
33
+ localAdjustments: [
34
+ ...(command.localAdjustments || []),
35
+ JEST_NO_WATCHMAN_ADJUSTMENT,
36
+ ],
37
+ })
38
+ }
39
+
40
+ /**
41
+ * Identifies Jest entrypoints and package scripts that can forward Jest options.
42
+ *
43
+ * @param {string[]} argv command arguments
44
+ * @returns {boolean} whether --no-watchman can be appended safely
45
+ */
46
+ function isJestCommand (argv) {
47
+ if (!Array.isArray(argv) || argv.length === 0) return false
48
+
49
+ const executable = path.basename(argv[0]).replace(/\.cmd$/i, '')
50
+ if (['npm', 'npx', 'pnpm', 'yarn', 'yarnpkg'].includes(executable)) return true
51
+
52
+ return argv.some(argument => /(?:^|[/\\])(?:jest|jest\.js)$/.test(argument))
53
+ }
54
+
55
+ /**
56
+ * Removes Datadog initialization from a command before an uninstrumented preflight.
57
+ *
58
+ * @param {object} command command to sanitize
59
+ * @returns {object} Datadog-clean command
60
+ */
61
+ function getDatadogCleanCommand (command) {
62
+ const inlineInitialization = getInlineDatadogInitialization(command)
63
+ if (inlineInitialization) {
64
+ throw new Error(
65
+ `Cannot create a Datadog-clean command because it ${inlineInitialization}. ` +
66
+ 'Remove inline Datadog initialization from the local validation command.'
67
+ )
68
+ }
69
+
70
+ const env = {}
71
+ for (const [name, value] of Object.entries(command.env || {})) {
72
+ if (name.startsWith('DD_') || (name === 'NODE_OPTIONS' && /dd-trace/.test(value))) continue
73
+ env[name] = value
74
+ }
75
+
76
+ return inheritApprovedExecutable(command, {
77
+ ...command,
78
+ env,
79
+ })
80
+ }
81
+
82
+ /**
83
+ * Finds Datadog initialization embedded in executable arguments or shell source.
84
+ *
85
+ * @param {object} command manifest command
86
+ * @returns {string|undefined} customer-facing inline initialization description
87
+ */
88
+ function getInlineDatadogInitialization (command) {
89
+ const source = command?.usesShell
90
+ ? String(command.shellCommand || '')
91
+ : (command?.argv || []).join(' ')
92
+ const normalized = source.replaceAll('\\', '/')
93
+
94
+ if (normalized.includes('dd-trace/ci/init') || normalized.includes('dd-trace/register.js') ||
95
+ normalized.includes(INIT_PATH) || normalized.includes(REGISTER_PATH)) {
96
+ return 'contains an inline dd-trace preload'
97
+ }
98
+ if (INLINE_DATADOG_ENV_PATTERN.test(source)) return 'contains an inline DD_* assignment'
99
+ }
100
+
101
+ /**
102
+ * Returns the CI wiring command with the replay shell recorded by CI discovery when available.
103
+ *
104
+ * @param {object} framework manifest framework entry
105
+ * @returns {object|undefined} command to run
106
+ */
107
+ function getCiWiringCommand (framework) {
108
+ const command = framework.ciWiringCommand
109
+ if (!command || !command.usesShell || command.shell || !framework.ciWiring?.shell) return command
110
+
111
+ const replayCommand = getShellReplayCommand(command, framework.ciWiring.shell)
112
+ if (replayCommand) return inheritApprovedExecutable(command, replayCommand)
113
+
114
+ const shell = getReplayShell(framework.ciWiring.shell)
115
+ if (!shell) return command
116
+
117
+ return inheritApprovedExecutable(command, {
118
+ ...command,
119
+ shell,
120
+ })
121
+ }
122
+
123
+ function getShellReplayCommand (command, shell) {
124
+ const tokens = tokenizeShellTemplate(shell)
125
+ const hasTemplate = tokens.includes('{0}')
126
+ if (tokens.length <= 1 && !hasTemplate) return
127
+
128
+ const argv = hasTemplate ? tokens.filter(token => token !== '{0}') : tokens
129
+ if (!isBourneShell(argv[0])) return
130
+
131
+ return {
132
+ ...command,
133
+ argv: [...argv, '-c', command.shellCommand],
134
+ shell: undefined,
135
+ shellCommand: undefined,
136
+ usesShell: false,
137
+ }
138
+ }
139
+
140
+ function getReplayShell (shell) {
141
+ const value = String(shell || '').trim()
142
+ if (!value) return
143
+
144
+ const firstToken = value.split(/\s+/)[0]
145
+ if (firstToken && value.includes('{0}')) return firstToken
146
+ if (!/\s/.test(value)) return value
147
+ }
148
+
149
+ function tokenizeShellTemplate (shell) {
150
+ return String(shell || '').trim().split(/\s+/).filter(Boolean)
151
+ }
152
+
153
+ function isBourneShell (executable) {
154
+ const basename = path.basename(String(executable || ''))
155
+ return basename === 'bash' || basename === 'sh' || basename === 'zsh'
156
+ }
157
+
158
+ module.exports = {
159
+ getCiWiringCommand,
160
+ getDatadogCleanCommand,
161
+ getInlineDatadogInitialization,
162
+ getLocalValidationCommand,
163
+ }
@@ -0,0 +1,133 @@
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 { parseBoundedJson } = require('./bounded-json')
8
+ const { validateManifest } = require('./manifest-schema')
9
+
10
+ const MAX_MANIFEST_BYTES = 5 * 1024 * 1024
11
+ const MAX_MANIFEST_COLLECTION_ENTRIES = 50_000
12
+ const MAX_MANIFEST_ERROR_BYTES = 16 * 1024
13
+ const MAX_MANIFEST_NESTING_DEPTH = 64
14
+ const MAX_MANIFEST_STRING_BYTES = 256 * 1024
15
+
16
+ function loadManifest (manifestPath) {
17
+ const resolvedPath = path.resolve(manifestPath)
18
+ const stat = fs.lstatSync(resolvedPath)
19
+ if (stat.isSymbolicLink() || !stat.isFile()) {
20
+ throw new Error(`Validation manifest must be a regular file, not a symbolic link: ${resolvedPath}`)
21
+ }
22
+ if (stat.size > MAX_MANIFEST_BYTES) {
23
+ throw new Error(`Validation manifest exceeds the ${MAX_MANIFEST_BYTES}-byte size limit: ${resolvedPath}`)
24
+ }
25
+ const raw = fs.readFileSync(resolvedPath)
26
+ const manifest = parseBoundedJson(raw, {
27
+ label: 'Validation manifest JSON',
28
+ maxCollectionEntries: MAX_MANIFEST_COLLECTION_ENTRIES,
29
+ maxNestingDepth: MAX_MANIFEST_NESTING_DEPTH,
30
+ maxStringBytes: MAX_MANIFEST_STRING_BYTES,
31
+ }).value
32
+
33
+ const errors = validateManifest(manifest)
34
+ if (errors.length > 0) {
35
+ throw new Error(`Invalid validation manifest:\n- ${renderErrors(errors)}`)
36
+ }
37
+ manifest.__path = resolvedPath
38
+ Object.defineProperty(manifest, '__sourceSha256', {
39
+ configurable: false,
40
+ enumerable: false,
41
+ value: crypto.createHash('sha256').update(raw).digest('hex'),
42
+ writable: false,
43
+ })
44
+ if (path.dirname(resolvedPath) !== path.resolve(manifest.repository.root)) {
45
+ throw new Error(`Validation manifest must be stored directly in repository.root: ${resolvedPath}`)
46
+ }
47
+ validatePhysicalManifestPaths(manifest)
48
+
49
+ return manifest
50
+ }
51
+
52
+ function renderErrors (errors) {
53
+ const rendered = errors.join('\n- ')
54
+ if (Buffer.byteLength(rendered) <= MAX_MANIFEST_ERROR_BYTES) return rendered
55
+ return `${Buffer.from(rendered).subarray(0, MAX_MANIFEST_ERROR_BYTES).toString('utf8')}\n- Additional errors omitted.`
56
+ }
57
+
58
+ function validatePhysicalManifestPaths (manifest) {
59
+ const root = fs.realpathSync(manifest.repository.root)
60
+
61
+ for (const [label, candidate] of getManifestPaths(manifest)) {
62
+ if (typeof candidate !== 'string' || !path.isAbsolute(candidate)) continue
63
+ let existing = candidate
64
+ while (!fs.existsSync(existing) && path.dirname(existing) !== existing) existing = path.dirname(existing)
65
+ const physical = fs.realpathSync(existing)
66
+ if (!isPathInside(root, physical)) {
67
+ throw new Error(`Validation manifest ${label} resolves outside repository.root: ${candidate}`)
68
+ }
69
+ }
70
+ }
71
+
72
+ function getManifestPaths (manifest) {
73
+ const paths = []
74
+ for (const [frameworkIndex, framework] of (manifest.frameworks || []).entries()) {
75
+ const prefix = `frameworks[${frameworkIndex}]`
76
+ paths.push(
77
+ [`${prefix}.project.root`, framework.project?.root],
78
+ [`${prefix}.project.packageJson`, framework.project?.packageJson],
79
+ [`${prefix}.ciWiring.configFile`, framework.ciWiring?.configFile],
80
+ [`${prefix}.ciWiring.workingDirectory`, framework.ciWiring?.workingDirectory]
81
+ )
82
+ for (const [index, configFile] of (framework.project?.configFiles || []).entries()) {
83
+ paths.push([`${prefix}.project.configFiles[${index}]`, configFile])
84
+ }
85
+ for (const [name, command] of getCommands(framework)) {
86
+ paths.push([`${prefix}.${name}.cwd`, command.cwd])
87
+ for (const [outputIndex, outputPath] of (command.outputPaths || []).entries()) {
88
+ paths.push([`${prefix}.${name}.outputPaths[${outputIndex}]`, outputPath])
89
+ }
90
+ }
91
+
92
+ const strategy = framework.generatedTestStrategy
93
+ paths.push([`${prefix}.generatedTestStrategy.testDirectory`, strategy?.testDirectory])
94
+ for (const [index, file] of (strategy?.files || []).entries()) {
95
+ paths.push([`${prefix}.generatedTestStrategy.files[${index}].path`, file.path])
96
+ }
97
+ for (const [index, cleanupPath] of (strategy?.cleanupPaths || []).entries()) {
98
+ paths.push([`${prefix}.generatedTestStrategy.cleanupPaths[${index}]`, cleanupPath])
99
+ }
100
+ for (const [scenarioIndex, scenario] of (strategy?.scenarios || []).entries()) {
101
+ for (const [identityIndex, identity] of (scenario?.testIdentities || []).entries()) {
102
+ paths.push([
103
+ `${prefix}.generatedTestStrategy.scenarios[${scenarioIndex}].testIdentities[${identityIndex}].file`,
104
+ identity.file,
105
+ ])
106
+ }
107
+ }
108
+ }
109
+ return paths
110
+ }
111
+
112
+ function getCommands (framework) {
113
+ const commands = []
114
+ for (const name of ['existingTestCommand', 'ciWiringCommand']) {
115
+ if (framework[name]) commands.push([name, framework[name]])
116
+ }
117
+ for (const [index, command] of (framework.setup?.commands || []).entries()) {
118
+ commands.push([`setup.commands[${index}]`, command])
119
+ }
120
+ for (const [index, scenario] of (framework.generatedTestStrategy?.scenarios || []).entries()) {
121
+ if (scenario?.runCommand) {
122
+ commands.push([`generatedTestStrategy.scenarios[${index}].runCommand`, scenario.runCommand])
123
+ }
124
+ }
125
+ return commands
126
+ }
127
+
128
+ function isPathInside (root, filename) {
129
+ const relative = path.relative(root, filename)
130
+ return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))
131
+ }
132
+
133
+ module.exports = { loadManifest }