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,657 +1,354 @@
1
1
  'use strict'
2
2
 
3
- const fs = require('fs')
4
- const path = require('path')
5
-
3
+ const { getCommandBlocker } = require('../command-blocker')
6
4
  const { runCommand } = require('../command-runner')
7
- const { getDatadogCleanCommand, getLocalValidationCommand } = require('../local-command')
5
+ const { eventsOfType } = require('../payload-normalizer')
6
+ const { getBasicCommand } = require('../runner-command')
8
7
 
9
8
  const {
10
9
  basicEventEvidence,
11
- error,
10
+ error: scenarioError,
12
11
  failWithDebugRerun,
13
- findInterestingLines,
14
12
  frameworkOutDir,
15
13
  hasAllBasicEventTypes,
16
14
  inconclusive,
17
15
  pass,
18
16
  runInstrumentedCommand,
19
- tailInterestingLines,
20
17
  } = require('./helpers')
21
18
 
19
+ /**
20
+ * Checks whether controlled initialization reports one real project test.
21
+ *
22
+ * @param {object} input scenario inputs
23
+ * @param {object} input.framework framework manifest entry
24
+ * @param {string} input.out validation output directory
25
+ * @param {object} input.options validator options
26
+ * @returns {Promise<object>} scenario result
27
+ */
22
28
  async function runBasicReporting ({ framework, out, options }) {
23
29
  const scenarioName = 'basic-reporting'
30
+ const command = getBasicCommand(framework)
31
+ let outDir
32
+
24
33
  try {
25
- const command = getBasicReportingCommand(framework)
26
- const { result, events, offline, outDir } = await runInstrumentedCommand({
34
+ const run = await runInstrumentedCommand({
35
+ allowMissingInitialization: true,
36
+ command,
27
37
  framework,
38
+ options,
28
39
  out,
29
40
  scenarioName,
30
- command,
31
- options,
32
- allowMissingInitialization: true,
33
41
  })
34
-
42
+ outDir = run.outDir
43
+ const complete = hasAllBasicEventTypes(run.events)
44
+ const selector = getSelectorEvidence(framework, run.events)
35
45
  const evidence = {
36
- commandExitCode: result.exitCode,
37
- commandTimedOut: result.timedOut,
38
- commandDescription: command?.description,
39
- commandOutputSummary: summarizeTestOutput(result.stdout, result.stderr),
40
- manifestNotes: Array.isArray(framework.notes) ? framework.notes : [],
46
+ ...basicEventEvidence(run.events),
47
+ commandExitCode: run.result.exitCode,
48
+ commandOutputSummary: summarizeTestOutput(run.result.stdout, run.result.stderr),
49
+ commandTimedOut: run.result.timedOut,
50
+ foundationalReportingEstablished: complete &&
51
+ run.offline.initialized &&
52
+ run.offline.inputs.settings?.status === 'loaded' &&
53
+ run.result.exitCode === 0 &&
54
+ !run.result.timedOut,
55
+ offlineExporterInitialized: run.offline.initialized,
41
56
  preflight: summarizePreflight(framework.preflight),
42
- offlineExporterInitialized: offline.initialized,
43
- settingsLoadedFromCache: offline.inputs.settings?.status === 'loaded',
44
- offlineExporterSummary: offline.summary,
45
- ...basicEventEvidence(events),
57
+ reportingPath: complete ? 'validator-direct-runner' : 'none',
58
+ selector,
59
+ settingsLoadedFromCache: run.offline.inputs.settings?.status === 'loaded',
46
60
  }
61
+ evidence.foundationalReportingEstablished = evidence.foundationalReportingEstablished && selector.verified
47
62
 
48
- if (evidence.offlineExporterInitialized && !evidence.settingsLoadedFromCache) {
49
- return error(
63
+ if (run.result.timedOut) {
64
+ return inconclusive(
50
65
  framework,
51
66
  scenarioName,
52
- new Error('Basic Reporting settings were not loaded from the authoritative offline cache fixture.'),
67
+ 'The initialized direct test exceeded its approved timeout. Basic Reporting remains incomplete.',
68
+ { ...evidence, commandFailure: classifyFailure(framework, run.result) },
53
69
  outDir
54
70
  )
55
71
  }
56
72
 
57
- if (!hasAllBasicEventTypes(events)) {
58
- if (result.exitCode !== 0) {
59
- evidence.commandFailure = summarizeCommandFailure(result, evidence)
60
- }
61
- const eventLevelFailure = getMissingEventDiagnosis({ framework, result, evidence })
62
- evidence.eventLevelFailure = eventLevelFailure
63
-
64
- return failBasicReportingWithDebugRerun({
65
- command,
66
- diagnosis: eventLevelFailure.summary,
67
- evidence,
73
+ if (!selector.verified) {
74
+ return inconclusive(
68
75
  framework,
69
- options,
70
- out,
71
- outDir,
72
76
  scenarioName,
73
- skipDebug: evidence.commandFailure?.buildErrors?.length > 0 ||
74
- !shouldRunDebugRerun(eventLevelFailure, result),
75
- })
77
+ 'The repository test wrapper ran, but captured events did not prove that it honored the approved ' +
78
+ 'representative test file. Basic Reporting remains incomplete.',
79
+ {
80
+ ...evidence,
81
+ recommendation: 'Use a repository wrapper that forwards the selected test file, or use a direct ' +
82
+ 'framework runner whose selector can be bounded by the validator.',
83
+ },
84
+ outDir
85
+ )
76
86
  }
77
87
 
78
- if (result.exitCode === 0) {
88
+ if (evidence.foundationalReportingEstablished) {
79
89
  return pass(
80
90
  framework,
81
91
  scenarioName,
82
- 'Basic reporting emitted session, module, suite, and test events.',
92
+ 'The direct test emitted session, module, suite, and test events.',
83
93
  evidence,
84
94
  outDir
85
95
  )
86
96
  }
87
97
 
88
- if (matchesPreflightExitCode(framework.preflight, result.exitCode)) {
89
- evidence.commandFailure = summarizeCommandFailure(result, evidence)
90
- evidence.commandExitMatchesPreflight = true
91
- return pass(
98
+ if (framework.preflight?.observedTestCount === null && evidence.testEvents === 0) {
99
+ return inconclusive(
92
100
  framework,
93
101
  scenarioName,
94
- 'Basic reporting emitted session, module, suite, and test events. ' +
95
- `The command exited ${result.exitCode}, matching the dd-trace-less preflight run.`,
96
- evidence,
102
+ 'The clean runner exited successfully without a parseable test count, and the initialized run emitted no ' +
103
+ 'test event. Basic Reporting remains incomplete because the validator cannot prove that a test executed.',
104
+ {
105
+ ...evidence,
106
+ recommendation: 'Use a representative test command that reports an executed-test count, or inspect the ' +
107
+ 'runner output and debug artifact before attributing the missing event to dd-trace.',
108
+ },
97
109
  outDir
98
110
  )
99
111
  }
100
112
 
101
- evidence.commandFailure = summarizeCommandFailure(result, evidence)
102
- evidence.commandExitMatchesPreflight = false
103
- const cleanConfirmation = await runCleanConfirmation({ command, framework, options, out, scenarioName })
104
- evidence.cleanConfirmation = cleanConfirmation.evidence
105
-
106
- if (!cleanConfirmation.evidence.exitMatchesPreflight) {
107
- return inconclusive(
113
+ if (!run.offline.initialized || !evidence.settingsLoadedFromCache) {
114
+ return failWithDebugRerun({
115
+ command,
116
+ diagnosis: run.offline.initialized
117
+ ? 'The clean test passed, but Test Optimization did not load the validator-owned offline settings.'
118
+ : 'The clean test passed, but the offline Test Optimization exporter did not initialize.',
119
+ evidence: {
120
+ ...evidence,
121
+ possibleLibraryBug: true,
122
+ recommendation: 'Inspect the debug artifact for dd-trace initialization errors and report it with the ' +
123
+ 'framework and dd-trace versions.',
124
+ },
108
125
  framework,
126
+ options,
127
+ out,
128
+ outDir,
109
129
  scenarioName,
110
- getUnstableBaselineDiagnosis(framework.preflight, result, cleanConfirmation.result),
111
- evidence,
130
+ })
131
+ }
132
+
133
+ if (run.result.exitCode !== 0) {
134
+ evidence.commandFailure = classifyFailure(framework, run.result)
135
+ const confirmation = await runCleanConfirmation({ command, framework, options, out })
136
+ evidence.cleanConfirmation = confirmation.evidence
137
+ if (!confirmation.evidence.matchesPreflight) {
138
+ return inconclusive(
139
+ framework,
140
+ scenarioName,
141
+ 'The clean baseline changed between runs, so the initialized failure cannot be attributed to dd-trace.',
142
+ evidence,
143
+ outDir,
144
+ confirmation.artifacts
145
+ )
146
+ }
147
+ const result = await failWithDebugRerun({
148
+ command,
149
+ diagnosis: complete
150
+ ? `The test passed twice without Datadog but exited ${run.result.exitCode} when initialized, after ` +
151
+ 'emitting ' +
152
+ 'the complete event hierarchy. This is a possible dd-trace compatibility bug.'
153
+ : `The test passed twice without Datadog but exited ${run.result.exitCode} when initialized and did not ` +
154
+ 'emit the complete event hierarchy. This is a possible dd-trace adapter or compatibility bug.',
155
+ evidence: {
156
+ ...evidence,
157
+ possibleLibraryBug: true,
158
+ recommendation: 'Attach the debug run, clean confirmations, framework version, and dd-trace version to ' +
159
+ 'the engineering investigation.',
160
+ },
161
+ framework,
162
+ options,
163
+ out,
112
164
  outDir,
113
- cleanConfirmation.artifacts
114
- )
165
+ scenarioName,
166
+ })
167
+ result.artifacts.push(...confirmation.artifacts)
168
+ return result
115
169
  }
116
170
 
117
- const failure = await failBasicReportingWithDebugRerun({
171
+ const missing = getMissingLevels(evidence)
172
+ return failWithDebugRerun({
118
173
  command,
119
- diagnosis: getPossibleCompatibilityDiagnosis(framework.preflight, result),
120
- evidence,
174
+ diagnosis: 'The direct test passed cleanly and while initialized, but no complete Test Optimization event ' +
175
+ `hierarchy was captured. Missing levels: ${missing.join(', ')}. This is a possible dd-trace adapter bug.`,
176
+ evidence: {
177
+ ...evidence,
178
+ missingEventLevels: missing,
179
+ possibleLibraryBug: true,
180
+ recommendation: 'Inspect the debug artifact for initialization and adapter errors. Include it with the ' +
181
+ 'framework and dd-trace versions in an engineering investigation.',
182
+ },
121
183
  framework,
122
184
  options,
123
185
  out,
124
186
  outDir,
125
187
  scenarioName,
126
188
  })
127
- failure.artifacts.push(...cleanConfirmation.artifacts)
189
+ } catch (error) {
190
+ const failure = scenarioError(framework, scenarioName, error, outDir || error?.artifactDirectory)
191
+ failure.diagnosis = `Basic Reporting could not complete: ${error?.message || error}`
192
+ failure.evidence.validationIncomplete = true
128
193
  return failure
129
- } catch (err) {
130
- return error(framework, scenarioName, err)
131
194
  }
132
195
  }
133
196
 
134
197
  /**
135
- * Repeats the selected command without Datadog after an instrumented exit-code mismatch.
198
+ * Repeats the direct clean command after an initialized-only failure.
136
199
  *
137
200
  * @param {object} input confirmation inputs
138
- * @param {object} input.command selected project command
139
- * @param {object} input.framework manifest framework entry
201
+ * @param {object} input.command direct command
202
+ * @param {object} input.framework framework entry
140
203
  * @param {object} input.options validator options
141
- * @param {string} input.out validation output directory
142
- * @param {string} input.scenarioName scenario identifier
143
- * @returns {Promise<{artifacts: string[], evidence: object, result: object}>} clean confirmation outcome
204
+ * @param {string} input.out output root
205
+ * @returns {Promise<{artifacts: string[], evidence: object}>} confirmation evidence
144
206
  */
145
- async function runCleanConfirmation ({ command, framework, options, out, scenarioName }) {
146
- const outDir = frameworkOutDir(out, framework, `${scenarioName}-clean-confirmation`)
147
- const result = await runCommand(getDatadogCleanCommand(command), {
207
+ async function runCleanConfirmation ({ command, framework, options, out }) {
208
+ const outDir = frameworkOutDir(out, framework, 'basic-reporting-clean-confirmation')
209
+ const result = await runCommand(command, {
148
210
  artifactRoot: out,
149
211
  envMode: 'clean',
150
- label: `${framework.id}:${scenarioName}:clean-confirmation`,
212
+ label: `${framework.id}:basic-reporting:clean-confirmation`,
151
213
  outDir,
152
214
  repositoryRoot: options.repositoryRoot,
153
215
  requireExecutableApproval: options.requireExecutableApproval,
154
216
  verbose: options.verbose,
155
217
  })
156
- const exitMatchesPreflight = !result.timedOut && matchesPreflightExitCode(framework.preflight, result.exitCode)
157
218
  return {
158
219
  artifacts: Object.values(result.artifacts),
159
- result,
160
220
  evidence: {
161
- ran: true,
162
221
  exitCode: result.exitCode,
222
+ matchesPreflight: !result.timedOut &&
223
+ framework.preflight?.exitCode === result.exitCode,
163
224
  timedOut: result.timedOut,
164
- exitMatchesPreflight,
165
- commandOutputSummary: summarizeTestOutput(result.stdout, result.stderr),
166
225
  },
167
226
  }
168
227
  }
169
228
 
170
229
  /**
171
- * Describes an exit mismatch that cannot be attributed because clean executions disagreed.
230
+ * Returns a compact preflight summary.
172
231
  *
173
- * @param {object} preflight initial clean preflight evidence
174
- * @param {object} instrumented instrumented command result
175
- * @param {object} cleanConfirmation repeated clean command result
176
- * @returns {string} customer-facing diagnosis
232
+ * @param {object} preflight preflight evidence
233
+ * @returns {object} compact evidence
177
234
  */
178
- function getUnstableBaselineDiagnosis (preflight, instrumented, cleanConfirmation) {
179
- const confirmation = cleanConfirmation.timedOut
180
- ? 'timed out'
181
- : `exited ${cleanConfirmation.exitCode}`
182
- return `The selected command exited ${preflight.exitCode} during the initial clean preflight, exited ` +
183
- `${instrumented.exitCode} with Datadog initialized, and ${confirmation} during a second clean run. The ` +
184
- 'non-Datadog baseline was not stable, so no Test Optimization compatibility conclusion was reached.'
235
+ function summarizePreflight (preflight = {}) {
236
+ return {
237
+ durationMs: preflight.durationMs,
238
+ exitCode: preflight.exitCode,
239
+ observedTestCount: preflight.observedTestCount,
240
+ ran: preflight.ran === true,
241
+ selectorVerification: preflight.selectorVerification,
242
+ timedOut: preflight.timedOut === true,
243
+ }
185
244
  }
186
245
 
187
246
  /**
188
- * Describes an instrumented-only exit mismatch after the clean baseline was reproduced.
247
+ * Verifies repository wrapper scope from captured test source files.
189
248
  *
190
- * @param {object} preflight initial clean preflight evidence
191
- * @param {object} instrumented instrumented command result
192
- * @returns {string} customer-facing diagnosis
249
+ * @param {object} framework framework manifest entry
250
+ * @param {object[]} events normalized captured events
251
+ * @returns {object} selector verification evidence
193
252
  */
194
- function getPossibleCompatibilityDiagnosis (preflight, instrumented) {
195
- return `The selected command exited ${preflight.exitCode} in both runs without Datadog, but exited ` +
196
- `${instrumented.exitCode} with Datadog initialized after reporting Test Optimization events. This may indicate ` +
197
- 'a dd-trace compatibility issue rather than a project baseline failure.'
198
- }
199
-
200
- function getBasicReportingCommand (framework) {
201
- return getLocalValidationCommand(framework, framework.existingTestCommand)
202
- }
203
-
204
- async function failBasicReportingWithDebugRerun (options) {
205
- const failure = await failWithDebugRerun(options)
206
- return refineBasicReportingFailure(failure)
207
- }
208
-
209
- function refineBasicReportingFailure (failure) {
210
- const evidence = failure.evidence || {}
211
- if (['dd-trace-preload-failed', 'command-setup-failed'].includes(evidence.eventLevelFailure?.kind)) {
212
- failure.status = 'error'
213
- evidence.localDiagnosis = evidence.eventLevelFailure
214
- return failure
253
+ function getSelectorEvidence (framework, events) {
254
+ const mode = framework.validation?.selectorScope
255
+ const expectedTestFile = framework.validation?.testFile
256
+ if (mode === 'bounded_direct_runner') {
257
+ return { mode, verified: true }
215
258
  }
216
-
217
- const diagnosis = getDebugAwareDiagnosis(failure.diagnosis, evidence)
218
- if (!diagnosis) return failure
219
-
220
- failure.diagnosis = diagnosis.summary
221
- evidence.localDiagnosis = diagnosis
222
-
223
- if (evidence.eventLevelFailure) {
224
- evidence.eventLevelFailure = {
225
- ...evidence.eventLevelFailure,
226
- summary: diagnosis.summary,
227
- recommendation: diagnosis.recommendation,
228
- signals: diagnosis.signals,
229
- }
259
+ if (mode !== 'instrumented_event_identity') {
260
+ return { mode: mode || 'missing', verified: false }
230
261
  }
231
262
 
232
- return failure
233
- }
234
-
235
- function getDebugAwareDiagnosis (currentDiagnosis, evidence) {
236
- if (evidence.eventLevelFailure?.kind !== 'no-test-optimization-events') return null
237
-
238
- const debugRerun = evidence.debugRerun
239
- if (!debugRerun || debugRerun.ran !== true) return null
240
-
241
- const testOutputSummary = summarizeTestOutput(
242
- (evidence.commandOutputSummary || []).join('\n'),
243
- (debugRerun.stdoutExcerpt || []).join('\n')
244
- )
245
- const testsRan = commandOutputShowsTestsRan(testOutputSummary)
246
- const debugLine = findDebugLine(debugRerun, /dd-trace is not initialized in a package manager/i)
247
- const noDebugEvents = !hasAnyTestOptimizationEvent(debugRerun)
248
-
249
- if (testsRan && debugLine && noDebugEvents) {
250
- return {
251
- kind: 'tests-ran-tracer-not-initialized',
252
- summary: 'The selected command ran tests, but no Test Optimization events reached the offline event artifact. ' +
253
- `The debug rerun printed "${debugLine}", which means the preload executed in the package-manager ` +
254
- 'wrapper without producing Test Optimization events from the test process.',
255
- recommendation: 'Try a direct test-runner command, or verify NODE_OPTIONS with dd-trace/ci/init reaches the ' +
256
- 'final test process rather than only the package-manager wrapper.',
257
- signals: getDebugSignals({
258
- debugLine,
259
- debugRerun,
260
- testOutputSummary,
261
- }),
262
- }
263
- }
264
-
265
- if (testsRan && noDebugEvents) {
266
- return {
267
- kind: 'tests-ran-no-test-optimization-events',
268
- summary: 'The selected command ran tests, but no Test Optimization events reached the offline event artifact. ' +
269
- 'The debug rerun did not emit Test Optimization events either.',
270
- recommendation: 'Inspect the debug rerun excerpt for tracer initialization or offline exporter errors, then ' +
271
- 'verify NODE_OPTIONS with dd-trace/ci/init reaches the final test process.',
272
- signals: getDebugSignals({
273
- debugRerun,
274
- testOutputSummary,
275
- }),
276
- }
277
- }
278
-
279
- if (debugLine && noDebugEvents) {
280
- return {
281
- kind: 'tracer-not-initialized',
282
- summary: `${currentDiagnosis} The debug rerun printed "${debugLine}".`,
283
- recommendation: 'Verify NODE_OPTIONS with dd-trace/ci/init reaches the final test process.',
284
- signals: getDebugSignals({
285
- debugLine,
286
- debugRerun,
287
- testOutputSummary,
288
- }),
289
- }
290
- }
291
- }
292
-
293
- function shouldRunDebugRerun (eventLevelFailure, result) {
294
- return result.timedOut !== true &&
295
- eventLevelFailure.kind !== 'vitest-benchmark' &&
296
- eventLevelFailure.kind !== 'custom-jest-runner'
297
- }
298
-
299
- function matchesPreflightExitCode (preflight, exitCode) {
300
- return preflight?.ran === true &&
301
- Number.isInteger(preflight.exitCode) &&
302
- preflight.exitCode === exitCode
303
- }
304
-
305
- function summarizePreflight (preflight) {
306
- if (!preflight || preflight.ran !== true) {
307
- return {
308
- ran: false,
309
- reason: 'No dd-trace-less preflight result was recorded in the manifest.',
310
- }
311
- }
263
+ const tests = eventsOfType(events, 'test')
264
+ const sourceFiles = tests.map(test => test.testSourceFile).filter(Boolean)
265
+ const matchingTestEvents = sourceFiles.filter(filename => {
266
+ return sourceFilesMatch(filename, expectedTestFile)
267
+ }).length
268
+ const differentSourceFiles = [...new Set(sourceFiles.filter(filename => {
269
+ return !sourceFilesMatch(filename, expectedTestFile)
270
+ }))].slice(0, 5)
271
+ const testEventsWithoutSourceFile = tests.length - sourceFiles.length
312
272
 
313
273
  return {
314
- ran: true,
315
- exitCode: preflight.exitCode,
316
- observedTestCount: preflight.observedTestCount,
317
- stdoutSummary: preflight.stdoutSummary,
318
- stderrSummary: preflight.stderrSummary,
274
+ differentSourceFiles,
275
+ expectedTestFile,
276
+ matchingTestEvents,
277
+ mode,
278
+ testEventsWithoutSourceFile,
279
+ verified: matchingTestEvents > 0 &&
280
+ differentSourceFiles.length === 0 &&
281
+ testEventsWithoutSourceFile === 0,
319
282
  }
320
283
  }
321
284
 
322
- function summarizeTestOutput (stdout = '', stderr = '') {
323
- return findInterestingLines(`${stdout}\n${stderr}`, [
324
- /\b\d+\s+passing\b/i,
325
- /\b\d+\s+pending\b/i,
326
- /\b\d+\s+failing\b/i,
327
- /\b\d+\s+passed\b/i,
328
- /\b\d+\s+failed\b/i,
329
- /\btests?\b.*\bpassed\b/i,
330
- /\btests?\b.*\bfailed\b/i,
331
- /\bSuccessfully ran target\b.*\btest\b/i,
332
- /\bsuccess:\s*\d+\b/i,
333
- /\bTasks:\s*\d+\s+successful\b/i,
334
- ], 8)
335
- }
336
-
337
- function commandOutputShowsTestsRan (lines) {
338
- return lines.some(line => {
339
- return /\b\d+\s+(?:passing|passed)\b/i.test(line) ||
340
- /\btests?\b.*\bpassed\b/i.test(line) ||
341
- /\bSuccessfully ran target\b.*\btest\b/i.test(line) ||
342
- /\bsuccess:\s*[1-9]\d*\b/i.test(line) ||
343
- /\bfailed:\s*[1-9]\d*\b/i.test(line) ||
344
- /\bTasks:\s*[1-9]\d*\s+successful\b/i.test(line)
345
- })
346
- }
347
-
348
- function findDebugLine (debugRerun, pattern) {
349
- const lines = [
350
- ...(debugRerun.debugLines || []),
351
- ...(debugRerun.stdoutExcerpt || []),
352
- ...(debugRerun.stderrExcerpt || []),
353
- ]
354
- return lines.find(line => pattern.test(line))
285
+ /**
286
+ * Compares absolute and repository-relative test source paths without filesystem access.
287
+ *
288
+ * @param {string} actual captured test source path
289
+ * @param {string} expected approved representative path
290
+ * @returns {boolean} whether both paths identify the same file
291
+ */
292
+ function sourceFilesMatch (actual, expected) {
293
+ const normalizedActual = normalizeSourceFile(actual)
294
+ const normalizedExpected = normalizeSourceFile(expected)
295
+ if (!normalizedActual || !normalizedExpected) return false
296
+ if (normalizedActual === normalizedExpected) return true
297
+ if (!normalizedActual.includes('/') || !normalizedExpected.includes('/')) return false
298
+ return normalizedExpected.endsWith(`/${normalizedActual}`) ||
299
+ normalizedActual.endsWith(`/${normalizedExpected}`)
355
300
  }
356
301
 
357
- function getDebugSignals ({ debugLine, debugRerun, testOutputSummary }) {
358
- return {
359
- debugLine,
360
- debugLines: debugRerun.debugLines || [],
361
- stdoutExcerpt: debugRerun.stdoutExcerpt || [],
362
- stderrExcerpt: debugRerun.stderrExcerpt || [],
363
- testOutputSummary,
364
- }
302
+ function normalizeSourceFile (filename) {
303
+ return String(filename || '')
304
+ .replace(/^file:\/\//, '')
305
+ .replaceAll('\\', '/')
306
+ .replace(/^\.\//, '')
307
+ .replaceAll(/\/+/g, '/')
365
308
  }
366
309
 
367
- function summarizeCommandFailure (result, evidence) {
368
- const output = `${result.stdout}\n${result.stderr}`
369
- const buildErrors = findInterestingLines(output, [
370
- /Could not resolve /,
371
- /Cannot find module/,
372
- /Module not found/,
373
- /Error \[ERR_MODULE_NOT_FOUND\]/,
374
- ])
375
- const assertionFailures = findInterestingLines(output, [
376
- /AssertionError/,
377
- /Timed out retrying/,
378
- /^\s+\d+\) /,
379
- ])
380
- const testEventsWereReported = evidence.testSessionEvents > 0 &&
381
- evidence.testModuleEvents > 0 &&
382
- evidence.testSuiteEvents > 0 &&
383
- evidence.testEvents > 0
384
- const summary = getFailureSummary({
385
- buildErrors,
386
- assertionFailures,
387
- exitCode: result.exitCode,
388
- testEventsWereReported,
389
- timedOut: result.timedOut,
310
+ /**
311
+ * Classifies a direct command failure.
312
+ *
313
+ * @param {object} framework framework entry
314
+ * @param {object} result command result
315
+ * @returns {object|undefined} blocker
316
+ */
317
+ function classifyFailure (framework, result) {
318
+ return getCommandBlocker(result, {
319
+ browserRequired: framework.browserRequired,
320
+ framework: framework.framework,
390
321
  })
391
-
392
- return {
393
- assertionFailures,
394
- buildErrors,
395
- stderrExcerpt: tailInterestingLines(result.stderr),
396
- stdoutExcerpt: tailInterestingLines(result.stdout),
397
- summary,
398
- testEventsWereReported,
399
- }
400
- }
401
-
402
- function getMissingEventDiagnosis ({ framework, result, evidence }) {
403
- const missingLevels = getMissingLevels(evidence)
404
- const commandFailure = evidence.commandFailure
405
- const vitestBenchmark = detectVitestBenchmark(framework, result)
406
- const frameworkSourceTreeRunner = detectFrameworkSourceTreeRunner(framework, result)
407
- const customJestRunner = detectCustomJestRunner(framework)
408
-
409
- if (commandFailure?.buildErrors?.length > 0 && !hasAnyTestOptimizationEvent(evidence)) {
410
- const preloadFailed = /(?:ci\/init\.js|internal\/preload)/.test(`${result.stdout}\n${result.stderr}`)
411
- return {
412
- kind: preloadFailed ? 'dd-trace-preload-failed' : 'command-setup-failed',
413
- missingLevels,
414
- signals: commandFailure.buildErrors,
415
- summary: preloadFailed
416
- ? `The Test Optimization preload failed before tests started: ${commandFailure.buildErrors[0]}. ` +
417
- 'No Test Optimization conclusion was reached.'
418
- : `${commandFailure.summary} No Test Optimization conclusion was reached.`,
419
- recommendation: preloadFailed
420
- ? 'Repair or reinstall dd-trace so its declared dependencies resolve, then rerun validation.'
421
- : 'Fix the selected project command or its setup, then rerun validation.',
422
- }
423
- }
424
-
425
- if (vitestBenchmark) {
426
- return {
427
- kind: 'vitest-benchmark',
428
- missingLevels,
429
- signals: vitestBenchmark.signals,
430
- summary: 'The selected Vitest command appears to run benchmark mode, not normal tests. ' +
431
- 'Test Optimization reported session/module/suite events, but Vitest benchmark mode did not emit ' +
432
- 'per-test events. Choose a normal Vitest test command such as "vitest run <test-file>" for validation.',
433
- recommendation: 'Replace the selected command with a normal Vitest test command; do not use `vitest bench` ' +
434
- 'or benchmark-only `*.bench.*` files for Test Optimization validation.',
435
- }
436
- }
437
-
438
- if (frameworkSourceTreeRunner && !hasAnyTestOptimizationEvent(evidence)) {
439
- return {
440
- kind: 'framework-source-tree-runner',
441
- missingLevels,
442
- signals: frameworkSourceTreeRunner.signals,
443
- summary: 'The selected command ran tests from the test framework source tree, but no Test Optimization ' +
444
- 'events reached the offline event artifact. This command is not equivalent to a customer project running an ' +
445
- 'installed test-runner package.',
446
- recommendation: 'Choose a project test command that uses an installed supported framework package. If this ' +
447
- 'repository is the framework itself, treat this entry as not runnable for Test Optimization validation.',
448
- }
449
- }
450
-
451
- if (!hasAnyTestOptimizationEvent(evidence)) {
452
- return {
453
- kind: 'no-test-optimization-events',
454
- missingLevels,
455
- summary: 'No Test Optimization test events reached the offline event artifact. The tracer may not have ' +
456
- 'initialized in the test process, or the selected command may not have executed tests.',
457
- recommendation: 'Check the debug rerun output for tracer initialization or offline exporter errors.',
458
- }
459
- }
460
-
461
- if (evidence.testEvents === 0) {
462
- if (customJestRunner) {
463
- return {
464
- kind: 'custom-jest-runner',
465
- missingLevels,
466
- customTestRunner: customJestRunner,
467
- signals: customJestRunner.signals,
468
- summary: `We detected a custom Jest-compatible runner: \`${customJestRunner.name}\`. The test command ` +
469
- 'executes tests, but it does not use a currently supported Jest entrypoint. Test Optimization ' +
470
- 'initialized, but this runner did not emit the Jest lifecycle events needed to report individual ' +
471
- 'suites and tests.',
472
- recommendation: 'Try a standard Jest runner command for validation, or choose a test command that does not ' +
473
- 'use the custom runner. If this project must use the custom runner, dd-trace may need explicit support ' +
474
- 'for that runner before per-test reporting and advanced Test Optimization features can work.',
475
- }
476
- }
477
-
478
- return {
479
- kind: 'missing-test-events',
480
- missingLevels,
481
- summary: 'Test Optimization initialized and emitted higher-level events, but per-test events were missing. ' +
482
- 'This usually points to an unsupported runner mode, unsupported framework configuration, or per-test hooks ' +
483
- 'not firing for the selected command.',
484
- recommendation: 'Choose a smaller standard test command, then inspect the debug rerun output for hook or ' +
485
- 'exporter errors.',
486
- }
487
- }
488
-
489
- return {
490
- kind: 'missing-event-levels',
491
- missingLevels,
492
- summary: `The command ran, but these required Test Optimization event levels were missing: ${
493
- missingLevels.join(', ')
494
- }.`,
495
- recommendation: 'Inspect the debug rerun output for tracer initialization, hook, or exporter errors.',
496
- }
497
322
  }
498
323
 
324
+ /**
325
+ * Returns missing Test Optimization hierarchy levels.
326
+ *
327
+ * @param {object} evidence event evidence
328
+ * @returns {string[]} missing levels
329
+ */
499
330
  function getMissingLevels (evidence) {
500
- const missing = []
501
- if (evidence.testSessionEvents === 0) missing.push('test_session_end')
502
- if (evidence.testModuleEvents === 0) missing.push('test_module_end')
503
- if (evidence.testSuiteEvents === 0) missing.push('test_suite_end')
504
- if (evidence.testEvents === 0) missing.push('test')
505
- return missing
506
- }
507
-
508
- function hasAnyTestOptimizationEvent (evidence) {
509
- return evidence.testSessionEvents > 0 ||
510
- evidence.testModuleEvents > 0 ||
511
- evidence.testSuiteEvents > 0 ||
512
- evidence.testEvents > 0
513
- }
514
-
515
- function detectVitestBenchmark (framework, result) {
516
- if (framework.framework !== 'vitest') return null
517
-
518
- const command = result.command || ''
519
- const output = `${result.stdout}\n${result.stderr}`
520
- const signals = []
521
-
522
- if (/\bvitest\s+bench\b/.test(command)) signals.push('command contains `vitest bench`')
523
- if (/\.bench\.[cm]?[jt]sx?\b/.test(command)) signals.push('command targets a `*.bench.*` file')
524
- if (/^\s*BENCH\s+Summary\b/m.test(output)) signals.push('stdout contains a Vitest BENCH summary')
525
- if (/Benchmarking is an experimental feature/.test(output)) {
526
- signals.push('stderr says Vitest benchmarking is experimental')
527
- }
528
-
529
- return signals.length > 0 ? { signals } : null
530
- }
531
-
532
- function detectCustomJestRunner (framework) {
533
- if (framework.framework !== 'jest') return null
534
-
535
- const configRunner = findJestRunnerInConfigFiles(framework.project?.configFiles || [])
536
- if (configRunner && isCustomJestRunner(configRunner.name)) return configRunner
537
-
538
- const packageRunner = findJestRunnerInPackageJson(framework.project?.packageJson)
539
- if (packageRunner && isCustomJestRunner(packageRunner.name)) return packageRunner
331
+ return [
332
+ ['session', evidence.testSessionEvents],
333
+ ['module', evidence.testModuleEvents],
334
+ ['suite', evidence.testSuiteEvents],
335
+ ['test', evidence.testEvents],
336
+ ].filter(([, count]) => count < 1).map(([level]) => level)
540
337
  }
541
338
 
542
- function findJestRunnerInConfigFiles (configFiles) {
543
- for (const configFile of configFiles) {
544
- const content = readFile(configFile)
545
- if (!content) continue
546
-
547
- const match = /(?:^|[,{]\s*)runner\s*:\s*['"]([^'"]+)['"]/.exec(content)
548
- if (!match) continue
549
-
550
- return {
551
- name: match[1],
552
- source: configFile,
553
- sourceType: 'config',
554
- signals: [
555
- `Jest config ${configFile} sets runner: ${match[1]}`,
556
- ],
557
- }
558
- }
559
- }
560
-
561
- function findJestRunnerInPackageJson (packageJsonPath) {
562
- if (!packageJsonPath) return
563
-
564
- try {
565
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
566
- const runner = packageJson.jest?.runner
567
- if (typeof runner !== 'string' || runner.length === 0) return
568
-
569
- return {
570
- name: runner,
571
- source: packageJsonPath,
572
- sourceType: 'package.json',
573
- signals: [
574
- `package.json jest.runner is ${runner}`,
575
- ],
576
- }
577
- } catch {}
578
- }
579
-
580
- function isCustomJestRunner (runner) {
581
- return runner !== 'jest-runner'
582
- }
583
-
584
- function detectFrameworkSourceTreeRunner (framework, result) {
585
- if (framework.framework !== 'mocha') return null
586
-
587
- const projectName = framework.project?.name || ''
588
- const commandAndOutput = [
589
- result.command || '',
590
- result.stdout || '',
591
- result.stderr || '',
592
- ].join('\n')
593
- const signals = []
594
-
595
- if (projectName === 'mocha') {
596
- signals.push('repository package name is `mocha`')
597
- }
598
- if (/\bnode\s+\.\/bin\/mocha\.js\b/.test(commandAndOutput) ||
599
- /\bnode\s+bin\/mocha\.js\b/.test(commandAndOutput)) {
600
- signals.push('selected command invokes the repository-local `bin/mocha.js` source-tree runner')
601
- }
602
- if (fileExists(framework.project?.root, 'lib/mocha.cjs') && fileExists(framework.project?.root, 'lib/runner.cjs')) {
603
- signals.push('repository contains Mocha source files `lib/mocha.cjs` and `lib/runner.cjs`')
604
- }
605
-
606
- return signals.length >= 2 ? { signals } : null
607
- }
608
-
609
- function fileExists (root, filename) {
610
- if (!root) return false
611
-
612
- try {
613
- return fs.existsSync(path.join(root, filename))
614
- } catch {
615
- return false
616
- }
617
- }
618
-
619
- function readFile (filename) {
620
- try {
621
- return fs.readFileSync(filename, 'utf8')
622
- } catch {}
623
- }
624
-
625
- function getFailureSummary ({ buildErrors, assertionFailures, exitCode, testEventsWereReported, timedOut }) {
626
- if (timedOut) {
627
- return 'The selected test command timed out before payload validation could pass.'
628
- }
629
-
630
- if (buildErrors.length > 0) {
631
- if (testEventsWereReported) {
632
- return 'The selected test command reported Datadog test events, but project setup/build errors made it fail.'
633
- }
634
-
635
- return 'The selected test command failed during project setup/build before payload validation could pass.'
636
- }
637
-
638
- if (assertionFailures.length > 0 && testEventsWereReported) {
639
- return 'The selected test command reported Datadog test events, but the tests failed.'
640
- }
641
-
642
- if (testEventsWereReported) {
643
- return `The selected test command reported Datadog test events, but exited ${exitCode}.`
644
- }
645
-
646
- return `The selected test command exited ${exitCode} before payload validation could pass.`
339
+ /**
340
+ * Extracts bounded useful test output lines.
341
+ *
342
+ * @param {string} [stdout] command stdout
343
+ * @param {string} [stderr] command stderr
344
+ * @returns {string[]} output summary
345
+ */
346
+ function summarizeTestOutput (stdout = '', stderr = '') {
347
+ const lines = `${stdout}\n${stderr}`.split(/\r?\n/)
348
+ const interesting = lines.filter(line => {
349
+ return /\b(?:error|fail|pass|scenario|suite|test|timed out|warning)\b/i.test(line)
350
+ })
351
+ return [...new Set(interesting.map(line => line.trim()).filter(Boolean))].slice(-12)
647
352
  }
648
353
 
649
- module.exports = {
650
- getDebugAwareDiagnosis,
651
- getBasicReportingCommand,
652
- getMissingEventDiagnosis,
653
- refineBasicReportingFailure,
654
- runBasicReporting,
655
- shouldRunDebugRerun,
656
- summarizeTestOutput,
657
- }
354
+ module.exports = { runBasicReporting, summarizeTestOutput }