dd-trace 6.3.0 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/index.electron.js +3 -0
- 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 +15 -8
- package/packages/datadog-instrumentations/src/ai.js +37 -26
- 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 +24 -10
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/express.js +20 -2
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/grpc/server.js +18 -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/helpers/router-helper.js +8 -8
- package/packages/datadog-instrumentations/src/http2/server.js +143 -17
- package/packages/datadog-instrumentations/src/jest.js +36 -4
- 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 +191 -106
- 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/services/kinesis.js +41 -16
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
- 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 +177 -59
- package/packages/datadog-plugin-graphql/src/parse.js +22 -1
- package/packages/datadog-plugin-graphql/src/request.js +32 -1
- package/packages/datadog-plugin-graphql/src/utils.js +42 -0
- package/packages/datadog-plugin-graphql/src/validate.js +15 -3
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-http2/src/server.js +38 -6
- package/packages/datadog-plugin-jest/src/util.js +21 -6
- 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-vitest/src/index.js +4 -2
- 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/index.electron.js +3 -0
- package/packages/dd-trace/index.js +2 -37
- 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/bootstrap.js +39 -0
- 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 +55 -5
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
- 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 +173 -9
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -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 +83 -0
- package/packages/dd-trace/src/constants.js +7 -0
- package/packages/dd-trace/src/datastreams/pathway.js +6 -4
- 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 +29 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
- 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/index.js +10 -2
- 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 +79 -7
- 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/noop/proxy.js +5 -4
- 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 +35 -0
- 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/opentracing/propagation/log.js +11 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
- package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
- package/packages/dd-trace/src/opentracing/tracer.js +7 -1
- 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 +65 -8
- package/packages/dd-trace/src/plugins/util/web.js +12 -0
- package/packages/dd-trace/src/proxy.js +99 -10
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -36
- 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
package/ci/diagnose.js
ADDED
|
@@ -0,0 +1,2100 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* eslint-disable eslint-rules/eslint-process-env */
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const { execFileSync } = require('node:child_process')
|
|
8
|
+
|
|
9
|
+
const satisfies = require('../vendor/dist/semifies')
|
|
10
|
+
const { DD_MAJOR, VERSION } = require('../version')
|
|
11
|
+
|
|
12
|
+
const MAX_TEXT_FILE_SIZE = 512 * 1024
|
|
13
|
+
const MAX_SCANNED_FILES = 1500
|
|
14
|
+
const MAX_SCANNED_TEXT_BYTES = 32 * 1024 * 1024
|
|
15
|
+
|
|
16
|
+
const PACKAGE_SECTIONS = [
|
|
17
|
+
'dependencies',
|
|
18
|
+
'devDependencies',
|
|
19
|
+
'optionalDependencies',
|
|
20
|
+
'peerDependencies',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
24
|
+
'.cache',
|
|
25
|
+
'.git',
|
|
26
|
+
'.hg',
|
|
27
|
+
'.next',
|
|
28
|
+
'.nuxt',
|
|
29
|
+
'.output',
|
|
30
|
+
'.parcel-cache',
|
|
31
|
+
'.serverless',
|
|
32
|
+
'.svn',
|
|
33
|
+
'.turbo',
|
|
34
|
+
'.yarn',
|
|
35
|
+
'build',
|
|
36
|
+
'coverage',
|
|
37
|
+
'dist',
|
|
38
|
+
'node_modules',
|
|
39
|
+
'out',
|
|
40
|
+
'target',
|
|
41
|
+
'tmp',
|
|
42
|
+
'vendor',
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
const SKIPPED_FILES = new Set([
|
|
46
|
+
'ci/diagnose.js',
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
const TEXT_EXTENSIONS = new Set([
|
|
50
|
+
'.cjs',
|
|
51
|
+
'.cts',
|
|
52
|
+
'.js',
|
|
53
|
+
'.json',
|
|
54
|
+
'.mjs',
|
|
55
|
+
'.mts',
|
|
56
|
+
'.sh',
|
|
57
|
+
'.ts',
|
|
58
|
+
'.tsx',
|
|
59
|
+
'.yaml',
|
|
60
|
+
'.yml',
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
const TEXT_FILE_NAMES = new Set([
|
|
64
|
+
'Dockerfile',
|
|
65
|
+
'Jenkinsfile',
|
|
66
|
+
'Makefile',
|
|
67
|
+
'docker-compose.yml',
|
|
68
|
+
'docker-compose.yaml',
|
|
69
|
+
'package.json',
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
const NODE_OPTIONS_RE = /\bNODE_OPTIONS\b/
|
|
73
|
+
const INIT_PRELOAD_TARGET =
|
|
74
|
+
String.raw`(?:dd-trace\/ci\/init|(?:[^\s'"]*[\/\\])?node_modules[\/\\]dd-trace[\/\\]ci[\/\\]init|\.\/ci\/init)`
|
|
75
|
+
const INIT_PRELOAD_RE =
|
|
76
|
+
new RegExp(String.raw`(?:^|[\s='"])(?:-r|--require)(?:=|\s+)['"]?${INIT_PRELOAD_TARGET}(?:\.js)?['"]?(?=$|\s|["'])`)
|
|
77
|
+
const REGISTER_PRELOAD_RE =
|
|
78
|
+
/(?:^|[\s='"])(?:--import|-r|--require)(?:=|\s+)['"]?dd-trace\/register(?:\.js)?['"]?(?=$|\s|["'])/
|
|
79
|
+
const WRONG_INIT_RE = /dd-trace\/(?:init|initialize\.mjs)\b|require\(['"]dd-trace['"]\)\.init\s*\(/
|
|
80
|
+
const DIRECT_CI_INIT_RE = /(?:require\(|import\s+)['"]dd-trace\/ci\/init(?:\.js)?['"]/
|
|
81
|
+
const CI_DISABLED_RE = /DD_CIVISIBILITY_ENABLED["'\s:=]+(?:false|0)\b/i
|
|
82
|
+
const ITR_DISABLED_RE = /DD_CIVISIBILITY_ITR_ENABLED["'\s:=]+(?:false|0)\b/i
|
|
83
|
+
const GIT_UPLOAD_DISABLED_RE = /DD_CIVISIBILITY_GIT_UPLOAD_ENABLED["'\s:=]+(?:false|0)\b/i
|
|
84
|
+
const AGENTLESS_ENABLED_RE = /DD_CIVISIBILITY_AGENTLESS_ENABLED["'\s:=]+(?:true|1)\b/i
|
|
85
|
+
const API_KEY_RE = /\b(?:DD_API_KEY|DATADOG_API_KEY)\b/
|
|
86
|
+
const SERVICE_RE = /\bDD_SERVICE\b/
|
|
87
|
+
const OTEL_OTLP_RE = /OTEL_TRACES_EXPORTER["'\s:=]+otlp\b/i
|
|
88
|
+
const WATCH_MODE_RE = /(?:^|\s)(?:watch|--watch|--watchAll)(?!(?:=false)(?:\s|$))(?:\s|=|$)/
|
|
89
|
+
|
|
90
|
+
const CYPRESS_MANUAL_PLUGIN_RE = /dd-trace\/ci\/cypress\/(?:plugin|after-run|after-spec)\b/
|
|
91
|
+
const CYPRESS_SUPPORT_RE = /dd-trace\/ci\/cypress\/support\b/
|
|
92
|
+
const CYPRESS_SUPPORT_DISABLED_RE = /supportFile\s*:\s*false|"supportFile"\s*:\s*false/
|
|
93
|
+
const CUCUMBER_PARALLEL_RE =
|
|
94
|
+
/\bcucumber(?:-js)?\b[\s\S]{0,200}\s--parallel\b|--parallel\b[\s\S]{0,200}\bcucumber(?:-js)?\b/
|
|
95
|
+
const JEST_FORCE_EXIT_RE = /\bforceExit\s*:\s*true\b|--forceExit\b|"forceExit"\s*:\s*true/
|
|
96
|
+
const JEST_JASMINE_RE = /jest-jasmine2/
|
|
97
|
+
|
|
98
|
+
const CURRENT_ENV_PROVIDER_KEYS = [
|
|
99
|
+
'GITHUB_ACTIONS',
|
|
100
|
+
'GITLAB_CI',
|
|
101
|
+
'CIRCLECI',
|
|
102
|
+
'JENKINS_URL',
|
|
103
|
+
'BUILDKITE',
|
|
104
|
+
'TRAVIS',
|
|
105
|
+
'TF_BUILD',
|
|
106
|
+
'BITBUCKET_BUILD_NUMBER',
|
|
107
|
+
'DRONE',
|
|
108
|
+
'TEAMCITY_VERSION',
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Builds the framework support table for the tracer major version that is running this script.
|
|
113
|
+
*
|
|
114
|
+
* @param {number} ddMajor dd-trace major version
|
|
115
|
+
* @returns {Array<object>} supported framework definitions
|
|
116
|
+
*/
|
|
117
|
+
function getFrameworkDefinitions (ddMajor) {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
id: 'jest',
|
|
121
|
+
name: 'Jest',
|
|
122
|
+
packages: ['jest', '@jest/core'],
|
|
123
|
+
commandPatterns: [/\bjest\b/],
|
|
124
|
+
configPatterns: [/^jest\.config\./, /^config-jest\./],
|
|
125
|
+
supportedRange: ddMajor >= 6 ? '>=28.0.0' : '>=24.8.0',
|
|
126
|
+
recommendation: ddMajor >= 6
|
|
127
|
+
? 'Upgrade Jest to >=28.0.0, or use dd-trace v5 for older Jest versions.'
|
|
128
|
+
: 'Upgrade Jest to >=24.8.0.',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'mocha',
|
|
132
|
+
name: 'Mocha',
|
|
133
|
+
packages: ['mocha'],
|
|
134
|
+
commandPatterns: [/\bmocha\b/],
|
|
135
|
+
configPatterns: [/^\.mocharc\./],
|
|
136
|
+
supportedRange: ddMajor >= 6 ? '>=8.0.0' : '>=5.2.0',
|
|
137
|
+
recommendation: ddMajor >= 6
|
|
138
|
+
? 'Upgrade Mocha to >=8.0.0, or use dd-trace v5 for older Mocha versions.'
|
|
139
|
+
: 'Upgrade Mocha to >=5.2.0.',
|
|
140
|
+
notes: [
|
|
141
|
+
'Impacted tests are detected at suite level for Mocha.',
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'cucumber',
|
|
146
|
+
name: 'Cucumber',
|
|
147
|
+
packages: ['@cucumber/cucumber'],
|
|
148
|
+
commandPatterns: [/\bcucumber-js\b/, /\bcucumber\b/],
|
|
149
|
+
configPatterns: [/^cucumber\./],
|
|
150
|
+
supportedRange: '>=7.0.0',
|
|
151
|
+
recommendation: 'Upgrade @cucumber/cucumber to >=7.0.0.',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'cypress',
|
|
155
|
+
name: 'Cypress',
|
|
156
|
+
packages: ['cypress'],
|
|
157
|
+
commandPatterns: [/\bcypress\s+(?:run|open)\b/],
|
|
158
|
+
configPatterns: [/^cypress\.config\./, /^cypress\.json$/],
|
|
159
|
+
supportedRange: ddMajor >= 6 ? '>=12.0.0' : '>=6.7.0',
|
|
160
|
+
autoInstrumentationRange: ddMajor >= 6 ? '>=12.0.0' : '>=10.2.0',
|
|
161
|
+
recommendation: ddMajor >= 6
|
|
162
|
+
? 'Upgrade Cypress to >=12.0.0, or use dd-trace v5 for older Cypress versions.'
|
|
163
|
+
: 'Upgrade Cypress to >=6.7.0.',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'playwright',
|
|
167
|
+
name: 'Playwright',
|
|
168
|
+
packages: ['@playwright/test'],
|
|
169
|
+
commandPatterns: [/\bplaywright\s+test\b/],
|
|
170
|
+
configPatterns: [/^playwright\.config\./],
|
|
171
|
+
supportedRange: ddMajor >= 6 ? '>=1.38.0' : '>=1.18.0',
|
|
172
|
+
recommendation: ddMajor >= 6
|
|
173
|
+
? 'Upgrade Playwright to >=1.38.0, or use dd-trace v5 for older Playwright versions.'
|
|
174
|
+
: 'Upgrade Playwright to >=1.18.0.',
|
|
175
|
+
notes: [
|
|
176
|
+
'Test Impact Analysis suite skipping is not supported for Playwright.',
|
|
177
|
+
'Impacted tests are detected at suite level for Playwright.',
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'vitest',
|
|
182
|
+
name: 'Vitest',
|
|
183
|
+
packages: ['vitest'],
|
|
184
|
+
commandPatterns: [/\bvitest\b/],
|
|
185
|
+
configPatterns: [/^vitest\.config\./, /^vite\.config\./],
|
|
186
|
+
supportedRange: '>=1.6.0',
|
|
187
|
+
recommendation: 'Upgrade Vitest to >=1.6.0.',
|
|
188
|
+
notes: [
|
|
189
|
+
'Test Impact Analysis suite skipping is not supported for Vitest.',
|
|
190
|
+
'Impacted tests are detected at suite level for Vitest.',
|
|
191
|
+
],
|
|
192
|
+
esmInitialization: true,
|
|
193
|
+
},
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const UNSUPPORTED_FRAMEWORKS = [
|
|
198
|
+
{
|
|
199
|
+
id: 'node-test',
|
|
200
|
+
name: 'Node.js test runner',
|
|
201
|
+
packages: [],
|
|
202
|
+
commandPatterns: [/\bnode\s+--test\b/, /\bnode\s+--experimental-test-coverage\b/],
|
|
203
|
+
},
|
|
204
|
+
{ id: 'ava', name: 'AVA', packages: ['ava'], commandPatterns: [/\bava\b/] },
|
|
205
|
+
{ id: 'tap', name: 'tap', packages: ['tap'], commandPatterns: [/\btap\b/] },
|
|
206
|
+
{ id: 'jasmine', name: 'Jasmine', packages: ['jasmine'], commandPatterns: [/\bjasmine\b/] },
|
|
207
|
+
{ id: 'karma', name: 'Karma', packages: ['karma'], commandPatterns: [/\bkarma\b/] },
|
|
208
|
+
{ id: 'uvu', name: 'uvu', packages: ['uvu'], commandPatterns: [/\buvu\b/] },
|
|
209
|
+
{
|
|
210
|
+
id: 'testcafe',
|
|
211
|
+
name: 'TestCafe',
|
|
212
|
+
packages: ['testcafe'],
|
|
213
|
+
commandPatterns: [/\btestcafe\b/],
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Runs all static checks for a repository.
|
|
219
|
+
*
|
|
220
|
+
* @param {object} [options] diagnosis options
|
|
221
|
+
* @param {string} [options.root] repository path to inspect
|
|
222
|
+
* @param {NodeJS.ProcessEnv} [options.env] environment to inspect
|
|
223
|
+
* @param {Function} [options.execFile] command runner used for git checks
|
|
224
|
+
* @param {string} [options.gitExecutable] trusted git executable used for git checks
|
|
225
|
+
* @param {number} [options.maxFiles] maximum number of text files to scan
|
|
226
|
+
* @param {number} [options.maxTotalBytes] maximum aggregate bytes of text files to scan
|
|
227
|
+
* @param {string[]} [options.excludePaths] repository paths to exclude from the text scan
|
|
228
|
+
* @returns {object} diagnosis report
|
|
229
|
+
*/
|
|
230
|
+
function runDiagnosis (options = {}) {
|
|
231
|
+
const root = path.resolve(options.root || process.cwd())
|
|
232
|
+
const physicalRoot = getPhysicalRoot(root)
|
|
233
|
+
const env = options.env || process.env
|
|
234
|
+
const execFile = options.execFile || execFileSync
|
|
235
|
+
const maxFiles = options.maxFiles || MAX_SCANNED_FILES
|
|
236
|
+
const maxTotalBytes = options.maxTotalBytes || MAX_SCANNED_TEXT_BYTES
|
|
237
|
+
const results = []
|
|
238
|
+
const files = collectTextFiles(root, physicalRoot, maxFiles, options.excludePaths)
|
|
239
|
+
const textFiles = readTextFiles(root, physicalRoot, files, maxTotalBytes)
|
|
240
|
+
const truncatedFileScan = files.truncated || textFiles.truncated
|
|
241
|
+
const manifests = readPackageManifests(root, textFiles)
|
|
242
|
+
const rootManifest = manifests.find(manifest => manifest.relativePath === 'package.json')
|
|
243
|
+
const rootPackageJsonState = getRootPackageJsonState(root, physicalRoot)
|
|
244
|
+
const scripts = collectScripts(manifests)
|
|
245
|
+
const workflowFiles = textFiles.filter(file => isWorkflowFile(file.relativePath))
|
|
246
|
+
const definitions = getFrameworkDefinitions(DD_MAJOR)
|
|
247
|
+
const supportedFrameworks = detectSupportedFrameworks(root, definitions, manifests, scripts, textFiles)
|
|
248
|
+
const eligibleFrameworks = getEligibleFrameworks(supportedFrameworks)
|
|
249
|
+
const unsupportedFrameworks = detectUnsupportedFrameworks(UNSUPPORTED_FRAMEWORKS, manifests, scripts)
|
|
250
|
+
const evidence = collectEvidence(textFiles, env)
|
|
251
|
+
|
|
252
|
+
checkPackageManifest(results, rootManifest, rootPackageJsonState)
|
|
253
|
+
checkDdTraceDependency(results, manifests, { root, truncatedFileScan, rootPackageJsonState })
|
|
254
|
+
checkSupportedFrameworks(results, supportedFrameworks)
|
|
255
|
+
checkUnsupportedFrameworks(results, unsupportedFrameworks, supportedFrameworks)
|
|
256
|
+
checkInitialization(results, supportedFrameworks, evidence, env)
|
|
257
|
+
checkFrameworkConfiguration(results, supportedFrameworks, evidence, textFiles, manifests)
|
|
258
|
+
checkCiConfiguration(results, workflowFiles, evidence, env)
|
|
259
|
+
const gitExecutable = options.gitExecutable || (options.execFile ? 'git' : findTrustedGitExecutable())
|
|
260
|
+
checkGit(results, root, env, execFile, gitExecutable)
|
|
261
|
+
checkCurrentEnvironment(results, env, evidence)
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
root,
|
|
265
|
+
ddTraceVersion: VERSION,
|
|
266
|
+
ddTraceMajor: DD_MAJOR,
|
|
267
|
+
scannedFileCount: textFiles.length,
|
|
268
|
+
truncatedFileScan,
|
|
269
|
+
supportedFrameworks: supportedFrameworks.map(serializeSupportedFramework),
|
|
270
|
+
eligibleFrameworks: eligibleFrameworks.map(serializeEligibleFramework),
|
|
271
|
+
unsupportedFrameworks: unsupportedFrameworks.map(serializeUnsupportedFramework),
|
|
272
|
+
results,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Adds a normalized result to the list.
|
|
278
|
+
*
|
|
279
|
+
* @param {Array<object>} results mutable result list
|
|
280
|
+
* @param {string} status result status
|
|
281
|
+
* @param {string} title short title
|
|
282
|
+
* @param {string} message result details
|
|
283
|
+
* @param {object} [extra] optional fields
|
|
284
|
+
*/
|
|
285
|
+
function addResult (results, status, title, message, extra = {}) {
|
|
286
|
+
results.push({
|
|
287
|
+
status,
|
|
288
|
+
title,
|
|
289
|
+
message,
|
|
290
|
+
...extra,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Reads package.json files collected by the repository scan.
|
|
296
|
+
*
|
|
297
|
+
* @param {string} root repository root
|
|
298
|
+
* @param {Array<object>} textFiles scanned text files
|
|
299
|
+
* @returns {Array<object>} parsed package manifests
|
|
300
|
+
*/
|
|
301
|
+
function readPackageManifests (root, textFiles) {
|
|
302
|
+
const manifests = []
|
|
303
|
+
|
|
304
|
+
for (const file of textFiles) {
|
|
305
|
+
if (path.basename(file.relativePath) !== 'package.json') continue
|
|
306
|
+
|
|
307
|
+
const json = parseJson(file.content)
|
|
308
|
+
if (!json) continue
|
|
309
|
+
|
|
310
|
+
manifests.push({
|
|
311
|
+
path: path.join(root, file.relativePath),
|
|
312
|
+
relativePath: file.relativePath,
|
|
313
|
+
json,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return manifests
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Checks whether the repository root has a package.json independently from the text-file scan.
|
|
322
|
+
*
|
|
323
|
+
* @param {string} root repository root
|
|
324
|
+
* @param {string|undefined} physicalRoot physical repository root
|
|
325
|
+
* @returns {{ exists: boolean }} root package.json state
|
|
326
|
+
*/
|
|
327
|
+
function getRootPackageJsonState (root, physicalRoot) {
|
|
328
|
+
return {
|
|
329
|
+
exists: Boolean(getSafeScannedFile(root, physicalRoot, 'package.json')),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Checks that a root package.json exists.
|
|
335
|
+
*
|
|
336
|
+
* @param {Array<object>} results mutable result list
|
|
337
|
+
* @param {object|undefined} rootManifest parsed root package manifest
|
|
338
|
+
* @param {object} rootPackageJsonState root package.json filesystem state
|
|
339
|
+
*/
|
|
340
|
+
function checkPackageManifest (results, rootManifest, rootPackageJsonState) {
|
|
341
|
+
if (rootManifest) {
|
|
342
|
+
addResult(results, 'ok', 'Root package.json found', 'Dependency and script checks can inspect package metadata.')
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (rootPackageJsonState.exists) {
|
|
347
|
+
addResult(
|
|
348
|
+
results,
|
|
349
|
+
'warning',
|
|
350
|
+
'Root package.json not determined',
|
|
351
|
+
'A root package.json file exists, but static diagnosis could not parse it as scanned text.',
|
|
352
|
+
{
|
|
353
|
+
recommendation:
|
|
354
|
+
'Check whether package.json is readable UTF-8 JSON and smaller than the static diagnosis file limit.',
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
addResult(
|
|
361
|
+
results,
|
|
362
|
+
'warning',
|
|
363
|
+
'No root package.json found',
|
|
364
|
+
'The diagnosis could not inspect root dependencies or package scripts.',
|
|
365
|
+
{ recommendation: 'Run this script from the JavaScript repository root, or pass --path <repository>.' }
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Checks whether dd-trace is declared in repository manifests.
|
|
371
|
+
*
|
|
372
|
+
* @param {Array<object>} results mutable result list
|
|
373
|
+
* @param {Array<object>} manifests package manifests
|
|
374
|
+
* @param {object} options dependency check options
|
|
375
|
+
* @param {string} [options.root] repository root
|
|
376
|
+
* @param {boolean} [options.truncatedFileScan] whether static file discovery was truncated
|
|
377
|
+
* @param {object} [options.rootPackageJsonState] root package manifest state
|
|
378
|
+
*/
|
|
379
|
+
function checkDdTraceDependency (results, manifests, options = {}) {
|
|
380
|
+
const entries = findDependencyEntries(manifests, ['dd-trace'])
|
|
381
|
+
|
|
382
|
+
if (entries.length) {
|
|
383
|
+
addResult(
|
|
384
|
+
results,
|
|
385
|
+
'ok',
|
|
386
|
+
'dd-trace dependency found',
|
|
387
|
+
`Detected dd-trace in ${formatLocations(entries.map(entry => entry.relativePath))}.`
|
|
388
|
+
)
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const installedPackageJson = options.root && path.join(options.root, 'node_modules', 'dd-trace', 'package.json')
|
|
393
|
+
let installed = false
|
|
394
|
+
try {
|
|
395
|
+
installed = installedPackageJson && fs.statSync(installedPackageJson).isFile()
|
|
396
|
+
} catch {}
|
|
397
|
+
if (installed) {
|
|
398
|
+
addResult(
|
|
399
|
+
results,
|
|
400
|
+
'ok',
|
|
401
|
+
'dd-trace package installed',
|
|
402
|
+
'Detected an installed dd-trace package even though it was not declared in the scanned package manifests.'
|
|
403
|
+
)
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (options.truncatedFileScan || (options.rootPackageJsonState?.exists && !hasParsedRootManifest(manifests))) {
|
|
408
|
+
addResult(
|
|
409
|
+
results,
|
|
410
|
+
'warning',
|
|
411
|
+
'dd-trace dependency not determined',
|
|
412
|
+
'Static diagnosis did not confirm a dd-trace dependency, but the file scan was incomplete or root package ' +
|
|
413
|
+
'metadata could not be parsed.',
|
|
414
|
+
{ recommendation: 'Inspect the package that runs tests and install dd-trace there if it is absent.' }
|
|
415
|
+
)
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
addResult(
|
|
420
|
+
results,
|
|
421
|
+
'warning',
|
|
422
|
+
'dd-trace dependency not found in package.json',
|
|
423
|
+
'The script did not find dd-trace in dependencies or devDependencies.',
|
|
424
|
+
{ recommendation: 'Install dd-trace in the project that runs the tests.' }
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Checks whether static diagnosis parsed the repository root package manifest.
|
|
430
|
+
*
|
|
431
|
+
* @param {Array<object>} manifests parsed package manifests
|
|
432
|
+
* @returns {boolean} true when the root package.json was parsed
|
|
433
|
+
*/
|
|
434
|
+
function hasParsedRootManifest (manifests) {
|
|
435
|
+
return manifests.some(manifest => manifest.relativePath === 'package.json')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Checks supported framework detections and versions.
|
|
440
|
+
*
|
|
441
|
+
* @param {Array<object>} results mutable result list
|
|
442
|
+
* @param {Array<object>} frameworks detected supported frameworks
|
|
443
|
+
*/
|
|
444
|
+
function checkSupportedFrameworks (results, frameworks) {
|
|
445
|
+
if (!frameworks.length) {
|
|
446
|
+
addResult(
|
|
447
|
+
results,
|
|
448
|
+
'warning',
|
|
449
|
+
'No supported test framework detected',
|
|
450
|
+
'No supported Test Optimization framework was found in dependencies, scripts, or config files.',
|
|
451
|
+
{
|
|
452
|
+
recommendation:
|
|
453
|
+
'Use Jest, Mocha, Cucumber, Cypress, Playwright, or Vitest with a supported version.',
|
|
454
|
+
}
|
|
455
|
+
)
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
for (const framework of frameworks) {
|
|
460
|
+
if (!framework.versionDetections.length) {
|
|
461
|
+
addResult(
|
|
462
|
+
results,
|
|
463
|
+
'warning',
|
|
464
|
+
`${framework.name} detected but version is unknown`,
|
|
465
|
+
`${framework.name} appears in scripts or config, but no package version could be determined.`,
|
|
466
|
+
{
|
|
467
|
+
locations: framework.locations,
|
|
468
|
+
recommendation:
|
|
469
|
+
`Ensure ${framework.packages.join(' or ')} is installed and matches ${framework.supportedRange}.`,
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const detection of framework.versionDetections) {
|
|
475
|
+
if (!detection.version) {
|
|
476
|
+
addResult(
|
|
477
|
+
results,
|
|
478
|
+
'warning',
|
|
479
|
+
`${framework.name} version could not be determined`,
|
|
480
|
+
`Detected ${detection.packageName}@${detection.rawVersion}, but the version is not statically comparable.`,
|
|
481
|
+
{
|
|
482
|
+
locations: [detection.relativePath],
|
|
483
|
+
recommendation: `Verify ${framework.name} satisfies ${framework.supportedRange}.`,
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const status = satisfies(detection.version, framework.supportedRange) ? 'ok' : 'error'
|
|
490
|
+
const source = detection.source === 'installed' ? 'installed package' : 'package manifest'
|
|
491
|
+
addResult(
|
|
492
|
+
results,
|
|
493
|
+
status,
|
|
494
|
+
`${framework.name} ${detection.version} ${status === 'ok' ? 'is supported' : 'is not supported'}`,
|
|
495
|
+
`Detected ${detection.packageName}@${detection.rawVersion} from ${source}; supported range is ` +
|
|
496
|
+
`${framework.supportedRange}.`,
|
|
497
|
+
{
|
|
498
|
+
locations: detection.relativePath ? [detection.relativePath] : undefined,
|
|
499
|
+
recommendation: status === 'error' ? framework.recommendation : undefined,
|
|
500
|
+
}
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
for (const note of framework.notes || []) {
|
|
505
|
+
addResult(results, 'info', `${framework.name} capability note`, note)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Checks unsupported framework detections.
|
|
512
|
+
*
|
|
513
|
+
* @param {Array<object>} results mutable result list
|
|
514
|
+
* @param {Array<object>} unsupported detected unsupported frameworks
|
|
515
|
+
* @param {Array<object>} supported detected supported frameworks
|
|
516
|
+
*/
|
|
517
|
+
function checkUnsupportedFrameworks (results, unsupported, supported) {
|
|
518
|
+
for (const framework of unsupported) {
|
|
519
|
+
const status = supported.length ? 'warning' : 'error'
|
|
520
|
+
addResult(
|
|
521
|
+
results,
|
|
522
|
+
status,
|
|
523
|
+
`${framework.name} is not supported by Test Optimization`,
|
|
524
|
+
`${framework.name} was detected in dependencies or test scripts.`,
|
|
525
|
+
{
|
|
526
|
+
locations: framework.locations,
|
|
527
|
+
recommendation:
|
|
528
|
+
'Use a supported JavaScript test framework for automatic Test Optimization instrumentation.',
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Checks Test Optimization initialization.
|
|
536
|
+
*
|
|
537
|
+
* @param {Array<object>} results mutable result list
|
|
538
|
+
* @param {Array<object>} frameworks detected supported frameworks
|
|
539
|
+
* @param {object} evidence repository evidence
|
|
540
|
+
* @param {NodeJS.ProcessEnv} env environment
|
|
541
|
+
*/
|
|
542
|
+
function checkInitialization (results, frameworks, evidence, env) {
|
|
543
|
+
if (!frameworks.length) return
|
|
544
|
+
|
|
545
|
+
const hasCiInit = evidence.hasCiInit || hasCiInitInNodeOptions(env.NODE_OPTIONS)
|
|
546
|
+
const hasCypressOnly = frameworks.length === 1 && frameworks[0].id === 'cypress'
|
|
547
|
+
const hasCypressManualPlugin = evidence.cypressManualPluginLocations.length > 0
|
|
548
|
+
|
|
549
|
+
if (hasCiInit) {
|
|
550
|
+
addResult(
|
|
551
|
+
results,
|
|
552
|
+
'ok',
|
|
553
|
+
'Test Optimization initialization found',
|
|
554
|
+
'Found dd-trace/ci/init preloaded through NODE_OPTIONS in repository files or the current environment.',
|
|
555
|
+
{ locations: evidence.ciInitLocations }
|
|
556
|
+
)
|
|
557
|
+
} else if (hasCypressOnly && hasCypressManualPlugin) {
|
|
558
|
+
addResult(
|
|
559
|
+
results,
|
|
560
|
+
'ok',
|
|
561
|
+
'Cypress manual plugin initialization found',
|
|
562
|
+
'Found the Cypress-specific dd-trace Test Optimization plugin setup.',
|
|
563
|
+
{ locations: evidence.cypressManualPluginLocations }
|
|
564
|
+
)
|
|
565
|
+
} else {
|
|
566
|
+
addResult(
|
|
567
|
+
results,
|
|
568
|
+
'error',
|
|
569
|
+
'Missing Test Optimization initialization',
|
|
570
|
+
'No NODE_OPTIONS preload for dd-trace/ci/init was found in repository files or the current environment.',
|
|
571
|
+
{
|
|
572
|
+
recommendation:
|
|
573
|
+
'Run tests with NODE_OPTIONS="-r dd-trace/ci/init". For ESM test runners, also include ' +
|
|
574
|
+
'--import dd-trace/register.js.',
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (evidence.directCiInitLocations.length) {
|
|
580
|
+
addResult(
|
|
581
|
+
results,
|
|
582
|
+
'error',
|
|
583
|
+
'Test Optimization initialization is imported directly',
|
|
584
|
+
'The diagnosis found require("dd-trace/ci/init") or import "dd-trace/ci/init". ' +
|
|
585
|
+
'That does not preload the tracer early enough for Test Optimization setup.',
|
|
586
|
+
{
|
|
587
|
+
locations: evidence.directCiInitLocations,
|
|
588
|
+
recommendation: 'Set NODE_OPTIONS="-r dd-trace/ci/init" on the test process instead.',
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (evidence.wrongInitLocations.length) {
|
|
594
|
+
addResult(
|
|
595
|
+
results,
|
|
596
|
+
'error',
|
|
597
|
+
'Plain dd-trace initialization found in test setup',
|
|
598
|
+
'The diagnosis found dd-trace/init, dd-trace/initialize.mjs, or require("dd-trace").init(). ' +
|
|
599
|
+
'That does not initialize the tracer in Test Optimization mode.',
|
|
600
|
+
{
|
|
601
|
+
locations: evidence.wrongInitLocations,
|
|
602
|
+
recommendation: 'Use dd-trace/ci/init for test commands instead of the plain tracing initializer.',
|
|
603
|
+
}
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (frameworks.some(framework => framework.esmInitialization) && hasCiInit && !evidence.hasRegister &&
|
|
608
|
+
!hasRegisterInNodeOptions(env.NODE_OPTIONS)) {
|
|
609
|
+
addResult(
|
|
610
|
+
results,
|
|
611
|
+
'warning',
|
|
612
|
+
'ESM loader registration not found',
|
|
613
|
+
'Vitest and other ESM-heavy test runners often need dd-trace/register.js before dd-trace/ci/init.',
|
|
614
|
+
{
|
|
615
|
+
recommendation:
|
|
616
|
+
'Use NODE_OPTIONS="--import dd-trace/register.js -r dd-trace/ci/init" for ESM test runs.',
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Checks framework-specific configuration pitfalls.
|
|
624
|
+
*
|
|
625
|
+
* @param {Array<object>} results mutable result list
|
|
626
|
+
* @param {Array<object>} frameworks detected supported frameworks
|
|
627
|
+
* @param {object} evidence repository evidence
|
|
628
|
+
* @param {Array<object>} textFiles scanned text files
|
|
629
|
+
* @param {Array<object>} manifests package manifests
|
|
630
|
+
*/
|
|
631
|
+
function checkFrameworkConfiguration (results, frameworks, evidence, textFiles, manifests) {
|
|
632
|
+
if (hasFramework(frameworks, 'vitest') && !hasFramework(frameworks, 'playwright') &&
|
|
633
|
+
findDependencyEntries(manifests, ['playwright']).length > 0) {
|
|
634
|
+
addResult(
|
|
635
|
+
results,
|
|
636
|
+
'info',
|
|
637
|
+
'Playwright package is not a Playwright Test runner',
|
|
638
|
+
'The repository uses Vitest and has the playwright package, but no @playwright/test runner was detected. ' +
|
|
639
|
+
'Treat Playwright as Vitest browser-provider infrastructure, not as another test framework.',
|
|
640
|
+
{}
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (hasFramework(frameworks, 'cypress')) {
|
|
645
|
+
checkCypressConfiguration(results, evidence)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (hasFramework(frameworks, 'jest')) {
|
|
649
|
+
const jestLocations = findLocations(textFiles, JEST_FORCE_EXIT_RE)
|
|
650
|
+
if (jestLocations.length) {
|
|
651
|
+
addResult(
|
|
652
|
+
results,
|
|
653
|
+
'warning',
|
|
654
|
+
'Jest forceExit can drop Test Optimization data',
|
|
655
|
+
'Jest\'s forceExit option can terminate before dd-trace flushes all test data.',
|
|
656
|
+
{
|
|
657
|
+
locations: jestLocations,
|
|
658
|
+
recommendation: 'Remove --forceExit or forceExit: true from Jest configuration when possible.',
|
|
659
|
+
}
|
|
660
|
+
)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const jasmineLocations = findLocations(textFiles, JEST_JASMINE_RE)
|
|
664
|
+
if (jasmineLocations.length) {
|
|
665
|
+
addResult(
|
|
666
|
+
results,
|
|
667
|
+
'info',
|
|
668
|
+
'Jest is configured with jest-jasmine2',
|
|
669
|
+
'dd-trace can avoid crashing with jest-jasmine2, but jest-circus is the better-supported runner.',
|
|
670
|
+
{
|
|
671
|
+
locations: jasmineLocations,
|
|
672
|
+
recommendation: 'Prefer the default jest-circus runner on supported Jest versions.',
|
|
673
|
+
}
|
|
674
|
+
)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const tsConfigLocations = findJestTypescriptConfigLocations(textFiles)
|
|
678
|
+
const hasTsNode = findDependencyEntries(manifests, ['ts-node']).length > 0
|
|
679
|
+
if (tsConfigLocations.length && !hasTsNode) {
|
|
680
|
+
addResult(
|
|
681
|
+
results,
|
|
682
|
+
'warning',
|
|
683
|
+
'Jest TypeScript config may need ts-node',
|
|
684
|
+
'Jest loads TypeScript configuration files before test transforms. Without ts-node or an equivalent ' +
|
|
685
|
+
'precompiled config, the selected command can fail before collecting tests.',
|
|
686
|
+
{
|
|
687
|
+
locations: tsConfigLocations,
|
|
688
|
+
recommendation:
|
|
689
|
+
'Install ts-node for the diagnostic run, or use a temporary JSON/CommonJS Jest config generated ' +
|
|
690
|
+
'from the repository config.',
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (hasFramework(frameworks, 'cucumber')) {
|
|
697
|
+
const cucumber = frameworks.find(framework => framework.id === 'cucumber')
|
|
698
|
+
const parallelLocations = findLocations(textFiles, CUCUMBER_PARALLEL_RE)
|
|
699
|
+
const hasOldParallel = cucumber.versionDetections.some(detection =>
|
|
700
|
+
detection.version && !satisfies(detection.version, '>=11.0.0')
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
if (parallelLocations.length && hasOldParallel) {
|
|
704
|
+
addResult(
|
|
705
|
+
results,
|
|
706
|
+
'warning',
|
|
707
|
+
'Cucumber parallel mode has feature limits before version 11',
|
|
708
|
+
'Some Test Optimization features for Cucumber parallel mode require @cucumber/cucumber >=11.0.0.',
|
|
709
|
+
{
|
|
710
|
+
locations: parallelLocations,
|
|
711
|
+
recommendation: 'Upgrade @cucumber/cucumber to >=11.0.0 when using --parallel.',
|
|
712
|
+
}
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Checks Cypress-specific setup.
|
|
720
|
+
*
|
|
721
|
+
* @param {Array<object>} results mutable result list
|
|
722
|
+
* @param {object} evidence repository evidence
|
|
723
|
+
*/
|
|
724
|
+
function checkCypressConfiguration (results, evidence) {
|
|
725
|
+
if (evidence.cypressSupportDisabledLocations.length) {
|
|
726
|
+
addResult(
|
|
727
|
+
results,
|
|
728
|
+
'warning',
|
|
729
|
+
'Cypress support file is disabled',
|
|
730
|
+
'Cypress browser-side hooks cannot be injected when supportFile is false.',
|
|
731
|
+
{
|
|
732
|
+
locations: evidence.cypressSupportDisabledLocations,
|
|
733
|
+
recommendation: 'Use a Cypress support file, or manually require dd-trace/ci/cypress/support.',
|
|
734
|
+
}
|
|
735
|
+
)
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (evidence.cypressSupportLocations.length) {
|
|
740
|
+
addResult(
|
|
741
|
+
results,
|
|
742
|
+
'ok',
|
|
743
|
+
'Cypress support hook found',
|
|
744
|
+
'Found dd-trace/ci/cypress/support in the repository.',
|
|
745
|
+
{ locations: evidence.cypressSupportLocations }
|
|
746
|
+
)
|
|
747
|
+
} else {
|
|
748
|
+
addResult(
|
|
749
|
+
results,
|
|
750
|
+
'info',
|
|
751
|
+
'Cypress support hook not explicitly configured',
|
|
752
|
+
'For supported Cypress versions, dd-trace can inject a temporary support wrapper when dd-trace/ci/init is used.',
|
|
753
|
+
{
|
|
754
|
+
recommendation:
|
|
755
|
+
'If browser-side test events are missing, add require("dd-trace/ci/cypress/support") to the ' +
|
|
756
|
+
'Cypress support file.',
|
|
757
|
+
}
|
|
758
|
+
)
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Checks static CI workflow files.
|
|
764
|
+
*
|
|
765
|
+
* @param {Array<object>} results mutable result list
|
|
766
|
+
* @param {Array<object>} workflowFiles scanned CI workflow files
|
|
767
|
+
* @param {object} evidence repository evidence
|
|
768
|
+
* @param {NodeJS.ProcessEnv} env environment
|
|
769
|
+
*/
|
|
770
|
+
function checkCiConfiguration (results, workflowFiles, evidence, env) {
|
|
771
|
+
if (!workflowFiles.length) {
|
|
772
|
+
addResult(
|
|
773
|
+
results,
|
|
774
|
+
'info',
|
|
775
|
+
'No CI workflow files found',
|
|
776
|
+
'The diagnosis did not find common CI configuration files to inspect.'
|
|
777
|
+
)
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
addResult(
|
|
782
|
+
results,
|
|
783
|
+
'ok',
|
|
784
|
+
'CI workflow files found',
|
|
785
|
+
`Inspected ${workflowFiles.length} CI workflow file(s).`,
|
|
786
|
+
{ locations: workflowFiles.map(file => file.relativePath) }
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
if (!evidence.hasCiInit && !hasCiInitInNodeOptions(env.NODE_OPTIONS)) {
|
|
790
|
+
addResult(
|
|
791
|
+
results,
|
|
792
|
+
'warning',
|
|
793
|
+
'CI workflows do not show Test Optimization initialization',
|
|
794
|
+
'No CI workflow file shows NODE_OPTIONS preloading dd-trace/ci/init.',
|
|
795
|
+
{
|
|
796
|
+
recommendation:
|
|
797
|
+
'Set NODE_OPTIONS="-r dd-trace/ci/init" in the CI job that runs the supported JavaScript test framework.',
|
|
798
|
+
}
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const shallowGithubLocations = []
|
|
803
|
+
const containerGithubLocations = []
|
|
804
|
+
|
|
805
|
+
for (const file of workflowFiles) {
|
|
806
|
+
if (!file.relativePath.startsWith('.github/workflows/')) continue
|
|
807
|
+
|
|
808
|
+
if (/actions\/checkout/.test(file.content) && !/fetch-depth\s*:\s*0\b/.test(file.content)) {
|
|
809
|
+
shallowGithubLocations.push(file.relativePath)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (/\bcontainer\s*:/.test(file.content) && !/safe\.directory/.test(file.content)) {
|
|
813
|
+
containerGithubLocations.push(file.relativePath)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (shallowGithubLocations.length) {
|
|
818
|
+
addResult(
|
|
819
|
+
results,
|
|
820
|
+
'warning',
|
|
821
|
+
'GitHub Actions checkout may be shallow',
|
|
822
|
+
'actions/checkout defaults to a shallow checkout, which can limit git metadata and impacted-test detection.',
|
|
823
|
+
{
|
|
824
|
+
locations: shallowGithubLocations,
|
|
825
|
+
recommendation: 'Set fetch-depth: 0 for the checkout step, or keep git unshallowing enabled.',
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (containerGithubLocations.length) {
|
|
831
|
+
addResult(
|
|
832
|
+
results,
|
|
833
|
+
'info',
|
|
834
|
+
'Containerized GitHub jobs may need Git safe.directory',
|
|
835
|
+
'Git can reject metadata commands in containerized jobs when checkout ownership differs from the container user.',
|
|
836
|
+
{
|
|
837
|
+
locations: containerGithubLocations,
|
|
838
|
+
recommendation: 'Run git config --global --add safe.directory "$GITHUB_WORKSPACE" when needed.',
|
|
839
|
+
}
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (evidence.hasAgentlessEnabled && !evidence.hasApiKey && !env.DD_API_KEY && !env.DATADOG_API_KEY) {
|
|
844
|
+
addResult(
|
|
845
|
+
results,
|
|
846
|
+
'warning',
|
|
847
|
+
'Agentless mode is enabled but no API key reference was found',
|
|
848
|
+
'DD_CIVISIBILITY_AGENTLESS_ENABLED requires DD_API_KEY or DATADOG_API_KEY at runtime.',
|
|
849
|
+
{
|
|
850
|
+
recommendation: 'Provide DD_API_KEY or DATADOG_API_KEY as a CI secret in the test job.',
|
|
851
|
+
}
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (evidence.gitStrategyNoneLocations.length) {
|
|
856
|
+
addResult(
|
|
857
|
+
results,
|
|
858
|
+
'warning',
|
|
859
|
+
'CI configuration disables git checkout',
|
|
860
|
+
'Git metadata extraction cannot work when the CI job does not check out the repository.',
|
|
861
|
+
{
|
|
862
|
+
locations: evidence.gitStrategyNoneLocations,
|
|
863
|
+
recommendation: 'Enable repository checkout for Test Optimization jobs.',
|
|
864
|
+
}
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Checks local git availability and repository metadata.
|
|
871
|
+
*
|
|
872
|
+
* @param {Array<object>} results mutable result list
|
|
873
|
+
* @param {string} root repository root
|
|
874
|
+
* @param {NodeJS.ProcessEnv} env environment
|
|
875
|
+
* @param {Function} execFile command runner
|
|
876
|
+
* @param {string|undefined} gitExecutable trusted git executable
|
|
877
|
+
*/
|
|
878
|
+
function checkGit (results, root, env, execFile, gitExecutable) {
|
|
879
|
+
const gitEnv = getGitEnvironment(gitExecutable, env)
|
|
880
|
+
if (!canRunGit(execFile, root, gitExecutable, gitEnv)) {
|
|
881
|
+
addResult(
|
|
882
|
+
results,
|
|
883
|
+
'error',
|
|
884
|
+
'git executable is not available',
|
|
885
|
+
'Test Optimization uses git to extract repository metadata and impacted files.',
|
|
886
|
+
{ recommendation: 'Install git in the CI image or runner that executes tests.' }
|
|
887
|
+
)
|
|
888
|
+
return
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
addResult(results, 'ok', 'git executable found', 'The current environment can execute git.')
|
|
892
|
+
|
|
893
|
+
const insideWorktree = runGit(execFile, root, gitExecutable, gitEnv, ['rev-parse', '--is-inside-work-tree'])
|
|
894
|
+
if (insideWorktree !== 'true') {
|
|
895
|
+
addResult(
|
|
896
|
+
results,
|
|
897
|
+
'warning',
|
|
898
|
+
'Current path is not inside a git worktree',
|
|
899
|
+
'Git metadata extraction needs the checked-out repository.',
|
|
900
|
+
{ recommendation: 'Run the test command from inside the checked-out repository.' }
|
|
901
|
+
)
|
|
902
|
+
return
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const head = runGit(execFile, root, gitExecutable, gitEnv, ['rev-parse', 'HEAD'])
|
|
906
|
+
const remote = runGit(execFile, root, gitExecutable, gitEnv, ['config', '--get', 'remote.origin.url'])
|
|
907
|
+
const branch = runGit(execFile, root, gitExecutable, gitEnv, ['branch', '--show-current'])
|
|
908
|
+
const shallow = runGit(execFile, root, gitExecutable, gitEnv, ['rev-parse', '--is-shallow-repository'])
|
|
909
|
+
|
|
910
|
+
if (head) {
|
|
911
|
+
addResult(results, 'ok', 'git commit SHA detected', `Current HEAD is ${head.slice(0, 12)}.`)
|
|
912
|
+
} else {
|
|
913
|
+
addResult(
|
|
914
|
+
results,
|
|
915
|
+
'warning',
|
|
916
|
+
'git commit SHA could not be detected',
|
|
917
|
+
'The diagnosis could not read git rev-parse HEAD.',
|
|
918
|
+
{ recommendation: 'Ensure the CI checkout includes a valid git repository.' }
|
|
919
|
+
)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (!remote) {
|
|
923
|
+
addResult(
|
|
924
|
+
results,
|
|
925
|
+
'warning',
|
|
926
|
+
'git remote origin is not configured',
|
|
927
|
+
'Repository URL metadata may be missing.',
|
|
928
|
+
{ recommendation: 'Configure remote.origin.url or provide DD_GIT_REPOSITORY_URL.' }
|
|
929
|
+
)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (!branch && !hasBranchMetadata(env)) {
|
|
933
|
+
addResult(
|
|
934
|
+
results,
|
|
935
|
+
'warning',
|
|
936
|
+
'git branch metadata could not be detected',
|
|
937
|
+
'The checkout appears detached and no CI branch metadata was found in the current environment.',
|
|
938
|
+
{ recommendation: 'Provide branch metadata through CI provider variables or DD_GIT_BRANCH.' }
|
|
939
|
+
)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (shallow === 'true') {
|
|
943
|
+
addResult(
|
|
944
|
+
results,
|
|
945
|
+
'warning',
|
|
946
|
+
'Repository is shallow',
|
|
947
|
+
'A shallow repository can limit metadata upload and impacted-test detection.',
|
|
948
|
+
{ recommendation: 'Use a full checkout, or keep DD_CIVISIBILITY_GIT_UNSHALLOW_ENABLED enabled.' }
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Checks current environment variables relevant to Test Optimization.
|
|
955
|
+
*
|
|
956
|
+
* @param {Array<object>} results mutable result list
|
|
957
|
+
* @param {NodeJS.ProcessEnv} env environment
|
|
958
|
+
* @param {object} evidence repository evidence
|
|
959
|
+
*/
|
|
960
|
+
function checkCurrentEnvironment (results, env, evidence) {
|
|
961
|
+
if (isFalseLike(env.DD_CIVISIBILITY_ENABLED) || evidence.hasCiVisibilityDisabled) {
|
|
962
|
+
addResult(
|
|
963
|
+
results,
|
|
964
|
+
'error',
|
|
965
|
+
'Test Optimization is explicitly disabled',
|
|
966
|
+
'DD_CIVISIBILITY_ENABLED is set to false or 0.',
|
|
967
|
+
{ recommendation: 'Remove DD_CIVISIBILITY_ENABLED=false from the test job.' }
|
|
968
|
+
)
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (isFalseLike(env.DD_CIVISIBILITY_ITR_ENABLED) || evidence.hasItrDisabled) {
|
|
972
|
+
addResult(
|
|
973
|
+
results,
|
|
974
|
+
'warning',
|
|
975
|
+
'Test Impact Analysis is disabled',
|
|
976
|
+
'DD_CIVISIBILITY_ITR_ENABLED is set to false or 0.',
|
|
977
|
+
{ recommendation: 'Remove DD_CIVISIBILITY_ITR_ENABLED=false if suite skipping should be enabled.' }
|
|
978
|
+
)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (isFalseLike(env.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED) || evidence.hasGitUploadDisabled) {
|
|
982
|
+
addResult(
|
|
983
|
+
results,
|
|
984
|
+
'warning',
|
|
985
|
+
'Git metadata upload is disabled',
|
|
986
|
+
'DD_CIVISIBILITY_GIT_UPLOAD_ENABLED is set to false or 0.',
|
|
987
|
+
{ recommendation: 'Remove DD_CIVISIBILITY_GIT_UPLOAD_ENABLED=false unless this is intentional.' }
|
|
988
|
+
)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (isTrueLike(env.DD_CIVISIBILITY_AGENTLESS_ENABLED) && !env.DD_API_KEY && !env.DATADOG_API_KEY) {
|
|
992
|
+
addResult(
|
|
993
|
+
results,
|
|
994
|
+
'error',
|
|
995
|
+
'Agentless mode is missing an API key',
|
|
996
|
+
'The current environment has DD_CIVISIBILITY_AGENTLESS_ENABLED set, but no DD_API_KEY or DATADOG_API_KEY.',
|
|
997
|
+
{ recommendation: 'Set DD_API_KEY or DATADOG_API_KEY in the CI job.' }
|
|
998
|
+
)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (!env.DD_SERVICE && !evidence.hasService) {
|
|
1002
|
+
addResult(
|
|
1003
|
+
results,
|
|
1004
|
+
'warning',
|
|
1005
|
+
'DD_SERVICE was not found',
|
|
1006
|
+
'A missing service name makes Test Optimization data harder to find and group.',
|
|
1007
|
+
{ recommendation: 'Set DD_SERVICE in the test job.' }
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if ((env.OTEL_TRACES_EXPORTER || '').toLowerCase() === 'otlp' || evidence.hasOtelOtlpExporter) {
|
|
1012
|
+
addResult(
|
|
1013
|
+
results,
|
|
1014
|
+
'warning',
|
|
1015
|
+
'OTEL_TRACES_EXPORTER=otlp was found',
|
|
1016
|
+
'An OTLP traces exporter can interfere with dd-trace test payloads in instrumented shells.',
|
|
1017
|
+
{ recommendation: 'Unset OTEL_TRACES_EXPORTER for dd-trace Test Optimization jobs.' }
|
|
1018
|
+
)
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (env.CI) {
|
|
1022
|
+
checkCurrentCiMetadata(results, env)
|
|
1023
|
+
} else {
|
|
1024
|
+
addResult(
|
|
1025
|
+
results,
|
|
1026
|
+
'info',
|
|
1027
|
+
'Current process is not running in CI',
|
|
1028
|
+
'Runtime CI metadata checks were skipped because CI is not set in the current environment.'
|
|
1029
|
+
)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Checks current CI provider metadata.
|
|
1035
|
+
*
|
|
1036
|
+
* @param {Array<object>} results mutable result list
|
|
1037
|
+
* @param {NodeJS.ProcessEnv} env environment
|
|
1038
|
+
*/
|
|
1039
|
+
function checkCurrentCiMetadata (results, env) {
|
|
1040
|
+
const providerDetected = CURRENT_ENV_PROVIDER_KEYS.some(key => env[key])
|
|
1041
|
+
if (!providerDetected) {
|
|
1042
|
+
addResult(
|
|
1043
|
+
results,
|
|
1044
|
+
'warning',
|
|
1045
|
+
'Current CI provider is not recognized',
|
|
1046
|
+
'The current environment has CI set, but no known CI provider variables were found.',
|
|
1047
|
+
{ recommendation: 'Provide CI and git metadata with DD_GIT_* variables if the provider is custom.' }
|
|
1048
|
+
)
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!hasShaMetadata(env)) {
|
|
1052
|
+
addResult(
|
|
1053
|
+
results,
|
|
1054
|
+
'warning',
|
|
1055
|
+
'Current CI commit SHA metadata is missing',
|
|
1056
|
+
'The current CI environment does not expose a recognized commit SHA variable.',
|
|
1057
|
+
{ recommendation: 'Provide DD_GIT_COMMIT_SHA when the CI provider does not expose one.' }
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (!hasBranchMetadata(env)) {
|
|
1062
|
+
addResult(
|
|
1063
|
+
results,
|
|
1064
|
+
'warning',
|
|
1065
|
+
'Current CI branch metadata is missing',
|
|
1066
|
+
'The current CI environment does not expose a recognized branch or tag variable.',
|
|
1067
|
+
{ recommendation: 'Provide DD_GIT_BRANCH or DD_GIT_TAG when the CI provider does not expose one.' }
|
|
1068
|
+
)
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Collects package scripts from all manifests.
|
|
1074
|
+
*
|
|
1075
|
+
* @param {Array<object>} manifests package manifests
|
|
1076
|
+
* @returns {Array<object>} scripts
|
|
1077
|
+
*/
|
|
1078
|
+
function collectScripts (manifests) {
|
|
1079
|
+
const scripts = []
|
|
1080
|
+
|
|
1081
|
+
for (const manifest of manifests) {
|
|
1082
|
+
const manifestScripts = manifest.json.scripts || {}
|
|
1083
|
+
for (const [name, command] of Object.entries(manifestScripts)) {
|
|
1084
|
+
if (typeof command !== 'string') continue
|
|
1085
|
+
|
|
1086
|
+
scripts.push({
|
|
1087
|
+
name,
|
|
1088
|
+
command,
|
|
1089
|
+
relativePath: manifest.relativePath,
|
|
1090
|
+
})
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return scripts
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Detects supported test frameworks.
|
|
1099
|
+
*
|
|
1100
|
+
* @param {string} root repository root
|
|
1101
|
+
* @param {Array<object>} definitions framework definitions
|
|
1102
|
+
* @param {Array<object>} manifests package manifests
|
|
1103
|
+
* @param {Array<object>} scripts package scripts
|
|
1104
|
+
* @param {Array<object>} textFiles scanned text files
|
|
1105
|
+
* @returns {Array<object>} detected supported frameworks
|
|
1106
|
+
*/
|
|
1107
|
+
function detectSupportedFrameworks (root, definitions, manifests, scripts, textFiles) {
|
|
1108
|
+
const frameworks = []
|
|
1109
|
+
|
|
1110
|
+
for (const definition of definitions) {
|
|
1111
|
+
const dependencyEntries = findDependencyEntries(manifests, definition.packages)
|
|
1112
|
+
const scriptMatches = findScriptMatches(scripts, definition.commandPatterns)
|
|
1113
|
+
const configMatches = findConfigMatches(textFiles, definition.configPatterns)
|
|
1114
|
+
|
|
1115
|
+
if (!dependencyEntries.length && !scriptMatches.length && !configMatches.length) continue
|
|
1116
|
+
|
|
1117
|
+
frameworks.push({
|
|
1118
|
+
...definition,
|
|
1119
|
+
dependencyEntries,
|
|
1120
|
+
scriptMatches,
|
|
1121
|
+
configMatches,
|
|
1122
|
+
locations: unique([
|
|
1123
|
+
...dependencyEntries.map(entry => entry.relativePath),
|
|
1124
|
+
...scriptMatches.map(script => script.relativePath),
|
|
1125
|
+
...configMatches,
|
|
1126
|
+
]),
|
|
1127
|
+
versionDetections: getVersionDetections(root, definition.packages, dependencyEntries),
|
|
1128
|
+
})
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return frameworks
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Gets frameworks that are eligible live-validation candidates.
|
|
1136
|
+
*
|
|
1137
|
+
* @param {Array<object>} frameworks detected supported frameworks
|
|
1138
|
+
* @returns {Array<object>} eligible frameworks with eligibility details
|
|
1139
|
+
*/
|
|
1140
|
+
function getEligibleFrameworks (frameworks) {
|
|
1141
|
+
const eligible = []
|
|
1142
|
+
|
|
1143
|
+
for (const framework of frameworks) {
|
|
1144
|
+
const command = getEligibleCommandMatch(framework)
|
|
1145
|
+
const version = getSupportedVersionDetection(framework, command?.relativePath)
|
|
1146
|
+
const reasons = []
|
|
1147
|
+
|
|
1148
|
+
if (!version) reasons.push(`No statically supported ${framework.name} version was found.`)
|
|
1149
|
+
if (!command) reasons.push(`No eligible ${framework.name} test command was found.`)
|
|
1150
|
+
|
|
1151
|
+
if (!version || !command) continue
|
|
1152
|
+
|
|
1153
|
+
eligible.push({
|
|
1154
|
+
...framework,
|
|
1155
|
+
eligibleCommand: command,
|
|
1156
|
+
eligibleVersion: version,
|
|
1157
|
+
eligibility: {
|
|
1158
|
+
command: command.command,
|
|
1159
|
+
commandLocation: command.relativePath,
|
|
1160
|
+
version: version.version,
|
|
1161
|
+
versionLocation: version.relativePath,
|
|
1162
|
+
},
|
|
1163
|
+
ineligibleReasons: reasons,
|
|
1164
|
+
})
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return eligible
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Gets the first supported version detection for a framework.
|
|
1172
|
+
*
|
|
1173
|
+
* @param {object} framework detected framework
|
|
1174
|
+
* @param {string} [relativePath] package manifest that owns the selected command
|
|
1175
|
+
* @returns {object|undefined} supported version detection
|
|
1176
|
+
*/
|
|
1177
|
+
function getSupportedVersionDetection (framework, relativePath) {
|
|
1178
|
+
const detections = framework.versionDetections || []
|
|
1179
|
+
const localDetections = relativePath
|
|
1180
|
+
? detections.filter(detection => detection.relativePath === relativePath)
|
|
1181
|
+
: []
|
|
1182
|
+
const candidates = localDetections.length > 0 ? localDetections : detections
|
|
1183
|
+
|
|
1184
|
+
for (const detection of candidates) {
|
|
1185
|
+
if (detection.version && satisfies(detection.version, framework.supportedRange)) return detection
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Gets an eligible script command for a framework.
|
|
1191
|
+
*
|
|
1192
|
+
* @param {object} framework detected framework
|
|
1193
|
+
* @returns {object|undefined} eligible command match
|
|
1194
|
+
*/
|
|
1195
|
+
function getEligibleCommandMatch (framework) {
|
|
1196
|
+
const scriptMatches = framework.scriptMatches || []
|
|
1197
|
+
|
|
1198
|
+
for (const script of scriptMatches) {
|
|
1199
|
+
if (isIneligibleFrameworkCommand(framework.id, script.command)) continue
|
|
1200
|
+
return script
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (scriptMatches.length > 0) return
|
|
1204
|
+
|
|
1205
|
+
if (framework.id === 'jest' || framework.id === 'mocha' || framework.id === 'vitest') {
|
|
1206
|
+
return framework.dependencyEntries?.[0] && {
|
|
1207
|
+
command: `direct ${framework.id} binary`,
|
|
1208
|
+
relativePath: framework.dependencyEntries[0].relativePath,
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Checks whether a framework command is ineligible for live validation.
|
|
1215
|
+
*
|
|
1216
|
+
* @param {string} frameworkId framework id
|
|
1217
|
+
* @param {string} command package script command
|
|
1218
|
+
* @returns {boolean} whether the command is ineligible
|
|
1219
|
+
*/
|
|
1220
|
+
function isIneligibleFrameworkCommand (frameworkId, command) {
|
|
1221
|
+
if (frameworkId === 'vitest' && /\bvitest\s+bench\b/.test(command)) return true
|
|
1222
|
+
if (WATCH_MODE_RE.test(command)) return true
|
|
1223
|
+
|
|
1224
|
+
return false
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Detects unsupported test frameworks.
|
|
1229
|
+
*
|
|
1230
|
+
* @param {Array<object>} definitions unsupported framework definitions
|
|
1231
|
+
* @param {Array<object>} manifests package manifests
|
|
1232
|
+
* @param {Array<object>} scripts package scripts
|
|
1233
|
+
* @returns {Array<object>} detected unsupported frameworks
|
|
1234
|
+
*/
|
|
1235
|
+
function detectUnsupportedFrameworks (definitions, manifests, scripts) {
|
|
1236
|
+
const frameworks = []
|
|
1237
|
+
|
|
1238
|
+
for (const definition of definitions) {
|
|
1239
|
+
const dependencyEntries = findDependencyEntries(manifests, definition.packages)
|
|
1240
|
+
const scriptMatches = findScriptMatches(scripts, definition.commandPatterns)
|
|
1241
|
+
|
|
1242
|
+
if (!dependencyEntries.length && !scriptMatches.length) continue
|
|
1243
|
+
|
|
1244
|
+
frameworks.push({
|
|
1245
|
+
...definition,
|
|
1246
|
+
locations: unique([
|
|
1247
|
+
...dependencyEntries.map(entry => entry.relativePath),
|
|
1248
|
+
...scriptMatches.map(script => script.relativePath),
|
|
1249
|
+
]),
|
|
1250
|
+
})
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
return frameworks
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Collects useful boolean evidence from scanned files and environment.
|
|
1258
|
+
*
|
|
1259
|
+
* @param {Array<object>} textFiles scanned text files
|
|
1260
|
+
* @param {NodeJS.ProcessEnv} env environment
|
|
1261
|
+
* @returns {object} evidence object
|
|
1262
|
+
*/
|
|
1263
|
+
function collectEvidence (textFiles, env) {
|
|
1264
|
+
const ciInitLocations = findNodeOptionsPreloadLocations(textFiles, INIT_PRELOAD_RE)
|
|
1265
|
+
const registerLocations = findNodeOptionsPreloadLocations(textFiles, REGISTER_PRELOAD_RE)
|
|
1266
|
+
|
|
1267
|
+
return {
|
|
1268
|
+
ciInitLocations,
|
|
1269
|
+
directCiInitLocations: findLocations(textFiles, DIRECT_CI_INIT_RE),
|
|
1270
|
+
wrongInitLocations: findLocations(textFiles.filter(isTestSetupOrCiFile), WRONG_INIT_RE),
|
|
1271
|
+
cypressManualPluginLocations: findLocations(textFiles, CYPRESS_MANUAL_PLUGIN_RE),
|
|
1272
|
+
cypressSupportLocations: findLocations(textFiles, CYPRESS_SUPPORT_RE),
|
|
1273
|
+
cypressSupportDisabledLocations: findLocations(textFiles, CYPRESS_SUPPORT_DISABLED_RE),
|
|
1274
|
+
gitStrategyNoneLocations: findLocations(textFiles, /GIT_STRATEGY\s*:\s*none\b/i),
|
|
1275
|
+
hasCiInit: ciInitLocations.length > 0,
|
|
1276
|
+
hasRegister: registerLocations.length > 0,
|
|
1277
|
+
hasCiVisibilityDisabled: findLocations(textFiles, CI_DISABLED_RE).length > 0,
|
|
1278
|
+
hasItrDisabled: findLocations(textFiles, ITR_DISABLED_RE).length > 0,
|
|
1279
|
+
hasGitUploadDisabled: findLocations(textFiles, GIT_UPLOAD_DISABLED_RE).length > 0,
|
|
1280
|
+
hasAgentlessEnabled: findLocations(textFiles, AGENTLESS_ENABLED_RE).length > 0 ||
|
|
1281
|
+
isTrueLike(env.DD_CIVISIBILITY_AGENTLESS_ENABLED),
|
|
1282
|
+
hasApiKey: findLocations(textFiles, API_KEY_RE).length > 0,
|
|
1283
|
+
hasService: findLocations(textFiles, SERVICE_RE).length > 0,
|
|
1284
|
+
hasOtelOtlpExporter: findLocations(textFiles, OTEL_OTLP_RE).length > 0,
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Finds dependency entries in package manifests.
|
|
1290
|
+
*
|
|
1291
|
+
* @param {Array<object>} manifests package manifests
|
|
1292
|
+
* @param {string[]} packageNames package names
|
|
1293
|
+
* @returns {Array<object>} matching dependency entries
|
|
1294
|
+
*/
|
|
1295
|
+
function findDependencyEntries (manifests, packageNames) {
|
|
1296
|
+
const entries = []
|
|
1297
|
+
const packageSet = new Set(packageNames)
|
|
1298
|
+
|
|
1299
|
+
for (const manifest of manifests) {
|
|
1300
|
+
for (const section of PACKAGE_SECTIONS) {
|
|
1301
|
+
const dependencies = manifest.json[section]
|
|
1302
|
+
if (!dependencies) continue
|
|
1303
|
+
|
|
1304
|
+
for (const [name, range] of Object.entries(dependencies)) {
|
|
1305
|
+
if (!packageSet.has(name)) continue
|
|
1306
|
+
|
|
1307
|
+
entries.push({
|
|
1308
|
+
packageName: name,
|
|
1309
|
+
rawVersion: String(range),
|
|
1310
|
+
section,
|
|
1311
|
+
relativePath: manifest.relativePath,
|
|
1312
|
+
})
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return entries
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Resolves framework package versions from installed modules and manifest ranges.
|
|
1322
|
+
*
|
|
1323
|
+
* @param {string} root repository root
|
|
1324
|
+
* @param {string[]} packageNames package names
|
|
1325
|
+
* @param {Array<object>} dependencyEntries dependency entries
|
|
1326
|
+
* @returns {Array<object>} version detections
|
|
1327
|
+
*/
|
|
1328
|
+
function getVersionDetections (root, packageNames, dependencyEntries) {
|
|
1329
|
+
const detections = []
|
|
1330
|
+
|
|
1331
|
+
for (const entry of dependencyEntries) {
|
|
1332
|
+
const packageRoot = path.dirname(path.resolve(root, entry.relativePath))
|
|
1333
|
+
const installedVersion = getInstalledPackageVersion(root, entry.packageName, packageRoot)
|
|
1334
|
+
detections.push({
|
|
1335
|
+
packageName: entry.packageName,
|
|
1336
|
+
rawVersion: installedVersion || entry.rawVersion,
|
|
1337
|
+
version: installedVersion || coerceVersion(entry.rawVersion),
|
|
1338
|
+
relativePath: entry.relativePath,
|
|
1339
|
+
source: installedVersion ? 'installed' : 'manifest',
|
|
1340
|
+
})
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (detections.length > 0) return uniqueVersionDetections(detections)
|
|
1344
|
+
|
|
1345
|
+
for (const packageName of packageNames) {
|
|
1346
|
+
const version = getInstalledPackageVersion(root, packageName, root)
|
|
1347
|
+
if (!version) continue
|
|
1348
|
+
|
|
1349
|
+
detections.push({
|
|
1350
|
+
packageName,
|
|
1351
|
+
rawVersion: version,
|
|
1352
|
+
version,
|
|
1353
|
+
source: 'installed',
|
|
1354
|
+
})
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
return uniqueVersionDetections(detections)
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Reads an installed package version from node_modules.
|
|
1362
|
+
*
|
|
1363
|
+
* @param {string} root repository root
|
|
1364
|
+
* @param {string} packageName package name
|
|
1365
|
+
* @param {string} packageRoot package directory where resolution begins
|
|
1366
|
+
* @returns {string|undefined} installed version
|
|
1367
|
+
*/
|
|
1368
|
+
function getInstalledPackageVersion (root, packageName, packageRoot) {
|
|
1369
|
+
const repositoryRoot = path.resolve(root)
|
|
1370
|
+
let directory = path.resolve(packageRoot)
|
|
1371
|
+
if (!isPathInside(repositoryRoot, directory)) return
|
|
1372
|
+
|
|
1373
|
+
while (isPathInside(repositoryRoot, directory)) {
|
|
1374
|
+
const packageJsonPath = path.join(directory, 'node_modules', ...packageName.split('/'), 'package.json')
|
|
1375
|
+
const json = readJsonFile(packageJsonPath)
|
|
1376
|
+
if (typeof json?.version === 'string') return json.version
|
|
1377
|
+
if (directory === repositoryRoot) return
|
|
1378
|
+
|
|
1379
|
+
const parent = path.dirname(directory)
|
|
1380
|
+
if (parent === directory) return
|
|
1381
|
+
directory = parent
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Finds scripts matching any pattern.
|
|
1387
|
+
*
|
|
1388
|
+
* @param {Array<object>} scripts package scripts
|
|
1389
|
+
* @param {RegExp[]} patterns command patterns
|
|
1390
|
+
* @returns {Array<object>} matching scripts
|
|
1391
|
+
*/
|
|
1392
|
+
function findScriptMatches (scripts, patterns) {
|
|
1393
|
+
const matches = []
|
|
1394
|
+
|
|
1395
|
+
for (const script of scripts) {
|
|
1396
|
+
if (!/test|spec|e2e|integration|unit/i.test(script.name) &&
|
|
1397
|
+
!patterns.some(pattern => pattern.test(script.command))) {
|
|
1398
|
+
continue
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (patterns.some(pattern => pattern.test(script.command))) {
|
|
1402
|
+
matches.push(script)
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
return matches
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Finds scanned config files that match definition patterns.
|
|
1411
|
+
*
|
|
1412
|
+
* @param {Array<object>} textFiles scanned text files
|
|
1413
|
+
* @param {RegExp[]} patterns filename patterns
|
|
1414
|
+
* @returns {string[]} matching relative paths
|
|
1415
|
+
*/
|
|
1416
|
+
function findConfigMatches (textFiles, patterns) {
|
|
1417
|
+
const matches = []
|
|
1418
|
+
|
|
1419
|
+
for (const file of textFiles) {
|
|
1420
|
+
const basename = path.basename(file.relativePath)
|
|
1421
|
+
if (patterns.some(pattern => pattern.test(basename))) {
|
|
1422
|
+
matches.push(file.relativePath)
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
return matches
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Finds Jest TypeScript config files.
|
|
1431
|
+
*
|
|
1432
|
+
* @param {Array<object>} textFiles scanned text files
|
|
1433
|
+
* @returns {string[]} matching relative paths
|
|
1434
|
+
*/
|
|
1435
|
+
function findJestTypescriptConfigLocations (textFiles) {
|
|
1436
|
+
return textFiles
|
|
1437
|
+
.map(file => file.relativePath)
|
|
1438
|
+
.filter(relativePath => /(?:^|\/)jest\.config\.(?:ts|mts|cts)$/.test(relativePath))
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Finds files where NODE_OPTIONS appears close to a supported Node preload option.
|
|
1443
|
+
*
|
|
1444
|
+
* @param {Array<object>} textFiles scanned text files
|
|
1445
|
+
* @param {RegExp} preloadPattern Node preload option pattern
|
|
1446
|
+
* @returns {string[]} matching relative paths
|
|
1447
|
+
*/
|
|
1448
|
+
function findNodeOptionsPreloadLocations (textFiles, preloadPattern) {
|
|
1449
|
+
const locations = []
|
|
1450
|
+
|
|
1451
|
+
for (const file of textFiles) {
|
|
1452
|
+
const lines = file.content.split(/\r?\n/)
|
|
1453
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1454
|
+
if (!NODE_OPTIONS_RE.test(lines[i])) continue
|
|
1455
|
+
|
|
1456
|
+
const window = lines.slice(i, i + 4).join('\n')
|
|
1457
|
+
preloadPattern.lastIndex = 0
|
|
1458
|
+
if (preloadPattern.test(window)) {
|
|
1459
|
+
locations.push(file.relativePath)
|
|
1460
|
+
break
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
return unique(locations)
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Finds files whose content matches a pattern.
|
|
1470
|
+
*
|
|
1471
|
+
* @param {Array<object>} textFiles scanned text files
|
|
1472
|
+
* @param {RegExp} pattern content pattern
|
|
1473
|
+
* @returns {string[]} matching relative paths
|
|
1474
|
+
*/
|
|
1475
|
+
function findLocations (textFiles, pattern) {
|
|
1476
|
+
const locations = []
|
|
1477
|
+
|
|
1478
|
+
for (const file of textFiles) {
|
|
1479
|
+
pattern.lastIndex = 0
|
|
1480
|
+
if (pattern.test(file.content)) {
|
|
1481
|
+
locations.push(file.relativePath)
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
return unique(locations)
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Collects text-like repository files.
|
|
1490
|
+
*
|
|
1491
|
+
* @param {string} root repository root
|
|
1492
|
+
* @param {string|undefined} physicalRoot physical repository root
|
|
1493
|
+
* @param {number} maxFiles maximum number of files
|
|
1494
|
+
* @param {string[]} [excludePaths] repository paths excluded from the scan
|
|
1495
|
+
* @returns {Array<string> & {truncated?: boolean}} relative paths
|
|
1496
|
+
*/
|
|
1497
|
+
function collectTextFiles (root, physicalRoot, maxFiles, excludePaths = []) {
|
|
1498
|
+
const files = []
|
|
1499
|
+
const seen = new Set()
|
|
1500
|
+
const exclusions = getScanExclusions(root, excludePaths)
|
|
1501
|
+
files.truncated = false
|
|
1502
|
+
|
|
1503
|
+
addTextFileIfPresent('package.json')
|
|
1504
|
+
|
|
1505
|
+
function walk (dir, relativeDir) {
|
|
1506
|
+
if (files.length >= maxFiles) {
|
|
1507
|
+
files.truncated = true
|
|
1508
|
+
return
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
let entries
|
|
1512
|
+
try {
|
|
1513
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1514
|
+
} catch {
|
|
1515
|
+
return
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
1519
|
+
|
|
1520
|
+
for (const entry of entries) {
|
|
1521
|
+
if (files.length >= maxFiles) {
|
|
1522
|
+
files.truncated = true
|
|
1523
|
+
return
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const relativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name
|
|
1527
|
+
const absolutePath = path.join(root, relativePath)
|
|
1528
|
+
if (isScanPathExcluded(relativePath, exclusions)) continue
|
|
1529
|
+
|
|
1530
|
+
if (entry.isDirectory()) {
|
|
1531
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) {
|
|
1532
|
+
walk(absolutePath, relativePath)
|
|
1533
|
+
}
|
|
1534
|
+
continue
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
if (entry.isFile() && isTextFileName(entry.name)) {
|
|
1538
|
+
addTextFile(relativePath)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function addTextFileIfPresent (relativePath) {
|
|
1544
|
+
if (!getSafeScannedFile(root, physicalRoot, relativePath)) return
|
|
1545
|
+
addTextFile(relativePath)
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function addTextFile (relativePath) {
|
|
1549
|
+
const normalizedPath = normalizeRelativePath(relativePath)
|
|
1550
|
+
if (seen.has(normalizedPath) || SKIPPED_FILES.has(normalizedPath)) return
|
|
1551
|
+
|
|
1552
|
+
if (files.length >= maxFiles) {
|
|
1553
|
+
files.truncated = true
|
|
1554
|
+
return
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
seen.add(normalizedPath)
|
|
1558
|
+
files.push(relativePath)
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
walk(root, '')
|
|
1562
|
+
return files
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Normalizes caller-provided scan exclusions to repository-relative paths.
|
|
1567
|
+
*
|
|
1568
|
+
* @param {string} root repository root
|
|
1569
|
+
* @param {string[]} excludePaths paths to exclude
|
|
1570
|
+
* @returns {Set<string>} normalized repository-relative exclusions
|
|
1571
|
+
*/
|
|
1572
|
+
function getScanExclusions (root, excludePaths) {
|
|
1573
|
+
const exclusions = new Set()
|
|
1574
|
+
for (const candidate of excludePaths) {
|
|
1575
|
+
if (typeof candidate !== 'string') continue
|
|
1576
|
+
const relative = path.relative(root, path.resolve(root, candidate))
|
|
1577
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) continue
|
|
1578
|
+
exclusions.add(normalizeRelativePath(relative))
|
|
1579
|
+
}
|
|
1580
|
+
return exclusions
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Checks whether a scanned path is inside a caller-provided exclusion.
|
|
1585
|
+
*
|
|
1586
|
+
* @param {string} relativePath repository-relative path
|
|
1587
|
+
* @param {Set<string>} exclusions normalized exclusions
|
|
1588
|
+
* @returns {boolean} whether the path must be skipped
|
|
1589
|
+
*/
|
|
1590
|
+
function isScanPathExcluded (relativePath, exclusions) {
|
|
1591
|
+
const normalizedPath = normalizeRelativePath(relativePath)
|
|
1592
|
+
for (const exclusion of exclusions) {
|
|
1593
|
+
if (normalizedPath === exclusion || normalizedPath.startsWith(`${exclusion}/`)) return true
|
|
1594
|
+
}
|
|
1595
|
+
return false
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Reads scanned text files within the size limit.
|
|
1600
|
+
*
|
|
1601
|
+
* @param {string} root repository root
|
|
1602
|
+
* @param {string|undefined} physicalRoot physical repository root
|
|
1603
|
+
* @param {string[]} files relative file paths
|
|
1604
|
+
* @param {number} maxTotalBytes maximum aggregate bytes to retain
|
|
1605
|
+
* @returns {Array<object>} text file records
|
|
1606
|
+
*/
|
|
1607
|
+
function readTextFiles (root, physicalRoot, files, maxTotalBytes) {
|
|
1608
|
+
const textFiles = []
|
|
1609
|
+
let totalBytes = 0
|
|
1610
|
+
textFiles.truncated = false
|
|
1611
|
+
|
|
1612
|
+
for (const relativePath of files) {
|
|
1613
|
+
const file = getSafeScannedFile(root, physicalRoot, relativePath)
|
|
1614
|
+
if (!file) continue
|
|
1615
|
+
|
|
1616
|
+
if (file.stat.size > MAX_TEXT_FILE_SIZE) continue
|
|
1617
|
+
if (totalBytes + file.stat.size > maxTotalBytes) {
|
|
1618
|
+
textFiles.truncated = true
|
|
1619
|
+
break
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
try {
|
|
1623
|
+
const content = fs.readFileSync(file.filename, 'utf8')
|
|
1624
|
+
const contentBytes = Buffer.byteLength(content)
|
|
1625
|
+
if (contentBytes > MAX_TEXT_FILE_SIZE) continue
|
|
1626
|
+
if (totalBytes + contentBytes > maxTotalBytes) {
|
|
1627
|
+
textFiles.truncated = true
|
|
1628
|
+
break
|
|
1629
|
+
}
|
|
1630
|
+
textFiles.push({
|
|
1631
|
+
relativePath: normalizeRelativePath(relativePath),
|
|
1632
|
+
content,
|
|
1633
|
+
})
|
|
1634
|
+
totalBytes += contentBytes
|
|
1635
|
+
} catch {
|
|
1636
|
+
// Ignore files that cannot be read as UTF-8.
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
return textFiles
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Resolves the physical repository root used to constrain scanned file reads.
|
|
1645
|
+
*
|
|
1646
|
+
* @param {string} root repository root
|
|
1647
|
+
* @returns {string|undefined} physical directory path
|
|
1648
|
+
*/
|
|
1649
|
+
function getPhysicalRoot (root) {
|
|
1650
|
+
try {
|
|
1651
|
+
const physicalRoot = fs.realpathSync(root)
|
|
1652
|
+
return fs.statSync(physicalRoot).isDirectory() ? physicalRoot : undefined
|
|
1653
|
+
} catch {}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Resolves one regular non-symlink file without escaping the physical repository root.
|
|
1658
|
+
*
|
|
1659
|
+
* @param {string} root lexical repository root
|
|
1660
|
+
* @param {string|undefined} physicalRoot physical repository root
|
|
1661
|
+
* @param {string} relativePath repository-relative candidate
|
|
1662
|
+
* @returns {{filename: string, stat: fs.Stats}|undefined} safe file information
|
|
1663
|
+
*/
|
|
1664
|
+
function getSafeScannedFile (root, physicalRoot, relativePath) {
|
|
1665
|
+
if (!physicalRoot) return
|
|
1666
|
+
|
|
1667
|
+
const lexicalRoot = path.resolve(root)
|
|
1668
|
+
const absolutePath = path.resolve(lexicalRoot, relativePath)
|
|
1669
|
+
if (!isPathInside(lexicalRoot, absolutePath)) return
|
|
1670
|
+
|
|
1671
|
+
try {
|
|
1672
|
+
const entry = fs.lstatSync(absolutePath)
|
|
1673
|
+
if (!entry.isFile() || entry.isSymbolicLink()) return
|
|
1674
|
+
|
|
1675
|
+
const filename = fs.realpathSync(absolutePath)
|
|
1676
|
+
if (!isPathInside(physicalRoot, filename)) return
|
|
1677
|
+
|
|
1678
|
+
const stat = fs.statSync(filename)
|
|
1679
|
+
return stat.isFile() ? { filename, stat } : undefined
|
|
1680
|
+
} catch {}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* Checks lexical or physical path containment.
|
|
1685
|
+
*
|
|
1686
|
+
* @param {string} root allowed root
|
|
1687
|
+
* @param {string} filename candidate path
|
|
1688
|
+
* @returns {boolean} whether the candidate is inside the root
|
|
1689
|
+
*/
|
|
1690
|
+
function isPathInside (root, filename) {
|
|
1691
|
+
const relative = path.relative(root, filename)
|
|
1692
|
+
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/**
|
|
1696
|
+
* Checks whether a filename should be scanned as text.
|
|
1697
|
+
*
|
|
1698
|
+
* @param {string} name filename
|
|
1699
|
+
* @returns {boolean} true if the file is text-like
|
|
1700
|
+
*/
|
|
1701
|
+
function isTextFileName (name) {
|
|
1702
|
+
return TEXT_FILE_NAMES.has(name) || TEXT_EXTENSIONS.has(path.extname(name))
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Checks whether a file is a common CI workflow file.
|
|
1707
|
+
*
|
|
1708
|
+
* @param {string} relativePath relative path
|
|
1709
|
+
* @returns {boolean} true if the path is a workflow file
|
|
1710
|
+
*/
|
|
1711
|
+
function isWorkflowFile (relativePath) {
|
|
1712
|
+
return relativePath.startsWith('.github/workflows/') ||
|
|
1713
|
+
relativePath === '.gitlab-ci.yml' ||
|
|
1714
|
+
relativePath === '.gitlab-ci.yaml' ||
|
|
1715
|
+
relativePath === 'bitbucket-pipelines.yml' ||
|
|
1716
|
+
relativePath === 'bitbucket-pipelines.yaml' ||
|
|
1717
|
+
relativePath === 'azure-pipelines.yml' ||
|
|
1718
|
+
relativePath === 'azure-pipelines.yaml' ||
|
|
1719
|
+
/^\.azure-pipelines\/.+\.ya?ml$/.test(relativePath) ||
|
|
1720
|
+
relativePath === 'Jenkinsfile' ||
|
|
1721
|
+
relativePath === '.circleci/config.yml' ||
|
|
1722
|
+
relativePath === '.circleci/config.yaml' ||
|
|
1723
|
+
relativePath === '.buildkite/pipeline.yml' ||
|
|
1724
|
+
relativePath === '.buildkite/pipeline.yaml'
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Checks whether a scanned file is likely to configure test process initialization.
|
|
1729
|
+
*
|
|
1730
|
+
* @param {object} file scanned text file
|
|
1731
|
+
* @returns {boolean} true when plain dd-trace init should be treated as test setup evidence
|
|
1732
|
+
*/
|
|
1733
|
+
function isTestSetupOrCiFile (file) {
|
|
1734
|
+
const relativePath = file.relativePath
|
|
1735
|
+
const basename = path.basename(relativePath)
|
|
1736
|
+
|
|
1737
|
+
if (isWorkflowFile(relativePath)) return true
|
|
1738
|
+
if (basename === 'package.json') return true
|
|
1739
|
+
if (/^(?:jest|config-jest|vitest|vite|playwright|cypress|cucumber)\.config\./.test(basename)) return true
|
|
1740
|
+
if (/^\.mocharc\./.test(basename)) return true
|
|
1741
|
+
if (basename === 'cypress.json') return true
|
|
1742
|
+
if (/(?:setup|bootstrap)/i.test(basename)) return true
|
|
1743
|
+
if (relativePath.startsWith('cypress/support/')) return true
|
|
1744
|
+
|
|
1745
|
+
return false
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Runs git --version to check availability.
|
|
1750
|
+
*
|
|
1751
|
+
* @param {Function} execFile command runner
|
|
1752
|
+
* @param {string} root repository root
|
|
1753
|
+
* @param {string|undefined} gitExecutable trusted git executable
|
|
1754
|
+
* @param {NodeJS.ProcessEnv} env credential-free git environment
|
|
1755
|
+
* @returns {boolean} true if git runs
|
|
1756
|
+
*/
|
|
1757
|
+
function canRunGit (execFile, root, gitExecutable, env) {
|
|
1758
|
+
if (!gitExecutable) return false
|
|
1759
|
+
|
|
1760
|
+
try {
|
|
1761
|
+
execFile(gitExecutable, ['--version'], { cwd: root, env, stdio: 'pipe', timeout: 2000 })
|
|
1762
|
+
return true
|
|
1763
|
+
} catch {
|
|
1764
|
+
return false
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Runs a git command and trims output.
|
|
1770
|
+
*
|
|
1771
|
+
* @param {Function} execFile command runner
|
|
1772
|
+
* @param {string} root repository root
|
|
1773
|
+
* @param {string} gitExecutable trusted git executable
|
|
1774
|
+
* @param {NodeJS.ProcessEnv} env credential-free git environment
|
|
1775
|
+
* @param {string[]} args git arguments
|
|
1776
|
+
* @returns {string} command output
|
|
1777
|
+
*/
|
|
1778
|
+
function runGit (execFile, root, gitExecutable, env, args) {
|
|
1779
|
+
try {
|
|
1780
|
+
return String(execFile(gitExecutable, args, { cwd: root, env, stdio: 'pipe', timeout: 2000 })).trim()
|
|
1781
|
+
} catch {
|
|
1782
|
+
return ''
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Resolves git without trusting repository-controlled PATH entries.
|
|
1788
|
+
*
|
|
1789
|
+
* @returns {string|undefined} trusted absolute git path
|
|
1790
|
+
*/
|
|
1791
|
+
function findTrustedGitExecutable () {
|
|
1792
|
+
const candidates = process.platform === 'win32'
|
|
1793
|
+
? [
|
|
1794
|
+
String.raw`C:\Program Files\Git\cmd\git.exe`,
|
|
1795
|
+
String.raw`C:\Program Files\Git\bin\git.exe`,
|
|
1796
|
+
String.raw`C:\Program Files (x86)\Git\cmd\git.exe`,
|
|
1797
|
+
String.raw`C:\Program Files (x86)\Git\bin\git.exe`,
|
|
1798
|
+
]
|
|
1799
|
+
: ['/usr/bin/git', '/usr/local/bin/git', '/opt/homebrew/bin/git']
|
|
1800
|
+
|
|
1801
|
+
for (const candidate of candidates) {
|
|
1802
|
+
let resolved
|
|
1803
|
+
try {
|
|
1804
|
+
resolved = fs.realpathSync(candidate)
|
|
1805
|
+
fs.accessSync(resolved, fs.constants.X_OK)
|
|
1806
|
+
} catch {
|
|
1807
|
+
continue
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
return resolved
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Creates the minimal environment needed by read-only local git metadata commands.
|
|
1816
|
+
*
|
|
1817
|
+
* @param {string|undefined} gitExecutable trusted git executable
|
|
1818
|
+
* @param {NodeJS.ProcessEnv} sourceEnv source environment
|
|
1819
|
+
* @returns {NodeJS.ProcessEnv} credential-free git environment
|
|
1820
|
+
*/
|
|
1821
|
+
function getGitEnvironment (gitExecutable, sourceEnv) {
|
|
1822
|
+
const env = {
|
|
1823
|
+
GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
|
|
1824
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
1825
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
1826
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
for (const name of ['COMSPEC', 'ComSpec', 'PATHEXT', 'SystemRoot', 'WINDIR', 'windir']) {
|
|
1830
|
+
if (sourceEnv[name] !== undefined) env[name] = sourceEnv[name]
|
|
1831
|
+
}
|
|
1832
|
+
if (gitExecutable && path.isAbsolute(gitExecutable)) env.PATH = path.dirname(gitExecutable)
|
|
1833
|
+
return env
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Reads a JSON file.
|
|
1838
|
+
*
|
|
1839
|
+
* @param {string} filePath absolute file path
|
|
1840
|
+
* @returns {object|undefined} parsed JSON
|
|
1841
|
+
*/
|
|
1842
|
+
function readJsonFile (filePath) {
|
|
1843
|
+
try {
|
|
1844
|
+
return parseJson(fs.readFileSync(filePath, 'utf8'))
|
|
1845
|
+
} catch {
|
|
1846
|
+
// File is absent or unreadable.
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* Parses JSON safely.
|
|
1852
|
+
*
|
|
1853
|
+
* @param {string} content JSON content
|
|
1854
|
+
* @returns {object|undefined} parsed JSON
|
|
1855
|
+
*/
|
|
1856
|
+
function parseJson (content) {
|
|
1857
|
+
try {
|
|
1858
|
+
return JSON.parse(content)
|
|
1859
|
+
} catch {
|
|
1860
|
+
// Invalid JSON is reported by the checks that depend on it.
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Extracts a comparable semantic version from a package range.
|
|
1866
|
+
*
|
|
1867
|
+
* @param {string} rawVersion raw package version or range
|
|
1868
|
+
* @returns {string|undefined} comparable version
|
|
1869
|
+
*/
|
|
1870
|
+
function coerceVersion (rawVersion) {
|
|
1871
|
+
const aliasedVersion = rawVersion.match(/^npm:[^@]+@(.+)$/)?.[1] || rawVersion
|
|
1872
|
+
if (isAmbiguousRange(aliasedVersion)) return
|
|
1873
|
+
|
|
1874
|
+
const match = aliasedVersion.match(/\d+(?:\.\d+){0,2}/)
|
|
1875
|
+
if (!match) return
|
|
1876
|
+
|
|
1877
|
+
const parts = match[0].split('.')
|
|
1878
|
+
while (parts.length < 3) {
|
|
1879
|
+
parts.push('0')
|
|
1880
|
+
}
|
|
1881
|
+
return parts.slice(0, 3).join('.')
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/**
|
|
1885
|
+
* Checks whether a manifest dependency range cannot be represented by one comparable version.
|
|
1886
|
+
*
|
|
1887
|
+
* @param {string} rawVersion package manifest version/range
|
|
1888
|
+
* @returns {boolean} true when static diagnosis should ask the user to verify the range
|
|
1889
|
+
*/
|
|
1890
|
+
function isAmbiguousRange (rawVersion) {
|
|
1891
|
+
const version = String(rawVersion || '').trim()
|
|
1892
|
+
if (!version) return true
|
|
1893
|
+
if (version.includes('||')) return true
|
|
1894
|
+
return /<=?\s*\d/.test(version)
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* Checks whether a framework id is present.
|
|
1899
|
+
*
|
|
1900
|
+
* @param {Array<object>} frameworks detected frameworks
|
|
1901
|
+
* @param {string} id framework id
|
|
1902
|
+
* @returns {boolean} true if present
|
|
1903
|
+
*/
|
|
1904
|
+
function hasFramework (frameworks, id) {
|
|
1905
|
+
return frameworks.some(framework => framework.id === id)
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/**
|
|
1909
|
+
* Checks whether NODE_OPTIONS preloads dd-trace/ci/init.
|
|
1910
|
+
*
|
|
1911
|
+
* @param {string|undefined} nodeOptions NODE_OPTIONS value
|
|
1912
|
+
* @returns {boolean} true if dd-trace/ci/init is present
|
|
1913
|
+
*/
|
|
1914
|
+
function hasCiInitInNodeOptions (nodeOptions) {
|
|
1915
|
+
return !!nodeOptions && INIT_PRELOAD_RE.test(nodeOptions)
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/**
|
|
1919
|
+
* Checks whether NODE_OPTIONS preloads dd-trace/register.js.
|
|
1920
|
+
*
|
|
1921
|
+
* @param {string|undefined} nodeOptions NODE_OPTIONS value
|
|
1922
|
+
* @returns {boolean} true if dd-trace/register.js is present
|
|
1923
|
+
*/
|
|
1924
|
+
function hasRegisterInNodeOptions (nodeOptions) {
|
|
1925
|
+
return !!nodeOptions && REGISTER_PRELOAD_RE.test(nodeOptions)
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* Checks whether environment contains branch or tag metadata.
|
|
1930
|
+
*
|
|
1931
|
+
* @param {NodeJS.ProcessEnv} env environment
|
|
1932
|
+
* @returns {boolean} true if branch metadata exists
|
|
1933
|
+
*/
|
|
1934
|
+
function hasBranchMetadata (env) {
|
|
1935
|
+
return !!(
|
|
1936
|
+
env.DD_GIT_BRANCH ||
|
|
1937
|
+
env.DD_GIT_TAG ||
|
|
1938
|
+
env.GITHUB_HEAD_REF ||
|
|
1939
|
+
env.GITHUB_REF_NAME ||
|
|
1940
|
+
env.CI_COMMIT_REF_NAME ||
|
|
1941
|
+
env.CIRCLE_BRANCH ||
|
|
1942
|
+
env.GIT_BRANCH ||
|
|
1943
|
+
env.BUILDKITE_BRANCH ||
|
|
1944
|
+
env.TRAVIS_BRANCH ||
|
|
1945
|
+
env.BITBUCKET_BRANCH ||
|
|
1946
|
+
env.DRONE_BRANCH ||
|
|
1947
|
+
env.BUDDY_EXECUTION_BRANCH ||
|
|
1948
|
+
env.BITRISE_GIT_BRANCH
|
|
1949
|
+
)
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* Checks whether environment contains commit SHA metadata.
|
|
1954
|
+
*
|
|
1955
|
+
* @param {NodeJS.ProcessEnv} env environment
|
|
1956
|
+
* @returns {boolean} true if SHA metadata exists
|
|
1957
|
+
*/
|
|
1958
|
+
function hasShaMetadata (env) {
|
|
1959
|
+
return !!(
|
|
1960
|
+
env.DD_GIT_COMMIT_SHA ||
|
|
1961
|
+
env.GITHUB_SHA ||
|
|
1962
|
+
env.CI_COMMIT_SHA ||
|
|
1963
|
+
env.CIRCLE_SHA1 ||
|
|
1964
|
+
env.GIT_COMMIT ||
|
|
1965
|
+
env.BUILDKITE_COMMIT ||
|
|
1966
|
+
env.TRAVIS_COMMIT ||
|
|
1967
|
+
env.BITBUCKET_COMMIT ||
|
|
1968
|
+
env.DRONE_COMMIT ||
|
|
1969
|
+
env.BUDDY_EXECUTION_REVISION
|
|
1970
|
+
)
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Checks truthy string env values.
|
|
1975
|
+
*
|
|
1976
|
+
* @param {string|undefined} value value to inspect
|
|
1977
|
+
* @returns {boolean} true if value is true-like
|
|
1978
|
+
*/
|
|
1979
|
+
function isTrueLike (value) {
|
|
1980
|
+
return /^(?:1|true)$/i.test(String(value || ''))
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* Checks false-like string env values.
|
|
1985
|
+
*
|
|
1986
|
+
* @param {string|undefined} value value to inspect
|
|
1987
|
+
* @returns {boolean} true if value is false-like
|
|
1988
|
+
*/
|
|
1989
|
+
function isFalseLike (value) {
|
|
1990
|
+
return /^(?:0|false)$/i.test(String(value || ''))
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Formats a list of locations for text output.
|
|
1995
|
+
*
|
|
1996
|
+
* @param {string[]} locations relative paths
|
|
1997
|
+
* @returns {string} formatted locations
|
|
1998
|
+
*/
|
|
1999
|
+
function formatLocations (locations) {
|
|
2000
|
+
const uniqueLocations = unique(locations).slice(0, 5)
|
|
2001
|
+
const suffix = locations.length > uniqueLocations.length
|
|
2002
|
+
? `, and ${locations.length - uniqueLocations.length} more`
|
|
2003
|
+
: ''
|
|
2004
|
+
return uniqueLocations.join(', ') + suffix
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Serializes a supported framework for JSON output.
|
|
2009
|
+
*
|
|
2010
|
+
* @param {object} framework detected framework
|
|
2011
|
+
* @returns {object} serializable framework summary
|
|
2012
|
+
*/
|
|
2013
|
+
function serializeSupportedFramework (framework) {
|
|
2014
|
+
return {
|
|
2015
|
+
id: framework.id,
|
|
2016
|
+
name: framework.name,
|
|
2017
|
+
packages: framework.packages,
|
|
2018
|
+
supportedRange: framework.supportedRange,
|
|
2019
|
+
locations: framework.locations,
|
|
2020
|
+
versionDetections: framework.versionDetections,
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/**
|
|
2025
|
+
* Serializes an eligible framework for JSON output.
|
|
2026
|
+
*
|
|
2027
|
+
* @param {object} framework eligible framework
|
|
2028
|
+
* @returns {object} serializable eligible framework summary
|
|
2029
|
+
*/
|
|
2030
|
+
function serializeEligibleFramework (framework) {
|
|
2031
|
+
return {
|
|
2032
|
+
id: framework.id,
|
|
2033
|
+
name: framework.name,
|
|
2034
|
+
command: framework.eligibility.command,
|
|
2035
|
+
commandLocation: framework.eligibility.commandLocation,
|
|
2036
|
+
supportedRange: framework.supportedRange,
|
|
2037
|
+
version: framework.eligibility.version,
|
|
2038
|
+
versionLocation: framework.eligibility.versionLocation,
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Serializes an unsupported framework for JSON output.
|
|
2044
|
+
*
|
|
2045
|
+
* @param {object} framework detected unsupported framework
|
|
2046
|
+
* @returns {object} serializable framework summary
|
|
2047
|
+
*/
|
|
2048
|
+
function serializeUnsupportedFramework (framework) {
|
|
2049
|
+
return {
|
|
2050
|
+
id: framework.id,
|
|
2051
|
+
name: framework.name,
|
|
2052
|
+
locations: framework.locations,
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* Deduplicates values.
|
|
2058
|
+
*
|
|
2059
|
+
* @param {Array<string>} values values
|
|
2060
|
+
* @returns {string[]} unique values
|
|
2061
|
+
*/
|
|
2062
|
+
function unique (values) {
|
|
2063
|
+
return [...new Set(values.filter(Boolean))]
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* Deduplicates version detections.
|
|
2068
|
+
*
|
|
2069
|
+
* @param {Array<object>} detections version detections
|
|
2070
|
+
* @returns {Array<object>} unique detections
|
|
2071
|
+
*/
|
|
2072
|
+
function uniqueVersionDetections (detections) {
|
|
2073
|
+
const seen = new Set()
|
|
2074
|
+
const uniqueDetections = []
|
|
2075
|
+
|
|
2076
|
+
for (const detection of detections) {
|
|
2077
|
+
const key = `${detection.packageName}:${detection.rawVersion}:${detection.relativePath || ''}`
|
|
2078
|
+
if (seen.has(key)) continue
|
|
2079
|
+
|
|
2080
|
+
seen.add(key)
|
|
2081
|
+
uniqueDetections.push(detection)
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
return uniqueDetections
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* Normalizes relative paths to POSIX separators for stable output.
|
|
2089
|
+
*
|
|
2090
|
+
* @param {string} relativePath relative path
|
|
2091
|
+
* @returns {string} normalized path
|
|
2092
|
+
*/
|
|
2093
|
+
function normalizeRelativePath (relativePath) {
|
|
2094
|
+
return relativePath.split(path.sep).join('/')
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
module.exports = {
|
|
2098
|
+
getFrameworkDefinitions,
|
|
2099
|
+
runDiagnosis,
|
|
2100
|
+
}
|