dd-trace 5.116.0 → 5.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +3 -26
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +75 -13
  20. package/ci/test-optimization-validation/generated-test-contract.js +283 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/plan-writer.js +243 -984
  28. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  29. package/ci/test-optimization-validation/redaction.js +2 -1
  30. package/ci/test-optimization-validation/report-writer.js +391 -1357
  31. package/ci/test-optimization-validation/result-semantics.js +86 -0
  32. package/ci/test-optimization-validation/runner-command.js +249 -0
  33. package/ci/test-optimization-validation/runner-contract.js +664 -0
  34. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  35. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  36. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  37. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  38. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  39. package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
  40. package/ci/test-optimization-validation/source-text.js +87 -0
  41. package/ci/test-optimization-validation/test-output.js +12 -22
  42. package/ext/tags.d.ts +1 -0
  43. package/index.d.ts +7 -0
  44. package/package.json +3 -3
  45. package/packages/datadog-instrumentations/src/cucumber.js +11 -12
  46. package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
  47. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  48. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  49. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  50. package/packages/datadog-instrumentations/src/jest.js +18 -17
  51. package/packages/datadog-instrumentations/src/mocha/main.js +79 -30
  52. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  53. package/packages/datadog-instrumentations/src/playwright.js +3 -24
  54. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -1
  55. package/packages/datadog-instrumentations/src/vitest-main.js +14 -26
  56. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  57. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  58. package/packages/datadog-plugin-cucumber/src/index.js +2 -1
  59. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  60. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  61. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  62. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  63. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  64. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  65. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  66. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  67. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  68. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  69. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  70. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  71. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  72. package/packages/dd-trace/src/exporter.js +16 -9
  73. package/packages/dd-trace/src/llmobs/index.js +9 -1
  74. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  75. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  76. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  77. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  78. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  79. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  80. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  81. package/packages/dd-trace/src/llmobs/util.js +56 -3
  82. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  83. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  84. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  85. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  86. package/packages/dd-trace/src/plugins/index.js +1 -0
  87. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  88. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  89. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  90. package/packages/dd-trace/src/span_processor.js +1 -1
  91. package/packages/dd-trace/src/span_stats.js +22 -4
  92. package/ci/test-optimization-validation/command-suitability.js +0 -471
  93. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  94. package/ci/test-optimization-validation/init-probe.js +0 -246
  95. package/ci/test-optimization-validation/late-initialization.js +0 -63
  96. package/ci/test-optimization-validation/local-command.js +0 -163
  97. package/ci/test-optimization-validation/setup-runner.js +0 -97
  98. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -1,75 +1,71 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- getCommandDetails,
5
- serializeDisplayCommand,
6
- } = require('./command-runner')
7
3
  const { sanitizeEnv } = require('./redaction')
8
4
 
9
5
  /**
10
- * Builds the normalized CI command metadata shape shared by reports and UI payloads.
6
+ * Builds the normalized static CI configuration metadata shared by reports.
11
7
  *
12
8
  * @param {object} framework manifest framework entry
13
- * @returns {object|undefined} CI command candidate context when available
9
+ * @returns {object|undefined} CI configuration context when available
14
10
  */
