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.
- package/README.electron.md +7 -0
- package/README.md +17 -0
- package/ci/diagnose.js +2100 -0
- package/ci/init.js +23 -2
- package/ci/runbook.md +198 -0
- package/ci/test-optimization-validation/approval-artifacts.js +143 -0
- package/ci/test-optimization-validation/approval.js +299 -0
- package/ci/test-optimization-validation/artifact-id.js +20 -0
- package/ci/test-optimization-validation/bounded-json.js +121 -0
- package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
- package/ci/test-optimization-validation/ci-discovery.js +181 -0
- package/ci/test-optimization-validation/ci-remediation.js +210 -0
- package/ci/test-optimization-validation/cli.js +903 -0
- package/ci/test-optimization-validation/command-blocker.js +81 -0
- package/ci/test-optimization-validation/command-output-policy.js +206 -0
- package/ci/test-optimization-validation/command-runner.js +707 -0
- package/ci/test-optimization-validation/command-suitability.js +471 -0
- package/ci/test-optimization-validation/executable-approval.js +48 -0
- package/ci/test-optimization-validation/executable.js +480 -0
- package/ci/test-optimization-validation/generated-file-policy.js +63 -0
- package/ci/test-optimization-validation/generated-files.js +351 -0
- package/ci/test-optimization-validation/generated-verifier.js +196 -0
- package/ci/test-optimization-validation/init-probe-preload.js +163 -0
- package/ci/test-optimization-validation/init-probe.js +246 -0
- package/ci/test-optimization-validation/late-initialization.js +63 -0
- package/ci/test-optimization-validation/local-command.js +163 -0
- package/ci/test-optimization-validation/manifest-loader.js +133 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
- package/ci/test-optimization-validation/manifest-schema.js +862 -0
- package/ci/test-optimization-validation/offline-fixtures.js +327 -0
- package/ci/test-optimization-validation/offline-output.js +406 -0
- package/ci/test-optimization-validation/payload-normalizer.js +72 -0
- package/ci/test-optimization-validation/plan-writer.js +1120 -0
- package/ci/test-optimization-validation/preflight-runner.js +114 -0
- package/ci/test-optimization-validation/redaction.js +331 -0
- package/ci/test-optimization-validation/report-writer.js +1508 -0
- package/ci/test-optimization-validation/safe-files.js +203 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
- package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
- package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
- package/ci/test-optimization-validation/setup-runner.js +97 -0
- package/ci/test-optimization-validation/static-diagnosis.js +159 -0
- package/ci/test-optimization-validation/test-output.js +129 -0
- package/ci/test-optimization-validation-manifest.schema.json +1 -0
- package/ci/validate-test-optimization.js +3 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +121 -8
- package/init.js +18 -0
- package/initialize.mjs +1 -1
- package/loader-hook.mjs +28 -18
- package/openfeature.d.ts +1 -0
- package/openfeature.js +4 -0
- package/package.json +10 -7
- package/packages/datadog-instrumentations/src/ai.js +8 -2
- package/packages/datadog-instrumentations/src/child_process.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
- package/packages/datadog-instrumentations/src/cucumber.js +20 -7
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
- package/packages/datadog-instrumentations/src/jest.js +26 -0
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
- package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
- package/packages/datadog-instrumentations/src/mongodb.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +24 -0
- package/packages/datadog-instrumentations/src/nyc.js +0 -2
- package/packages/datadog-instrumentations/src/oracledb.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +159 -22
- package/packages/datadog-instrumentations/src/process.js +3 -0
- package/packages/datadog-instrumentations/src/router.js +18 -8
- package/packages/datadog-instrumentations/src/selenium.js +52 -26
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
- package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
- package/packages/datadog-instrumentations/src/ws.js +2 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
- package/packages/datadog-plugin-cypress/src/index.js +6 -1
- package/packages/datadog-plugin-cypress/src/support.js +92 -26
- package/packages/datadog-plugin-electron/src/ipc.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +87 -45
- package/packages/datadog-plugin-graphql/src/validate.js +2 -2
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +177 -64
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-ws/src/close.js +2 -1
- package/packages/datadog-plugin-ws/src/producer.js +2 -1
- package/packages/datadog-plugin-ws/src/receiver.js +2 -1
- package/packages/datadog-plugin-ws/src/server.js +2 -1
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
- package/packages/dd-trace/src/azure_metadata.js +2 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
- package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +66 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
- package/packages/dd-trace/src/exporters/common/request.js +59 -30
- package/packages/dd-trace/src/exporters/common/url.js +15 -1
- package/packages/dd-trace/src/feature-registry.js +10 -3
- package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
- package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
- package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
- package/packages/dd-trace/src/llmobs/noop.js +6 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
- package/packages/dd-trace/src/llmobs/sdk.js +17 -0
- package/packages/dd-trace/src/llmobs/tagger.js +68 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
- package/packages/dd-trace/src/llmobs/util.js +32 -0
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
- package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
- package/packages/dd-trace/src/openfeature/index.js +0 -2
- package/packages/dd-trace/src/openfeature/noop.js +1 -28
- package/packages/dd-trace/src/openfeature/register.js +16 -27
- package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
- package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
- package/packages/dd-trace/src/plugins/util/git.js +3 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
- package/packages/dd-trace/src/plugins/util/test.js +56 -4
- package/packages/dd-trace/src/proxy.js +84 -7
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -14
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +26 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- 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 }
|