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.
- 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 +11 -8
- 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 +31 -19
- 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/channel.js +74 -0
- 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 +44 -17
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +79 -32
- 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 +162 -46
- 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 +3 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +14 -28
- package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
- 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 +3 -2
- 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-mocha/src/index.js +2 -1
- package/packages/datadog-plugin-playwright/src/index.js +181 -65
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-vitest/src/index.js +5 -5
- 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/profiling/profilers/space.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
- 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,780 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const { buildCiCommandCandidate } = require('../ci-command-candidate')
|
|
7
|
+
const { buildCiRemediation } = require('../ci-remediation')
|
|
8
|
+
const { getCommandBlocker } = require('../command-blocker')
|
|
9
|
+
const { serializeCommand } = require('../command-runner')
|
|
10
|
+
const { getFrameworkCiDiscoveryContradiction } = require('../ci-discovery')
|
|
11
|
+
const { runInitializationProbe } = require('../init-probe')
|
|
12
|
+
const { findLateInitialization } = require('../late-initialization')
|
|
13
|
+
const { getCiWiringCommand } = require('../local-command')
|
|
14
|
+
const { ensureSafeDirectory } = require('../safe-files')
|
|
15
|
+
const { getMissingEventDiagnosis, summarizeTestOutput } = require('./basic-reporting')
|
|
16
|
+
const {
|
|
17
|
+
basicEventEvidence,
|
|
18
|
+
error,
|
|
19
|
+
fail,
|
|
20
|
+
findInterestingLines,
|
|
21
|
+
frameworkOutDir,
|
|
22
|
+
hasAllBasicEventTypes,
|
|
23
|
+
incomplete,
|
|
24
|
+
inconclusive,
|
|
25
|
+
pass,
|
|
26
|
+
runInstrumentedCommand,
|
|
27
|
+
tailInterestingLines,
|
|
28
|
+
} = require('./helpers')
|
|
29
|
+
|
|
30
|
+
const INCONCLUSIVE_CI_WIRING_FAILURES = new Set([
|
|
31
|
+
'package-manager-filesystem-blocked',
|
|
32
|
+
'package-manager-version-mismatch',
|
|
33
|
+
'watchman-filesystem-blocked',
|
|
34
|
+
'ci-wiring-command-failed-before-tests',
|
|
35
|
+
'ci-wiring-command-timed-out',
|
|
36
|
+
'ci-wiring-no-observed-tests',
|
|
37
|
+
'ci-wiring-project-filter-mismatch',
|
|
38
|
+
'ci-wiring-test-filter-mismatch',
|
|
39
|
+
'no-test-optimization-events',
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
async function runCiWiring ({ manifest, framework, out, options, basicResult }) {
|
|
43
|
+
const scenarioName = 'ci-wiring'
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const command = getCiWiringCommand(framework)
|
|
47
|
+
if (!command) return getMissingCiWiringCommandResult(framework, manifest)
|
|
48
|
+
|
|
49
|
+
const outDir = frameworkOutDir(out, framework, scenarioName)
|
|
50
|
+
ensureSafeDirectory(out, outDir, 'CI wiring artifact directory')
|
|
51
|
+
const baseEvidence = getCiWiringBaseEvidence({ framework, manifest, basicResult, command })
|
|
52
|
+
const run = await runInstrumentedCommand({
|
|
53
|
+
framework,
|
|
54
|
+
out,
|
|
55
|
+
scenarioName,
|
|
56
|
+
command,
|
|
57
|
+
options,
|
|
58
|
+
ciWiring: true,
|
|
59
|
+
})
|
|
60
|
+
const { result, events } = run
|
|
61
|
+
|
|
62
|
+
const ciWiringPreflight = getComparableCiWiringPreflight(framework, command)
|
|
63
|
+
const evidence = {
|
|
64
|
+
...baseEvidence,
|
|
65
|
+
commandExitCode: result.exitCode,
|
|
66
|
+
commandTimedOut: result.timedOut,
|
|
67
|
+
commandDescription: command.description,
|
|
68
|
+
commandOutputSummary: summarizeTestOutput(result.stdout, result.stderr),
|
|
69
|
+
ciCommandExecution: {
|
|
70
|
+
mode: 'full-replay',
|
|
71
|
+
fullReplayRan: true,
|
|
72
|
+
},
|
|
73
|
+
preflight: summarizePreflight(ciWiringPreflight),
|
|
74
|
+
settingsLoadedFromCache: run.offline.inputs.settings?.status === 'loaded',
|
|
75
|
+
offlineExporterCapture: {
|
|
76
|
+
mode: run.offline.captureMode,
|
|
77
|
+
completionCount: run.offline.completionCount,
|
|
78
|
+
observedEventCount: run.offline.observedEventCount,
|
|
79
|
+
retainedEventCount: run.offline.retainedEventCount,
|
|
80
|
+
sampled: run.offline.sampled,
|
|
81
|
+
},
|
|
82
|
+
offlineExporterSummary: run.offline.summary,
|
|
83
|
+
...basicEventEvidence(events),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!hasAllBasicEventTypes(events)) {
|
|
87
|
+
const commandFailure = summarizeCiCommandFailure(result, evidence)
|
|
88
|
+
if (commandFailure.kind !== 'ci-wiring-command-result-unknown') {
|
|
89
|
+
evidence.commandFailure = commandFailure
|
|
90
|
+
}
|
|
91
|
+
evidence.debugSignals = summarizeCiDebugSignals(result)
|
|
92
|
+
const probe = await maybeRunInitializationProbe({ command, framework, options, outDir, result, evidence })
|
|
93
|
+
if (probe.summary) evidence.initializationProbe = probe.summary
|
|
94
|
+
evidence.monorepoFindings = getMonorepoFindings({ framework, command, probe: probe.summary })
|
|
95
|
+
evidence.eventLevelFailure = getCiWiringEventFailure({ framework, result, evidence, basicResult })
|
|
96
|
+
if (isInconclusiveCiWiringFailure(evidence.eventLevelFailure)) {
|
|
97
|
+
return inconclusive(
|
|
98
|
+
framework,
|
|
99
|
+
scenarioName,
|
|
100
|
+
evidence.eventLevelFailure.summary,
|
|
101
|
+
evidence,
|
|
102
|
+
outDir,
|
|
103
|
+
probe.artifacts
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return fail(framework, scenarioName, evidence.eventLevelFailure.summary, evidence, outDir, probe.artifacts)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result.exitCode === 0) {
|
|
110
|
+
return pass(
|
|
111
|
+
framework,
|
|
112
|
+
scenarioName,
|
|
113
|
+
'The CI test command emitted session, module, suite, and test events with the initialization configured ' +
|
|
114
|
+
'by CI.',
|
|
115
|
+
evidence,
|
|
116
|
+
outDir
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (matchesPreflightExitCode(ciWiringPreflight, result.exitCode)) {
|
|
121
|
+
evidence.commandExitMatchesPreflight = true
|
|
122
|
+
return pass(
|
|
123
|
+
framework,
|
|
124
|
+
scenarioName,
|
|
125
|
+
'The CI test command emitted session, module, suite, and test events with the initialization configured ' +
|
|
126
|
+
'by CI. ' +
|
|
127
|
+
`The command exited ${result.exitCode}, matching the dd-trace-less preflight run.`,
|
|
128
|
+
evidence,
|
|
129
|
+
outDir
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
evidence.commandExitMatchesPreflight = false
|
|
134
|
+
return fail(
|
|
135
|
+
framework,
|
|
136
|
+
scenarioName,
|
|
137
|
+
`CI wiring emitted Test Optimization events, but the command exited ${result.exitCode}.`,
|
|
138
|
+
evidence,
|
|
139
|
+
outDir
|
|
140
|
+
)
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return error(framework, scenarioName, err)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isInconclusiveCiWiringFailure (failure) {
|
|
147
|
+
return INCONCLUSIVE_CI_WIRING_FAILURES.has(failure.kind)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getCiWiringBaseEvidence ({ framework, manifest, basicResult, command }) {
|
|
151
|
+
return {
|
|
152
|
+
commandDescription: command.description,
|
|
153
|
+
ciCommandCandidate: buildCiCommandCandidate(framework),
|
|
154
|
+
ciWiring: framework.ciWiring,
|
|
155
|
+
ciRemediation: buildCiRemediation(framework),
|
|
156
|
+
nodeOptionsRemoval: findNodeOptionsRemoval(framework, manifest),
|
|
157
|
+
existingDatadogInitScripts: findDatadogInitScripts(manifest, framework),
|
|
158
|
+
lateInitialization: findLateInitialization(manifest, framework),
|
|
159
|
+
directInitializationBasicReporting: summarizeBasicReportingResult(basicResult),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function maybeRunInitializationProbe ({ command, framework, options, outDir, result, evidence }) {
|
|
164
|
+
if (result.timedOut === true || evidence.commandFailure?.blockedByExecutionEnvironment ||
|
|
165
|
+
evidence.commandFailure?.toolchainBlocked) return {}
|
|
166
|
+
if (!commandOutputShowsTestsRan(evidence.commandOutputSummary)) return {}
|
|
167
|
+
if (evidence.nodeOptionsRemoval) {
|
|
168
|
+
return {
|
|
169
|
+
summary: {
|
|
170
|
+
ran: false,
|
|
171
|
+
skippedBecauseConfigurationProvesRemoval: true,
|
|
172
|
+
reason: `The package script expansion ${evidence.nodeOptionsRemoval.command} explicitly removes ` +
|
|
173
|
+
'NODE_OPTIONS before the test runner starts.',
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
return await runInitializationProbe({
|
|
180
|
+
command,
|
|
181
|
+
framework,
|
|
182
|
+
options,
|
|
183
|
+
outDir,
|
|
184
|
+
})
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return {
|
|
187
|
+
summary: {
|
|
188
|
+
ran: false,
|
|
189
|
+
error: err && err.message ? err.message : String(err),
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getMissingCiWiringCommandResult (framework, manifest) {
|
|
196
|
+
const contradiction = getFrameworkCiDiscoveryContradiction(framework, manifest)
|
|
197
|
+
if (contradiction) {
|
|
198
|
+
return incomplete(framework, 'ci-wiring',
|
|
199
|
+
`CI wiring was not replayed: ${contradiction.reason} No live CI-wiring conclusion was reached.`, {
|
|
200
|
+
ciCommandCandidate: buildCiCommandCandidate(framework),
|
|
201
|
+
ciWiring: framework.ciWiring,
|
|
202
|
+
ciDiscovery: contradiction.ciDiscovery,
|
|
203
|
+
recommendation: contradiction.recommendation,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const ciWiring = framework.ciWiring
|
|
208
|
+
const diagnosis = ciWiring?.replayBlocker ||
|
|
209
|
+
ciWiring?.diagnosis ||
|
|
210
|
+
ciWiring?.reason ||
|
|
211
|
+
'No replayable CI wiring command was provided in the manifest.'
|
|
212
|
+
const ciRemediation = buildCiRemediation(framework)
|
|
213
|
+
const evidence = {
|
|
214
|
+
ciCommandCandidate: buildCiCommandCandidate(framework),
|
|
215
|
+
ciWiring,
|
|
216
|
+
ciRemediation,
|
|
217
|
+
recommendation: 'Resolve the recorded CI replay blocker, then add ciWiringCommand and rerun validation.',
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return incomplete(
|
|
221
|
+
framework,
|
|
222
|
+
'ci-wiring',
|
|
223
|
+
`CI wiring was not replayed: ${diagnosis} No live CI-wiring conclusion was reached.`,
|
|
224
|
+
evidence
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getCiWiringEventFailure ({ framework, result, evidence, basicResult }) {
|
|
229
|
+
const localFailure = getMissingEventDiagnosis({ framework, result, evidence })
|
|
230
|
+
const testsRan = commandOutputShowsTestsRan(evidence.commandOutputSummary)
|
|
231
|
+
|
|
232
|
+
if (testsRan) {
|
|
233
|
+
return {
|
|
234
|
+
...localFailure,
|
|
235
|
+
kind: 'ci-wiring-no-test-optimization-events',
|
|
236
|
+
summary: getCiWiringTestsRanSummary({ basicResult, evidence, framework }),
|
|
237
|
+
recommendation: getCiWiringTestsRanRecommendation({ basicResult, evidence, framework }),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const commandFailure = evidence.commandFailure || summarizeCiCommandFailure(result, evidence)
|
|
242
|
+
if (commandFailure.kind !== 'ci-wiring-command-result-unknown') {
|
|
243
|
+
return {
|
|
244
|
+
...localFailure,
|
|
245
|
+
kind: commandFailure.kind,
|
|
246
|
+
missingLevels: localFailure.missingLevels,
|
|
247
|
+
signals: commandFailure.signals,
|
|
248
|
+
summary: commandFailure.summary,
|
|
249
|
+
recommendation: commandFailure.recommendation,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...localFailure,
|
|
255
|
+
summary: 'The CI-shaped command did not emit Test Optimization events, and the validator could not determine ' +
|
|
256
|
+
'from its output whether tests ran. Review the recorded stdout/stderr artifacts for the selected CI step.',
|
|
257
|
+
recommendation: 'Verify the selected CI step is the real test step, then rerun after making the command output ' +
|
|
258
|
+
'or preflight evidence identify the test runner result.',
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getCiWiringTestsRanSummary ({ basicResult, evidence, framework }) {
|
|
263
|
+
if (evidence.nodeOptionsRemoval) {
|
|
264
|
+
return getNodeOptionsRemovalDiagnosis({ basicResult, evidence, framework })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const summary = 'The test command used by the CI job was identified and ran tests. When it ran with only the ' +
|
|
268
|
+
'environment and setup described by the CI job, no Test Optimization events reached the offline event artifact.'
|
|
269
|
+
const probeSummary = getInitializationProbeSummary(evidence.initializationProbe, framework)
|
|
270
|
+
const lateInitializationSummary = getLateInitializationSummary(evidence.lateInitialization)
|
|
271
|
+
|
|
272
|
+
if (basicResult?.status === 'pass') {
|
|
273
|
+
return `${summary}${lateInitializationSummary} ` +
|
|
274
|
+
'The same selected test command ' +
|
|
275
|
+
'reported test data when the ' +
|
|
276
|
+
'validator supplied the ' +
|
|
277
|
+
'required Datadog initialization directly, so this repository can report when dd-trace is initialized ' +
|
|
278
|
+
`correctly.${probeSummary}`
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return `${summary}${lateInitializationSummary}${probeSummary}`
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getCiWiringTestsRanRecommendation ({ basicResult, evidence, framework }) {
|
|
285
|
+
const existingInitScripts = evidence.existingDatadogInitScripts || []
|
|
286
|
+
const lateInitialization = evidence.lateInitialization || []
|
|
287
|
+
const probeReachedTestRunner = evidence.initializationProbe?.ran === true &&
|
|
288
|
+
evidence.initializationProbe.reachedTestRunnerProcess === true
|
|
289
|
+
const nodeOptionsRemoval = evidence.nodeOptionsRemoval
|
|
290
|
+
let recommendation
|
|
291
|
+
|
|
292
|
+
if (nodeOptionsRemoval) {
|
|
293
|
+
const source = nodeOptionsRemoval.scriptName && nodeOptionsRemoval.packageJson
|
|
294
|
+
? `Script \`${nodeOptionsRemoval.scriptName}\` in \`${nodeOptionsRemoval.packageJson}\``
|
|
295
|
+
: 'The package script'
|
|
296
|
+
recommendation = `${source} clears NODE_OPTIONS before the test runner starts. Remove the empty ` +
|
|
297
|
+
'`NODE_OPTIONS=` assignment, or pass the CI-provided `-r dd-trace/ci/init` preload to the next command.'
|
|
298
|
+
} else if (lateInitialization.length > 0) {
|
|
299
|
+
const setupFiles = lateInitialization.map(finding => `\`${finding.setupFile}\``).join(', ')
|
|
300
|
+
recommendation = `Move Test Optimization initialization out of Vitest setup file ${setupFiles}. ` +
|
|
301
|
+
'Vitest setup files run after the test runner starts, which is too late for dd-trace to instrument the ' +
|
|
302
|
+
'runner. Set `NODE_OPTIONS=-r dd-trace/ci/init` on the CI test command instead.'
|
|
303
|
+
} else if (existingInitScripts.length > 0) {
|
|
304
|
+
const scriptNames = existingInitScripts.map(script => `\`${script.name}\``).join(', ')
|
|
305
|
+
recommendation = `The package already defines ${scriptNames} with the required ` +
|
|
306
|
+
'`dd-trace/ci/init` preload. Update the identified CI test step to invoke that script, or copy its ' +
|
|
307
|
+
'`NODE_OPTIONS` initialization into the CI test command.'
|
|
308
|
+
} else if (probeReachedTestRunner) {
|
|
309
|
+
recommendation = `${evidence.ciRemediation?.summary || buildCiRemediation(framework).summary} ` +
|
|
310
|
+
'The NODE_OPTIONS probe reached the test runner for this command shape, so no package-manager or wrapper ' +
|
|
311
|
+
'change is needed.'
|
|
312
|
+
} else {
|
|
313
|
+
recommendation = 'Verify that the CI workflow sets NODE_OPTIONS with dd-trace/ci/init for the final test ' +
|
|
314
|
+
'runner, and that any package manager, monorepo runner, or wrapper preserves it.'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (basicResult?.status === 'pass' && !nodeOptionsRemoval && lateInitialization.length === 0 &&
|
|
318
|
+
!probeReachedTestRunner) {
|
|
319
|
+
return `${recommendation} Compare the passing direct-initialization command with the CI job command to find ` +
|
|
320
|
+
'where the Datadog setup differs.'
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return recommendation
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getNodeOptionsRemovalDiagnosis ({ basicResult, evidence, framework }) {
|
|
327
|
+
const finding = evidence.nodeOptionsRemoval
|
|
328
|
+
const frameworkName = getDisplayFrameworkName(framework.framework)
|
|
329
|
+
const ciCommand = evidence.ciCommandCandidate?.command
|
|
330
|
+
? `When CI runs \`${evidence.ciCommandCandidate.command}\`, `
|
|
331
|
+
: 'In the selected CI test job, '
|
|
332
|
+
const source = finding.scriptName && finding.packageJson
|
|
333
|
+
? `script \`${finding.scriptName}\` in \`${finding.packageJson}\``
|
|
334
|
+
: 'a package script'
|
|
335
|
+
const directResult = basicResult?.status === 'pass'
|
|
336
|
+
? ` When the same ${frameworkName} test command runs with ` +
|
|
337
|
+
'`NODE_OPTIONS=-r dd-trace/ci/init` supplied directly, it reports test data successfully.'
|
|
338
|
+
: ''
|
|
339
|
+
|
|
340
|
+
return 'The CI test command ran tests, but no Test Optimization events reached the offline event artifact. ' +
|
|
341
|
+
`${ciCommand}` +
|
|
342
|
+
`${source} expands to \`${finding.command}\`. The empty \`NODE_OPTIONS=\` assignment clears the Datadog ` +
|
|
343
|
+
`preload before ${frameworkName} starts.${directResult}`
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function findNodeOptionsRemoval (framework, manifest) {
|
|
347
|
+
const commands = framework.ciWiring?.packageScriptExpansionChain || []
|
|
348
|
+
for (const command of commands) {
|
|
349
|
+
if (typeof command !== 'string') continue
|
|
350
|
+
if (/(?:^|\s)NODE_OPTIONS\s*=\s*(?=\s|$)/.test(command) ||
|
|
351
|
+
/(?:^|\s)unset\s+NODE_OPTIONS(?:\s|$)/.test(command) ||
|
|
352
|
+
/(?:^|\s)env\s+-u\s+NODE_OPTIONS(?:\s|$)/.test(command)) {
|
|
353
|
+
return {
|
|
354
|
+
command,
|
|
355
|
+
...findPackageScriptSource(manifest, framework, command),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function findPackageScriptSource (manifest, framework, command) {
|
|
362
|
+
const roots = new Set([manifest?.repository?.root, framework.project?.root].filter(Boolean))
|
|
363
|
+
for (const root of roots) {
|
|
364
|
+
const packageJsonPath = path.join(root, 'package.json')
|
|
365
|
+
let packageJson
|
|
366
|
+
try {
|
|
367
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
368
|
+
} catch {
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const [scriptName, scriptCommand] of Object.entries(packageJson.scripts || {})) {
|
|
373
|
+
if (scriptCommand === command) return { packageJson: packageJsonPath, scriptName }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function getLateInitializationSummary (findings) {
|
|
380
|
+
if (!Array.isArray(findings) || findings.length === 0) return ''
|
|
381
|
+
const setupFiles = findings.map(finding => `\`${finding.setupFile}\``).join(', ')
|
|
382
|
+
return ' Static configuration inspection found Test Optimization initialization in Vitest setup file ' +
|
|
383
|
+
`${setupFiles}. Vitest loads setup files after the runner starts, so this initialization is too late to ` +
|
|
384
|
+
'instrument the test runner.'
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Finds package scripts that already set the required Test Optimization preload.
|
|
389
|
+
*
|
|
390
|
+
* @param {object|undefined} manifest normalized validation manifest
|
|
391
|
+
* @param {object} framework manifest framework entry
|
|
392
|
+
* @returns {{name: string, packageJson: string}[]} matching package scripts
|
|
393
|
+
*/
|
|
394
|
+
function findDatadogInitScripts (manifest, framework) {
|
|
395
|
+
const roots = new Set([framework.project?.root, manifest?.repository?.root].filter(Boolean))
|
|
396
|
+
const scripts = []
|
|
397
|
+
|
|
398
|
+
for (const root of roots) {
|
|
399
|
+
let packageJson
|
|
400
|
+
try {
|
|
401
|
+
packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
402
|
+
} catch {
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const [name, command] of Object.entries(packageJson.scripts || {})) {
|
|
407
|
+
if (typeof command !== 'string' || !/\bNODE_OPTIONS\s*=.*\bdd-trace\/ci\/init\b/.test(command)) {
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
scripts.push({
|
|
411
|
+
name,
|
|
412
|
+
packageJson: path.join(root, 'package.json'),
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return scripts
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function summarizeCiCommandFailure (result, evidence) {
|
|
421
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`
|
|
422
|
+
const testsRan = commandOutputShowsTestsRan(evidence.commandOutputSummary || [])
|
|
423
|
+
const common = {
|
|
424
|
+
exitCode: result.exitCode,
|
|
425
|
+
stderrExcerpt: tailInterestingLines(result.stderr),
|
|
426
|
+
stdoutExcerpt: tailInterestingLines(result.stdout),
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (testsRan) {
|
|
430
|
+
return {
|
|
431
|
+
...common,
|
|
432
|
+
kind: 'ci-wiring-command-result-unknown',
|
|
433
|
+
signals: [],
|
|
434
|
+
summary: 'The CI-shaped command ran tests; missing Test Optimization events are reported separately.',
|
|
435
|
+
recommendation: 'Review CI wiring event failure evidence.',
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const commandBlocker = getCommandBlocker(result)
|
|
440
|
+
if (commandBlocker) return { ...common, ...commandBlocker }
|
|
441
|
+
|
|
442
|
+
const preloadFailure = detectDatadogPreloadResolutionFailure(output)
|
|
443
|
+
if (preloadFailure) {
|
|
444
|
+
return {
|
|
445
|
+
...common,
|
|
446
|
+
kind: 'ci-wiring-preload-resolution-failed',
|
|
447
|
+
signals: preloadFailure.signals,
|
|
448
|
+
summary: 'The CI-shaped command failed before tests started because Node could not resolve the Test ' +
|
|
449
|
+
'Optimization preload `dd-trace/ci/init` from the command working directory.',
|
|
450
|
+
recommendation: 'Make sure `dd-trace` is installed where the CI command starts, or run the CI command from ' +
|
|
451
|
+
'the package working directory that can resolve `dd-trace/ci/init`. After the preload resolves, rerun CI ' +
|
|
452
|
+
'wiring validation to check whether the required Datadog setup reaches the final test runner.',
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (result.timedOut === true) {
|
|
457
|
+
return {
|
|
458
|
+
...common,
|
|
459
|
+
kind: 'ci-wiring-command-timed-out',
|
|
460
|
+
signals: [],
|
|
461
|
+
summary: 'The CI-shaped command timed out before the validator could observe Test Optimization events.',
|
|
462
|
+
recommendation: 'Choose a smaller representative CI test command or record the setup needed to make the ' +
|
|
463
|
+
'selected command complete within the validation timeout.',
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (result.exitCode !== 0 && !testsRan) {
|
|
468
|
+
const termination = Number.isInteger(result.exitCode) ? `exited ${result.exitCode}` : 'failed'
|
|
469
|
+
const projectFilterMismatch = output.match(/No projects matched the filter\s+["']([^"']+)["']/i)
|
|
470
|
+
if (projectFilterMismatch) {
|
|
471
|
+
return {
|
|
472
|
+
...common,
|
|
473
|
+
kind: 'ci-wiring-project-filter-mismatch',
|
|
474
|
+
signals: [projectFilterMismatch[0]],
|
|
475
|
+
summary: `The CI-shaped command ${termination} before tests because its added project filter ` +
|
|
476
|
+
`\`${projectFilterMismatch[1]}\` is not exposed by the configuration loaded from the CI working ` +
|
|
477
|
+
'directory. No CI wiring conclusion was reached.',
|
|
478
|
+
recommendation: 'Remove the invented project selector. Choose a representative test from a project the ' +
|
|
479
|
+
'original CI command actually loads, or mark CI replay unavailable when the real CI wrapper cannot be ' +
|
|
480
|
+
'focused safely.',
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (/No test files found/i.test(output)) {
|
|
485
|
+
return {
|
|
486
|
+
...common,
|
|
487
|
+
kind: 'ci-wiring-test-filter-mismatch',
|
|
488
|
+
signals: findInterestingLines(output, [/No test files found/, /filter:/, /include:/]),
|
|
489
|
+
summary: `The CI-shaped command ${termination} before tests because its focused test filter matched no ` +
|
|
490
|
+
'files in the project configuration loaded by CI. No CI wiring conclusion was reached.',
|
|
491
|
+
recommendation: 'Choose a real test included by the exact CI-loaded project and use it consistently for ' +
|
|
492
|
+
'Basic Reporting and CI wiring. If the CI command cannot be focused without changing its project, cwd, ' +
|
|
493
|
+
'or wrapper chain, mark CI replay unavailable.',
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const buildErrors = findInterestingLines(output, [
|
|
498
|
+
/Cannot find module/,
|
|
499
|
+
/Module not found/,
|
|
500
|
+
/Error \[ERR_MODULE_NOT_FOUND\]/,
|
|
501
|
+
/Could not resolve /,
|
|
502
|
+
/command not found/,
|
|
503
|
+
])
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
...common,
|
|
507
|
+
buildErrors,
|
|
508
|
+
kind: 'ci-wiring-command-failed-before-tests',
|
|
509
|
+
signals: buildErrors,
|
|
510
|
+
summary: `The CI-shaped command ${termination} before the validator observed any tests running. ` +
|
|
511
|
+
'No CI wiring conclusion about Test Optimization initialization was reached for this command.',
|
|
512
|
+
recommendation: 'Fix or document the command/setup failure first. CI wiring can only be interpreted after ' +
|
|
513
|
+
'the selected CI-shaped command reaches the test runner.',
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (result.exitCode === 0 && !testsRan) {
|
|
518
|
+
return {
|
|
519
|
+
...common,
|
|
520
|
+
kind: 'ci-wiring-no-observed-tests',
|
|
521
|
+
signals: [],
|
|
522
|
+
summary: 'The CI-shaped command exited 0, but the validator did not observe test-runner output or Test ' +
|
|
523
|
+
'Optimization events.',
|
|
524
|
+
recommendation: 'Verify that the selected CI step actually runs tests. If it is a wrapper with unusual ' +
|
|
525
|
+
'output, record preflight observedTestCount or choose a representative command whose output identifies ' +
|
|
526
|
+
'the test result.',
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
...common,
|
|
532
|
+
kind: 'ci-wiring-command-result-unknown',
|
|
533
|
+
signals: [],
|
|
534
|
+
summary: 'The CI-shaped command result did not explain why Test Optimization events were missing.',
|
|
535
|
+
recommendation: 'Review stdout, stderr, and debug lines for the selected CI-shaped command.',
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function getComparableCiWiringPreflight (framework, command) {
|
|
540
|
+
if (framework.ciWiringPreflight?.ran === true) {
|
|
541
|
+
return {
|
|
542
|
+
...framework.ciWiringPreflight,
|
|
543
|
+
source: 'ciWiringPreflight',
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (commandsHaveSameExecutionShape(command, framework.existingTestCommand)) {
|
|
548
|
+
return {
|
|
549
|
+
...framework.preflight,
|
|
550
|
+
source: 'existingTestCommand',
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
ran: false,
|
|
556
|
+
reason: 'No dd-trace-less preflight result was recorded for the selected CI wiring command shape.',
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function commandsHaveSameExecutionShape (left, right) {
|
|
561
|
+
if (!left || !right) return false
|
|
562
|
+
if (left.cwd !== right.cwd) return false
|
|
563
|
+
if (Boolean(left.usesShell) !== Boolean(right.usesShell)) return false
|
|
564
|
+
return serializeCommand(left) === serializeCommand(right)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function detectDatadogPreloadResolutionFailure (output) {
|
|
568
|
+
if (!/dd-trace(?:\/ci\/init)?/.test(output)) return null
|
|
569
|
+
if (!/MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND|Cannot find module|Cannot find package/.test(output)) return null
|
|
570
|
+
|
|
571
|
+
const signals = findInterestingLines(output, [
|
|
572
|
+
/Cannot find module ['"]dd-trace\/ci\/init['"]/,
|
|
573
|
+
/Cannot find package ['"]dd-trace['"]/,
|
|
574
|
+
/Error \[ERR_MODULE_NOT_FOUND\].*dd-trace\/ci\/init/,
|
|
575
|
+
/MODULE_NOT_FOUND/,
|
|
576
|
+
/internal\/preload/,
|
|
577
|
+
], 8)
|
|
578
|
+
|
|
579
|
+
return { signals }
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function summarizeCiDebugSignals (result) {
|
|
583
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`
|
|
584
|
+
const lines = findInterestingLines(output, [
|
|
585
|
+
/dd-trace/i,
|
|
586
|
+
/datadog/i,
|
|
587
|
+
/ci visibility/i,
|
|
588
|
+
/test optimization/i,
|
|
589
|
+
/ECONNREFUSED/,
|
|
590
|
+
/ECONNRESET/,
|
|
591
|
+
/ETIMEDOUT/,
|
|
592
|
+
/socket hang up/,
|
|
593
|
+
/failed to send/i,
|
|
594
|
+
/writer/i,
|
|
595
|
+
], 12)
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
debugEnvEnabled: true,
|
|
599
|
+
lines,
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function summarizeBasicReportingResult (basicResult) {
|
|
604
|
+
if (!basicResult) {
|
|
605
|
+
return {
|
|
606
|
+
ran: false,
|
|
607
|
+
reason: 'Basic Reporting was not run before CI wiring.',
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
ran: true,
|
|
613
|
+
status: basicResult.status,
|
|
614
|
+
diagnosis: basicResult.diagnosis,
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function getInitializationProbeSummary (probe, framework) {
|
|
619
|
+
if (!probe || probe.ran !== true) return ''
|
|
620
|
+
|
|
621
|
+
const frameworkName = getDisplayFrameworkName(framework.framework)
|
|
622
|
+
if (!probe.reachedAnyNodeProcess) {
|
|
623
|
+
return ' The initialization probe did not reach any Node.js process in the CI command.'
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (probe.reachedTestRunnerProcess) {
|
|
627
|
+
return ` The NODE_OPTIONS probe reached a ${frameworkName} process, so NODE_OPTIONS can reach the test ` +
|
|
628
|
+
'runner in this command shape; inspect whether the CI workflow actually configures the required Datadog ' +
|
|
629
|
+
'initialization and environment.'
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const wrappers = formatToolNames([...probe.wrapperSignals, ...probe.packageManagerSignals])
|
|
633
|
+
if (wrappers) {
|
|
634
|
+
return ` The NODE_OPTIONS probe reached ${wrappers}, but it did not appear to reach a ${frameworkName} ` +
|
|
635
|
+
'process. This usually means a package manager, monorepo runner, or wrapper removes NODE_OPTIONS before ' +
|
|
636
|
+
'the tests start.'
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return ` The NODE_OPTIONS probe reached a Node.js process, but it did not appear to reach a ${frameworkName} ` +
|
|
640
|
+
'process.'
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function getMonorepoFindings ({ framework, command, probe }) {
|
|
644
|
+
const findings = []
|
|
645
|
+
const commandText = [
|
|
646
|
+
command.description,
|
|
647
|
+
command.usesShell ? command.shellCommand : command.argv?.join(' '),
|
|
648
|
+
framework.ciWiring?.diagnosis,
|
|
649
|
+
...(framework.ciWiring?.runnerToolChain || []),
|
|
650
|
+
...(framework.ciWiring?.toolChain || []),
|
|
651
|
+
...(framework.ciWiring?.commandChain || []),
|
|
652
|
+
].filter(Boolean).join('\n')
|
|
653
|
+
|
|
654
|
+
if (/\bnx\b/i.test(commandText) || hasProbeTool(probe, 'nx')) {
|
|
655
|
+
findings.push({
|
|
656
|
+
id: 'nx-executor-env-forwarding',
|
|
657
|
+
tool: 'nx',
|
|
658
|
+
reason: 'Nx executors and wrapper scripts can sit between the CI command and the final test runner.',
|
|
659
|
+
recommendation: 'Verify that NODE_OPTIONS and Datadog environment variables are preserved by every Nx ' +
|
|
660
|
+
'target, executor, and wrapper that spawns the test runner.',
|
|
661
|
+
})
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (/\bturbo(?:repo)?\b/i.test(commandText) || hasProbeTool(probe, 'turbo')) {
|
|
665
|
+
findings.push({
|
|
666
|
+
id: 'turbo-env-pass-through',
|
|
667
|
+
tool: 'turbo',
|
|
668
|
+
reason: 'Turborepo can filter environment variables for tasks.',
|
|
669
|
+
recommendation: 'Verify turbo.json pass-through settings preserve NODE_OPTIONS and required DD_* variables ' +
|
|
670
|
+
'for test tasks.',
|
|
671
|
+
})
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (/\blage\b/i.test(commandText) || hasProbeTool(probe, 'lage')) {
|
|
675
|
+
findings.push({
|
|
676
|
+
id: 'lage-env-forwarding',
|
|
677
|
+
tool: 'lage',
|
|
678
|
+
reason: 'Lage can run package scripts through an intermediate task process.',
|
|
679
|
+
recommendation: 'Verify the Lage task and any package script it invokes preserve NODE_OPTIONS and required ' +
|
|
680
|
+
'DD_* variables for the final test runner.',
|
|
681
|
+
})
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (probe?.reachedAnyNodeProcess && !probe.reachedTestRunnerProcess && !findNodeOptionsRemoval(framework)) {
|
|
685
|
+
findings.push({
|
|
686
|
+
id: 'node-options-not-observed-in-test-runner',
|
|
687
|
+
tool: 'node',
|
|
688
|
+
reason: 'The NODE_OPTIONS probe reached an intermediate Node.js process but not the detected test runner.',
|
|
689
|
+
recommendation: 'Trace the command chain from the CI step to the test runner and find where NODE_OPTIONS is ' +
|
|
690
|
+
'removed or replaced.',
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return findings
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @param {object|undefined} probe
|
|
699
|
+
* @param {string} name
|
|
700
|
+
*/
|
|
701
|
+
function hasProbeTool (probe, name) {
|
|
702
|
+
const signals = [
|
|
703
|
+
...(probe?.wrapperSignals || []),
|
|
704
|
+
...(probe?.packageManagerSignals || []),
|
|
705
|
+
...(probe?.testRunnerSignals || []),
|
|
706
|
+
]
|
|
707
|
+
return signals.some(signal => signal.name === name)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* @param {Array<{ name?: string }>} signals
|
|
712
|
+
*/
|
|
713
|
+
function formatToolNames (signals) {
|
|
714
|
+
const names = []
|
|
715
|
+
const seen = new Set()
|
|
716
|
+
|
|
717
|
+
for (const signal of signals) {
|
|
718
|
+
if (!signal.name || seen.has(signal.name)) continue
|
|
719
|
+
seen.add(signal.name)
|
|
720
|
+
names.push(signal.name)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (names.length === 0) return ''
|
|
724
|
+
if (names.length === 1) return names[0]
|
|
725
|
+
return `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* @param {string|undefined} frameworkName
|
|
730
|
+
*/
|
|
731
|
+
function getDisplayFrameworkName (frameworkName) {
|
|
732
|
+
return {
|
|
733
|
+
cucumber: 'Cucumber',
|
|
734
|
+
cypress: 'Cypress',
|
|
735
|
+
jest: 'Jest',
|
|
736
|
+
mocha: 'Mocha',
|
|
737
|
+
playwright: 'Playwright',
|
|
738
|
+
vitest: 'Vitest',
|
|
739
|
+
}[frameworkName] || frameworkName || 'test runner'
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function commandOutputShowsTestsRan (lines) {
|
|
743
|
+
return lines.some(line => {
|
|
744
|
+
return /\b\d+\s+(?:passing|passed|failing|failed)\b/i.test(line) ||
|
|
745
|
+
/\btests?\b.*\b(?:passed|failed)\b/i.test(line) ||
|
|
746
|
+
/\bSuccessfully ran target\b.*\btest\b/i.test(line) ||
|
|
747
|
+
/\bsuccess:\s*[1-9]\d*\b/i.test(line) ||
|
|
748
|
+
/\bfailed:\s*[1-9]\d*\b/i.test(line) ||
|
|
749
|
+
/\bTasks:\s*[1-9]\d*\s+successful\b/i.test(line)
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function matchesPreflightExitCode (preflight, exitCode) {
|
|
754
|
+
return preflight?.ran === true &&
|
|
755
|
+
Number.isInteger(preflight.exitCode) &&
|
|
756
|
+
preflight.exitCode === exitCode
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function summarizePreflight (preflight) {
|
|
760
|
+
if (!preflight || preflight.ran !== true) {
|
|
761
|
+
return {
|
|
762
|
+
ran: false,
|
|
763
|
+
reason: preflight?.reason || 'No dd-trace-less preflight result was recorded in the manifest.',
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
ran: true,
|
|
769
|
+
source: preflight.source,
|
|
770
|
+
exitCode: preflight.exitCode,
|
|
771
|
+
observedTestCount: preflight.observedTestCount,
|
|
772
|
+
stdoutSummary: preflight.stdoutSummary,
|
|
773
|
+
stderrSummary: preflight.stderrSummary,
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
module.exports = {
|
|
778
|
+
getCiWiringCommand,
|
|
779
|
+
runCiWiring,
|
|
780
|
+
}
|