15
11
  function buildCiCommandCandidate (framework) {
16
12
  const ciWiring = framework.ciWiring || {}
17
- const command = framework.ciWiringCommand
18
-
19
- if (!command && !hasCiWiringContext(ciWiring)) return
13
+ if (Object.keys(ciWiring).length === 0) return
20
14
 
21
15
  return removeUndefined({
22
- provider: ciWiring.provider || undefined,
23
- configFile: ciWiring.configFile || undefined,
24
- workflow: ciWiring.workflow || undefined,
25
- job: ciWiring.job || undefined,
26
- step: ciWiring.step || undefined,
27
- runner: ciWiring.runner || undefined,
28
- shell: ciWiring.shell || undefined,
29
- command: command ? serializeDisplayCommand(command) : ciWiring.command,
30
- cwd: command?.cwd || ciWiring.workingDirectory,
16
+ provider: ciWiring.provider,
17
+ configFile: ciWiring.configFile,
18
+ workflow: ciWiring.workflow,
19
+ job: ciWiring.job,
20
+ step: ciWiring.step,
21
+ runner: ciWiring.runner,
22
+ shell: ciWiring.shell,
23
+ command: typeof ciWiring.command === 'string' ? ciWiring.command : undefined,
24
+ cwd: ciWiring.workingDirectory,
25
+ terminalTestCommand: ciWiring.terminalTestCommand,
31
26
  whySelected: ciWiring.whySelected || ciWiring.selectionReason || ciWiring.diagnosis,
32
- replayability: ciWiring.replayability,
33
- replayBlocker: ciWiring.replayBlocker,
34
27
  initialization: ciWiring.initialization,
35
- env: buildCiEnvSummary(ciWiring, command),
28
+ transport: ciWiring.transport,
29
+ env: buildCiEnvSummary(ciWiring),
36
30
  packageScriptExpansionChain: getFirstArray(
37
31
  ciWiring.packageScriptExpansionChain,
38
32
  ciWiring.scriptExpansionChain,
39
33
  ciWiring.commandExpansion
40
34
  ),
41
35
  runnerToolChain: getFirstArray(
36
+ ciWiring.wrapperChain,
42
37
  ciWiring.runnerToolChain,
43
38
  ciWiring.toolChain,
44
39
  ciWiring.commandChain
45
40
  ),
46
41
  setupCommandIds: ciWiring.setupCommandIds,
47
42
  unresolved: ciWiring.unresolved,
48
- commandDetails: command && getCommandDetails(command),
49
43
  })
50
44
  }
51
45
 
52
- function buildCiEnvSummary (ciWiring, command) {
46
+ function buildCiEnvSummary (ciWiring) {
53
47
  const summary = removeUndefined({
54
48
  workflow: sanitizeEnv(ciWiring.workflowEnv || ciWiring.env?.workflow),
55
49
  job: sanitizeEnv(ciWiring.jobEnv || ciWiring.env?.job),
56
- step: sanitizeEnv(ciWiring.stepEnv || command?.env || ciWiring.env?.step),
50
+ step: sanitizeEnv(ciWiring.stepEnv || ciWiring.env?.step),
57
51
  inherited: sanitizeEnv(ciWiring.inheritedEnv),
58
52
  })
59
53
 
60
54
  return Object.keys(summary).length > 0 ? summary : undefined
61
55
  }
62
56
 
63
- function hasCiWiringContext (ciWiring) {
64
- return Object.keys(ciWiring).length > 0
65
- }
66
-
67
57
  function getFirstArray (...values) {
68
58
  for (const value of values) {
69
- if (Array.isArray(value) && value.length > 0) return value
59
+ if (Array.isArray(value) && value.length > 0) return value.map(formatChainEntry)
70
60
  }
71
61
  }
72
62
 
63
+ function formatChainEntry (entry) {
64
+ if (typeof entry === 'string') return entry
65
+ if (!entry || typeof entry !== 'object') return String(entry)
66
+ return entry.source ? `${entry.source}: ${entry.command}` : entry.command
67
+ }
68
+
73
69
  function removeUndefined (object) {
74
70
  const result = {}
75
71
  for (const [key, value] of Object.entries(object)) {
@@ -78,6 +74,4 @@ function removeUndefined (object) {
78
74
  return result
79
75
  }
80
76
 
81
- module.exports = {
82
- buildCiCommandCandidate,
83
- }
77
+ module.exports = { buildCiCommandCandidate }
@@ -86,7 +86,7 @@ function getFrameworkCiDiscoveryContradiction (framework, manifest) {
86
86
  reason: 'CI workflow files were found by validator static diagnosis, but this manifest entry says no CI ' +
87
87
  'workflow was found. The manifest cannot support a "no CI workflow found" conclusion.',
88
88
  recommendation: 'Inspect the discovered CI files with hidden-directory-aware discovery, then update ciWiring, ' +
89
- 'ciWiringCommand, omittedTestCommands, notes, or unresolved blockers before rerunning live validation.',
89
+ 'ciWiring configuration evidence, omittedTestCommands, notes, or unresolved blockers before validation.',
90
90
  ciDiscovery,
91
91
  }
92
92
  }
@@ -0,0 +1,141 @@
1
+ 'use strict'
2
+
3
+ const { parseLiteralEnvironmentPrefix } = require('./literal-environment')
4
+
5
+ const MAX_SCRIPT_EXPANSIONS = 16
6
+ const DYNAMIC_VALUE_PATTERN = /[$`]|\r|\n|%[^%\s]+%|![^!\s]+!/
7
+
8
+ /**
9
+ * Expands bounded local package-script references without executing them.
10
+ *
11
+ * @param {string} command selected CI command
12
+ * @param {Record<string, string>} scripts package.json scripts
13
+ * @returns {{error?: string, lifecycleScripts: string[], terminals: object[]}} expansion
14
+ */
15
+ function expandLocalPackageScripts (command, scripts) {
16
+ const state = { count: 0, lifecycleScripts: new Set() }
17
+ const result = expandCommand(command, scripts || {}, [], new Set(), state)
18
+ return {
19
+ ...(result.error ? { error: result.error } : {}),
20
+ lifecycleScripts: [...state.lifecycleScripts],
21
+ terminals: result.terminals || [],
22
+ }
23
+ }
24
+
25
+ function expandCommand (command, scripts, path, stack, state) {
26
+ if (++state.count > MAX_SCRIPT_EXPANSIONS) {
27
+ return { error: `local package-script expansion exceeded ${MAX_SCRIPT_EXPANSIONS} commands` }
28
+ }
29
+ const segments = splitLiteralAndChain(command)
30
+ if (!segments) return { error: 'the command contains unsupported dynamic shell syntax' }
31
+
32
+ const terminals = []
33
+ for (const segment of segments) {
34
+ if (hasStatefulShellSemantics(segment)) {
35
+ return { error: 'the command contains unsupported stateful shell semantics' }
36
+ }
37
+ const invocation = getPackageScriptInvocation(segment)
38
+ if (!invocation) {
39
+ terminals.push({ command: segment, path: [...path, segment] })
40
+ continue
41
+ }
42
+ if (typeof scripts[invocation.script] !== 'string') {
43
+ return { error: `local package script ${invocation.script} is unavailable` }
44
+ }
45
+ if (stack.has(invocation.script)) {
46
+ return { error: `local package-script cycle includes ${invocation.script}` }
47
+ }
48
+ const lifecycleNames = invocation.manager === 'npm'
49
+ ? [`pre${invocation.script}`, `post${invocation.script}`]
50
+ .filter(name => typeof scripts[name] === 'string')
51
+ : []
52
+ for (const lifecycle of lifecycleNames) state.lifecycleScripts.add(lifecycle)
53
+
54
+ for (const scriptName of [
55
+ ...(lifecycleNames.includes(`pre${invocation.script}`) ? [`pre${invocation.script}`] : []),
56
+ invocation.script,
57
+ ...(lifecycleNames.includes(`post${invocation.script}`) ? [`post${invocation.script}`] : []),
58
+ ]) {
59
+ if (stack.has(scriptName)) return { error: `local package-script cycle includes ${scriptName}` }
60
+ const nextStack = new Set(stack)
61
+ nextStack.add(invocation.script)
62
+ nextStack.add(scriptName)
63
+ const expanded = expandCommand(
64
+ scripts[scriptName],
65
+ scripts,
66
+ [...path, segment],
67
+ nextStack,
68
+ state
69
+ )
70
+ if (expanded.error) return expanded
71
+ terminals.push(...expanded.terminals)
72
+ }
73
+ }
74
+ return { terminals }
75
+ }
76
+
77
+ /**
78
+ * Splits only literal `&&` chains and rejects other shell control flow or expansion.
79
+ *
80
+ * @param {string} command command text
81
+ * @returns {string[]|undefined} literal command segments
82
+ */
83
+ function splitLiteralAndChain (command) {
84
+ const source = String(command || '').trim()
85
+ if (!source || DYNAMIC_VALUE_PATTERN.test(source)) return
86
+ const segments = []
87
+ let quote
88
+ let start = 0
89
+
90
+ for (let index = 0; index < source.length; index++) {
91
+ const character = source[index]
92
+ if (character === '\\') {
93
+ index++
94
+ continue
95
+ }
96
+ if (quote) {
97
+ if (character === quote) quote = undefined
98
+ continue
99
+ }
100
+ if (character === '"' || character === "'") {
101
+ quote = character
102
+ continue
103
+ }
104
+ if (character === '&' && source[index + 1] === '&') {
105
+ const segment = source.slice(start, index).trim()
106
+ if (!segment) return
107
+ segments.push(segment)
108
+ start = ++index + 1
109
+ continue
110
+ }
111
+ if (';&|()'.includes(character)) return
112
+ }
113
+ if (quote) return
114
+ const finalSegment = source.slice(start).trim()
115
+ if (!finalSegment) return
116
+ segments.push(finalSegment)
117
+ return segments
118
+ }
119
+
120
+ function hasStatefulShellSemantics (command) {
121
+ const prefix = parseLiteralEnvironmentPrefix(command)
122
+ const executable = command.slice(prefix.length).trim()
123
+ return !executable ||
124
+ /^(?:cd|eval|exec|export|popd|pushd|set|source|unset)\b/.test(executable) ||
125
+ /^\.(?:\s|$)/.test(executable)
126
+ }
127
+
128
+ function getPackageScriptInvocation (command) {
129
+ const literalPrefix = parseLiteralEnvironmentPrefix(command)
130
+ const source = command.slice(literalPrefix.length).trim()
131
+ .replace(/^(?:c8|nyc)(?:\.cmd)?\s+/, '')
132
+ const match = /^(npm(?:\.cmd)?|pnpm(?:\.cmd)?|yarn(?:pkg)?(?:\.cmd)?)\s+(?:(run|run-script)\s+)?([\w:-]+)$/
133
+ .exec(source)
134
+ if (!match) return
135
+
136
+ const manager = match[1].replace(/\.cmd$/i, '')
137
+ if (manager === 'npm' && !match[2] && !['restart', 'start', 'stop', 'test'].includes(match[3])) return
138
+ return { manager, script: match[3] }
139
+ }
140
+
141
+ module.exports = { expandLocalPackageScripts }
@@ -2,7 +2,9 @@
2
2
 
3
3
  const path = require('node:path')
4
4
 
5
- const { serializeDisplayCommand } = require('./command-runner')
5
+ const { findEnvironmentEntry } = require('./environment')
6
+ const { splitNodeOptions } = require('./executable')
7
+ const { removeEmptyLiteralEnvironmentAssignments } = require('./literal-environment')
6
8
 
7
9
  const GITHUB_API_KEY_REFERENCE = '$' + '{{ secrets.DD_API_KEY }}'
8
10
  const AGENTLESS_ENV = {
@@ -31,7 +33,7 @@ function buildCiRemediation (framework) {
31
33
  const location = getCiLocation(ciWiring)
32
34
  const nodeOptions = getNodeOptions(framework)
33
35
  const recommendedValues = getRecommendedValues(framework)
34
- const variants = getVariants(transport, ciWiring, framework.ciWiringCommand, recommendedValues, nodeOptions)
36
+ const variants = getVariants(transport, ciWiring, recommendedValues, nodeOptions)
35
37
 
36
38
  return {
37
39
  provider: ciWiring.provider || 'unknown',
@@ -47,25 +49,8 @@ function buildCiRemediation (framework) {
47
49
  }
48
50
 
49
51
  function getConfiguredTransport (framework) {
50
- const env = collectCiEnv(framework)
51
- if (isTrue(env.DD_CIVISIBILITY_AGENTLESS_ENABLED)) return 'agentless'
52
- if (env.DD_AGENT_HOST || env.DD_TRACE_AGENT_URL || env.DD_TRACE_AGENT_HOSTNAME) return 'agent'
53
- return 'unknown'
54
- }
55
-
56
- function collectCiEnv (framework) {
57
- const ciWiring = framework.ciWiring || {}
58
- return {
59
- ...ciWiring.workflowEnv,
60
- ...ciWiring.jobEnv,
61
- ...ciWiring.stepEnv,
62
- ...ciWiring.inheritedEnv,
63
- ...framework.ciWiringCommand?.env,
64
- }
65
- }
66
-
67
- function isTrue (value) {
68
- return ['1', 'true'].includes(String(value || '').toLowerCase())
52
+ const mode = framework.ciWiring?.transport?.mode
53
+ return ['agentless', 'agent', 'none'].includes(mode) ? mode : 'unknown'
69
54
  }
70
55
 
71
56
  function getCiLocation (ciWiring) {
@@ -106,12 +91,12 @@ function getAgentAlternative () {
106
91
  'DD_CIVISIBILITY_AGENTLESS_ENABLED.'
107
92
  }
108
93
 
109
- function getVariants (transport, ciWiring, command, recommendedValues, nodeOptions) {
110
- if (transport === 'agent') return [getVariant('agent', ciWiring, command, recommendedValues, nodeOptions)]
111
- return [getVariant('agentless', ciWiring, command, recommendedValues, nodeOptions)]
94
+ function getVariants (transport, ciWiring, recommendedValues, nodeOptions) {
95
+ if (transport === 'agent') return [getVariant('agent', ciWiring, recommendedValues, nodeOptions)]
96
+ return [getVariant('agentless', ciWiring, recommendedValues, nodeOptions)]
112
97
  }
113
98
 
114
- function getVariant (transport, ciWiring, command, recommendedValues, nodeOptions) {
99
+ function getVariant (transport, ciWiring, recommendedValues, nodeOptions) {
115
100
  const transportEnv = transport === 'agentless' ? AGENTLESS_ENV : {}
116
101
  const requiredEnv = { NODE_OPTIONS: nodeOptions, ...transportEnv }
117
102
  const recommendedEnv = Object.fromEntries(recommendedValues.map(({ name, value }) => [name, value]))
@@ -128,13 +113,77 @@ function getVariant (transport, ciWiring, command, recommendedValues, nodeOption
128
113
  })),
129
114
  recommendedValues,
130
115
  optionalValues: OPTIONAL_VALUES[transport],
131
- snippet: formatSnippet({ ...requiredEnv, ...recommendedEnv }, ciWiring, command),
116
+ snippet: formatSnippet({ ...requiredEnv, ...recommendedEnv }, ciWiring),
132
117
  }
133
118
  }
134
119
 
135
120
  function getNodeOptions (framework) {
136
- if (framework.framework === 'vitest') return '--import dd-trace/register.js -r dd-trace/ci/init'
137
- return '-r dd-trace/ci/init'
121
+ const existing = getEffectiveNodeOptions(framework.ciWiring)
122
+ const options = existing ? [existing] : []
123
+ if (framework.framework === 'vitest' &&
124
+ !hasNodeModuleOption(existing, ['--import'], isDatadogRegisterSpecifier)) {
125
+ options.push('--import dd-trace/register.js')
126
+ }
127
+ if (!hasNodeModuleOption(existing, ['-r', '--require'], isDatadogCiInitSpecifier)) {
128
+ options.push('-r dd-trace/ci/init')
129
+ }
130
+ return options.join(' ')
131
+ }
132
+
133
+ function getEffectiveNodeOptions (ciWiring = {}) {
134
+ const platform = getCiPlatform(ciWiring)
135
+ let value
136
+ for (const field of ['inheritedEnv', 'workflowEnv', 'jobEnv', 'stepEnv']) {
137
+ const entry = findEnvironmentEntry(ciWiring[field], 'NODE_OPTIONS', platform)
138
+ if (entry) value = entry[1]
139
+ }
140
+ if (typeof value !== 'string') return ''
141
+ const literal = value.trim()
142
+ return hasDynamicEnvironmentReference(literal) ? '' : literal
143
+ }
144
+
145
+ function hasNodeModuleOption (value, optionNames, matchesSpecifier) {
146
+ if (!value) return false
147
+ let args
148
+ try {
149
+ args = splitNodeOptions(value)
150
+ } catch {
151
+ return false
152
+ }
153
+ for (let index = 0; index < args.length; index++) {
154
+ const argument = args[index]
155
+ let specifier
156
+ if (optionNames.includes(argument)) {
157
+ specifier = args[++index]
158
+ } else {
159
+ for (const optionName of optionNames) {
160
+ const prefix = `${optionName}=`
161
+ if (argument.startsWith(prefix)) specifier = argument.slice(prefix.length)
162
+ }
163
+ }
164
+ if (matchesSpecifier(specifier)) return true
165
+ }
166
+ return false
167
+ }
168
+
169
+ function isDatadogCiInitSpecifier (specifier) {
170
+ return isDatadogModuleSpecifier(specifier, 'ci/init')
171
+ }
172
+
173
+ function isDatadogRegisterSpecifier (specifier) {
174
+ return isDatadogModuleSpecifier(specifier, 'register')
175
+ }
176
+
177
+ function isDatadogModuleSpecifier (specifier, entrypoint) {
178
+ if (typeof specifier !== 'string') return false
179
+ const normalized = specifier.replaceAll('\\', '/')
180
+ return [`dd-trace/${entrypoint}`, `dd-trace/${entrypoint}.js`].some(candidate => {
181
+ return normalized === candidate || normalized.endsWith(`/${candidate}`)
182
+ })
183
+ }
184
+
185
+ function hasDynamicEnvironmentReference (value) {
186
+ return /(?:\$[A-Za-z_{]|\$\(|%[A-Za-z_][A-Za-z0-9_]*%)/.test(value)
138
187
  }
139
188
 
140
189
  function getRecommendedValues (framework) {
@@ -142,8 +191,7 @@ function getRecommendedValues (framework) {
142
191
  const context = [
143
192
  framework.ciWiring?.step,
144
193
  framework.ciWiring?.job,
145
- framework.existingTestCommand?.description,
146
- framework.ciWiringCommand?.description,
194
+ framework.ciWiring?.command,
147
195
  ].filter(Boolean).join(' ')
148
196
  const testKind = /\bunit\b/i.test(context)
149
197
  ? 'unit-tests'
@@ -172,17 +220,27 @@ function normalizeName (value) {
172
220
  .replaceAll(/^-|-$/g, '') || 'test'
173
221
  }
174
222
 
175
- function formatSnippet (env, ciWiring, command) {
223
+ function formatSnippet (env, ciWiring) {
176
224
  if (ciWiring.provider === 'github-actions') {
177
- const testCommand = getTestCommand(ciWiring, command)
178
- return [
225
+ const testCommand = getTestCommand(ciWiring)
226
+ const lines = [
179
227
  ciWiring.configFile ? `# ${formatPath(ciWiring.configFile)}` : '# GitHub Actions workflow',
180
228
  ciWiring.job ? `# Job: ${ciWiring.job}` : '# Selected test job',
229
+ ]
230
+ if (!testCommand) {
231
+ return [
232
+ ...lines,
233
+ 'env:',
234
+ ...Object.entries(env).map(([name, value]) => ` ${name}: ${quoteYamlValue(value)}`),
235
+ ].join('\n')
236
+ }
237
+ return [
238
+ ...lines,
181
239
  `- name: ${quoteYamlValue(ciWiring.step || 'Run tests with Datadog')}`,
182
240
  ' env:',
183
241
  ...Object.entries(env).map(([name, value]) => ` ${name}: ${quoteYamlValue(value)}`),
184
242
  ' run: |',
185
- ...String(testCommand).split(/\r?\n/).map(line => ` ${line}`),
243
+ ...testCommand.split(/\r?\n/).map(line => ` ${line}`),
186
244
  ].join('\n')
187
245
  }
188
246
 
@@ -196,10 +254,22 @@ function quoteShellValue (value) {
196
254
  return JSON.stringify(String(value))
197
255
  }
198
256
 
199
- function getTestCommand (ciWiring, command) {
200
- if (command) return serializeDisplayCommand(command)
201
- return ciWiring.packageScriptExpansionChain?.[0] || ciWiring.runnerToolChain?.[0] ||
202
- '# keep the existing test command here'
257
+ function getTestCommand (ciWiring) {
258
+ if (typeof ciWiring.command === 'string' && ciWiring.command.trim()) {
259
+ return removeEmptyLiteralEnvironmentAssignments(
260
+ ciWiring.command,
261
+ 'NODE_OPTIONS',
262
+ getCiPlatform(ciWiring)
263
+ )
264
+ }
265
+ return ciWiring.packageScriptExpansionChain?.[0] || ciWiring.runnerToolChain?.[0]
266
+ }
267
+
268
+ function getCiPlatform (ciWiring) {
269
+ const shellName = path.basename(String(ciWiring.shell || '')).toLowerCase()
270
+ return ['cmd', 'cmd.exe', 'powershell', 'powershell.exe', 'pwsh', 'pwsh.exe'].includes(shellName)
271
+ ? 'win32'
272
+ : process.platform
203
273
  }
204
274
 
205
275
  function quoteYamlValue (value) {
@@ -207,4 +277,7 @@ function quoteYamlValue (value) {
207
277
  return JSON.stringify(String(value))
208
278
  }
209
279
 
210
- module.exports = { buildCiRemediation }
280
+ module.exports = {
281
+ buildCiRemediation,
282
+ getConfiguredTransport,
283
+ }