dd-trace 6.4.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/init.js +18 -0
  52. package/initialize.mjs +1 -1
  53. package/loader-hook.mjs +28 -18
  54. package/openfeature.d.ts +1 -0
  55. package/openfeature.js +4 -0
  56. package/package.json +10 -7
  57. package/packages/datadog-instrumentations/src/ai.js +8 -2
  58. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  60. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  61. package/packages/datadog-instrumentations/src/cucumber.js +20 -7
  62. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  63. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  64. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  65. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  67. package/packages/datadog-instrumentations/src/jest.js +26 -0
  68. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  69. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  71. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  72. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  73. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  74. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  75. package/packages/datadog-instrumentations/src/next.js +24 -0
  76. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  77. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  78. package/packages/datadog-instrumentations/src/pg.js +2 -2
  79. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  80. package/packages/datadog-instrumentations/src/process.js +3 -0
  81. package/packages/datadog-instrumentations/src/router.js +18 -8
  82. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  83. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  84. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  85. package/packages/datadog-instrumentations/src/ws.js +2 -1
  86. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  87. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  88. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  89. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  90. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  91. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  92. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  93. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  94. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  95. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  96. package/packages/datadog-plugin-http2/src/client.js +1 -1
  97. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  98. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  99. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  100. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  101. package/packages/datadog-plugin-router/src/index.js +11 -2
  102. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  103. package/packages/datadog-plugin-ws/src/close.js +2 -1
  104. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  105. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  106. package/packages/datadog-plugin-ws/src/server.js +2 -1
  107. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  110. package/packages/dd-trace/src/azure_metadata.js +2 -1
  111. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  112. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  113. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  114. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  115. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  116. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  117. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  119. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  120. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  121. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  122. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  123. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  124. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  127. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  128. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  129. package/packages/dd-trace/src/config/helper.js +1 -0
  130. package/packages/dd-trace/src/config/index.js +10 -0
  131. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  132. package/packages/dd-trace/src/exporter.js +2 -0
  133. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  134. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  135. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  136. package/packages/dd-trace/src/feature-registry.js +10 -3
  137. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  138. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  139. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  140. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  141. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  142. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  143. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  146. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  147. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  149. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  150. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  151. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  153. package/packages/dd-trace/src/llmobs/util.js +32 -0
  154. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  155. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  156. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  157. package/packages/dd-trace/src/openfeature/index.js +0 -2
  158. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  159. package/packages/dd-trace/src/openfeature/register.js +16 -27
  160. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  161. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  162. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  163. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  164. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  165. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  166. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  167. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  168. package/packages/dd-trace/src/proxy.js +84 -7
  169. package/packages/dd-trace/src/span_processor.js +5 -0
  170. package/packages/dd-trace/src/standalone/index.js +0 -14
  171. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  172. package/packages/dd-trace/src/util.js +26 -0
  173. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  174. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  175. package/vendor/dist/protobufjs/index.js +1 -1
  176. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  177. package/vendor/dist/shell-quote/index.js +1 -1
  178. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,406 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const { parseBoundedJson } = require('./bounded-json')
7
+ const { normalizeRequests } = require('./payload-normalizer')
8
+
9
+ const MAX_COMPLETION_BYTES = 4096
10
+ const MAX_COMPLETION_FILES = 2048
11
+ const MAX_COMPLETION_TOTAL_BYTES = 2 * 1024 * 1024
12
+ const MAX_DECODED_COLLECTION_ENTRIES = 100_000
13
+ const MAX_OUTPUT_BYTES = 16 * 1024 * 1024
14
+ const MAX_OUTPUT_FILES = 10_000
15
+ const MAX_OUTPUT_MODULES = 500
16
+ const MAX_OUTPUT_SUITES = 1000
17
+ const MAX_OUTPUT_TESTS = 2000
18
+ const MAX_OUTPUT_STRING_BYTES = 64 * 1024
19
+ const MAX_SAMPLED_EVENTS_PER_PROCESS = 11
20
+ const INPUT_NAMES = new Set(['known_tests', 'settings', 'skippable_tests', 'test_management'])
21
+ const EVENT_TYPES = new Set(['test', 'test_module_end', 'test_session_end', 'test_suite_end'])
22
+ const META_FIELDS = new Set([
23
+ 'test.command',
24
+ 'test.early_flake.enabled',
25
+ 'test.final_status',
26
+ 'test.is_new',
27
+ 'test.is_retry',
28
+ 'test.module',
29
+ 'test.name',
30
+ 'test.retry_reason',
31
+ 'test.source.file',
32
+ 'test.status',
33
+ 'test.suite',
34
+ 'test.test_management.attempt_to_fix_passed',
35
+ 'test.test_management.enabled',
36
+ 'test.test_management.is_attempt_to_fix',
37
+ 'test.test_management.is_quarantined',
38
+ 'test.test_management.is_test_disabled',
39
+ ])
40
+ const METRIC_FIELDS = new Set(['test.is_new', 'test.is_retry'])
41
+ const ROOT_ENTRIES = new Set(['completions', 'payloads'])
42
+ const PAYLOAD_KINDS = new Set(['tests'])
43
+
44
+ /**
45
+ * Reads bounded projected payloads and reconciles them with authoritative per-process completion records.
46
+ *
47
+ * @param {string} outputRoot payload output root
48
+ * @returns {object} parsed output and aggregate capture metadata
49
+ */
50
+ function readOfflineOutput (outputRoot) {
51
+ assertDirectory(outputRoot, 'output root')
52
+ assertDirectoryEntries(outputRoot, ROOT_ENTRIES)
53
+
54
+ const payloadsRoot = path.join(outputRoot, 'payloads')
55
+ const completionsRoot = path.join(outputRoot, 'completions')
56
+ const exporterInitialized = exists(payloadsRoot) || exists(completionsRoot)
57
+ if (!exporterInitialized) return emptyOutput()
58
+
59
+ const state = {
60
+ bytes: 0,
61
+ completionBytes: 0,
62
+ decodedEntries: 0,
63
+ files: 0,
64
+ }
65
+ const completions = readCompletions(outputRoot, completionsRoot, state)
66
+ const captureMode = getCaptureMode(completions)
67
+ state.eventCounts = { modules: 0, suites: 0, tests: 0, total: 0 }
68
+ const artifactsByProcess = new Map()
69
+ const events = []
70
+
71
+ if (exists(payloadsRoot)) {
72
+ assertDirectory(payloadsRoot, 'payloads directory')
73
+ assertPathInside(outputRoot, payloadsRoot)
74
+ assertDirectoryEntries(payloadsRoot, PAYLOAD_KINDS)
75
+ readPayloadFiles(payloadsRoot, 'tests', state, (processId, value) => {
76
+ assertTestPayload(value)
77
+ const normalized = normalizeRequests([{ url: '/api/v2/citestcycle', payload: value }])
78
+ for (const event of normalized) {
79
+ assertRecognizedEventBudget(event, captureMode, completions.length, state.eventCounts)
80
+ events.push(event)
81
+ }
82
+ const artifact = getProcessArtifacts(artifactsByProcess, processId)
83
+ artifact.payloadFiles++
84
+ artifact.eventsRetained += normalized.length
85
+ })
86
+ }
87
+
88
+ if (completions.length === 0) {
89
+ throw new Error('Offline Test Optimization exporter initialized but did not write completion evidence.')
90
+ }
91
+ reconcileCompletions(completions, artifactsByProcess)
92
+
93
+ const summary = aggregateCompletions(completions)
94
+ return {
95
+ captureMode,
96
+ completionCount: completions.length,
97
+ events,
98
+ initialized: true,
99
+ inputs: summary.inputs,
100
+ observedEventCount: summary.eventsObserved,
101
+ payloadFileCount: summary.payloadFiles,
102
+ retainedEventCount: summary.eventsRetained,
103
+ sampled: summary.eventsObserved > summary.eventsRetained,
104
+ summary,
105
+ }
106
+ }
107
+
108
+ function readCompletions (outputRoot, directory, state) {
109
+ if (!exists(directory)) return []
110
+ assertDirectory(directory, 'completions directory')
111
+ assertPathInside(outputRoot, directory)
112
+ const filenames = fs.readdirSync(directory).sort()
113
+ if (filenames.length > MAX_COMPLETION_FILES) {
114
+ throw new Error(`Offline validation output exceeds ${MAX_COMPLETION_FILES} completion records.`)
115
+ }
116
+
117
+ const completions = []
118
+ const processIds = new Set()
119
+ for (const name of filenames) {
120
+ const match = /^completion-([a-f0-9]{32})\.json$/.exec(name)
121
+ if (!match) throw new Error('Offline validation completions directory contains an unexpected entry.')
122
+ const buffer = readRegularFile(path.join(directory, name), MAX_COMPLETION_BYTES, state, true)
123
+ const parsed = parseOutputJson(buffer, 'completion record', state, 1000)
124
+ assertCompletion(parsed, match[1])
125
+ if (processIds.has(parsed.processId)) throw new Error('Offline validation contains duplicate completion records.')
126
+ processIds.add(parsed.processId)
127
+ completions.push(parsed)
128
+ }
129
+ return completions
130
+ }
131
+
132
+ function readPayloadFiles (payloadsRoot, kind, state, consume) {
133
+ const directory = path.join(payloadsRoot, kind)
134
+ if (!exists(directory)) return
135
+ assertDirectory(directory, `${kind} payload directory`)
136
+ assertPathInside(payloadsRoot, directory)
137
+
138
+ const pattern = new RegExp(String.raw`^${kind}-([a-f0-9]{32})-[0-9]+-[0-9]+-[0-9]+\.json$`)
139
+ const filenames = fs.readdirSync(directory).sort()
140
+ if (state.files + filenames.length > MAX_OUTPUT_FILES) {
141
+ throw new Error(`Offline validation output exceeds ${MAX_OUTPUT_FILES} payload files.`)
142
+ }
143
+ for (const name of filenames) {
144
+ const match = pattern.exec(name)
145
+ if (!match) throw new Error(`Offline validation ${kind} payload directory contains an unexpected entry.`)
146
+ state.files++
147
+ const buffer = readRegularFile(path.join(directory, name), MAX_OUTPUT_BYTES, state, false)
148
+ consume(match[1], parseOutputJson(buffer, `${kind} payload`, state, MAX_DECODED_COLLECTION_ENTRIES))
149
+ }
150
+ }
151
+
152
+ function readRegularFile (filename, individualLimit, state, completion) {
153
+ const stat = fs.lstatSync(filename)
154
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink > 1) {
155
+ throw new Error('Offline validation artifact must be a regular, unlinked file.')
156
+ }
157
+ if (stat.size > individualLimit) throw new Error(`Offline validation artifact exceeds ${individualLimit} bytes.`)
158
+ const totalKey = completion ? 'completionBytes' : 'bytes'
159
+ const totalLimit = completion ? MAX_COMPLETION_TOTAL_BYTES : MAX_OUTPUT_BYTES
160
+ if (state[totalKey] + stat.size > totalLimit) {
161
+ throw new Error(`Offline validation output exceeds ${totalLimit} aggregate bytes.`)
162
+ }
163
+
164
+ const file = fs.openSync(filename, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0))
165
+ try {
166
+ const opened = fs.fstatSync(file)
167
+ if (!opened.isFile() || opened.nlink > 1 || opened.dev !== stat.dev || opened.ino !== stat.ino) {
168
+ throw new Error('Offline validation artifact changed while it was opened.')
169
+ }
170
+ const buffer = fs.readFileSync(file)
171
+ const completed = fs.fstatSync(file)
172
+ if (completed.size !== opened.size || completed.mtimeMs !== opened.mtimeMs) {
173
+ throw new Error('Offline validation artifact changed while it was read.')
174
+ }
175
+ state[totalKey] += buffer.length
176
+ return buffer
177
+ } finally {
178
+ fs.closeSync(file)
179
+ }
180
+ }
181
+
182
+ function parseOutputJson (buffer, label, state, perFileEntries) {
183
+ if (buffer.length === 0) throw new Error(`Offline validation ${label} is empty.`)
184
+ const parsed = parseBoundedJson(buffer, {
185
+ label: `Offline validation ${label}`,
186
+ maxCollectionEntries: perFileEntries,
187
+ maxNestingDepth: 64,
188
+ maxStringBytes: MAX_OUTPUT_STRING_BYTES,
189
+ })
190
+ state.decodedEntries += parsed.collectionEntries
191
+ if (state.decodedEntries > MAX_DECODED_COLLECTION_ENTRIES) {
192
+ throw new Error(
193
+ `Offline validation output exceeds ${MAX_DECODED_COLLECTION_ENTRIES} aggregate decoded entries.`
194
+ )
195
+ }
196
+ return parsed.value
197
+ }
198
+
199
+ function assertTestPayload (value) {
200
+ if (!isObject(value) || value.version !== 1 || !Array.isArray(value.events) ||
201
+ Object.keys(value).sort().join(',') !== 'events,version') {
202
+ throw new Error('Offline validation test payload has an unsupported JSON shape.')
203
+ }
204
+ for (const event of value.events) {
205
+ if (!isObject(event) || !EVENT_TYPES.has(event.type) || !isObject(event.content) ||
206
+ !isObject(event.content.meta) || !isObject(event.content.metrics) ||
207
+ Object.keys(event).sort().join(',') !== 'content,type' ||
208
+ Object.keys(event.content).sort().join(',') !== 'meta,metrics' ||
209
+ Object.keys(event.content.meta).some(name => !META_FIELDS.has(name)) ||
210
+ Object.keys(event.content.metrics).some(name => !METRIC_FIELDS.has(name))) {
211
+ throw new Error('Offline validation test payload contains an unsupported event shape.')
212
+ }
213
+ }
214
+ }
215
+
216
+ function assertCompletion (completion, filenameProcessId) {
217
+ if (!isObject(completion) || Object.keys(completion).sort().join(',') !==
218
+ 'captureMode,counts,errors,inputs,processId,version' || completion.version !== 1 ||
219
+ completion.processId !== filenameProcessId || !/^[a-f0-9]{32}$/.test(completion.processId) ||
220
+ !['sample', 'strict'].includes(completion.captureMode) || !isObject(completion.counts) ||
221
+ !isObject(completion.inputs) || !Array.isArray(completion.errors) || completion.errors.length > 20) {
222
+ throw invalidCompletionError()
223
+ }
224
+ const countKeys = ['eventsObserved', 'eventsRetained', 'payloadFiles']
225
+ if (Object.keys(completion.counts).sort().join(',') !== countKeys.sort().join(',') ||
226
+ countKeys.some(name => !isCount(completion.counts[name])) ||
227
+ completion.counts.eventsRetained > completion.counts.eventsObserved ||
228
+ completion.errors.some(error => typeof error !== 'string' || error.length > 100)) {
229
+ throw invalidCompletionError()
230
+ }
231
+ assertCompletionInputs(completion.inputs)
232
+ if (completion.captureMode === 'sample' && completion.counts.eventsRetained > MAX_SAMPLED_EVENTS_PER_PROCESS) {
233
+ throw invalidCompletionError()
234
+ }
235
+ }
236
+
237
+ function getCaptureMode (completions) {
238
+ if (completions.length === 0) return
239
+ const captureMode = completions[0].captureMode
240
+ if (completions.some(completion => completion.captureMode !== captureMode)) {
241
+ throw new Error('Offline Test Optimization completion records use inconsistent capture modes.')
242
+ }
243
+ return captureMode
244
+ }
245
+
246
+ function assertCompletionInputs (inputs) {
247
+ for (const [name, input] of Object.entries(inputs)) {
248
+ if (!INPUT_NAMES.has(name) || !isObject(input) || Object.keys(input).join(',') !== 'status' ||
249
+ !['error', 'loaded'].includes(input.status)) {
250
+ throw invalidCompletionError()
251
+ }
252
+ }
253
+ }
254
+
255
+ function reconcileCompletions (completions, artifactsByProcess) {
256
+ const completionsByProcess = new Map(completions.map(completion => [completion.processId, completion]))
257
+ for (const processId of artifactsByProcess.keys()) {
258
+ if (!completionsByProcess.has(processId)) {
259
+ throw new Error('Offline validation payload artifacts do not have matching completion evidence.')
260
+ }
261
+ }
262
+ for (const completion of completions) {
263
+ const artifacts = artifactsByProcess.get(completion.processId) || emptyProcessArtifacts()
264
+ const counts = completion.counts
265
+ if (counts.payloadFiles !== artifacts.payloadFiles || counts.eventsRetained !== artifacts.eventsRetained) {
266
+ throw new Error('Offline Test Optimization completion evidence does not match retained payload artifacts.')
267
+ }
268
+ }
269
+ }
270
+
271
+ function aggregateCompletions (completions) {
272
+ const aggregate = {
273
+ errors: [],
274
+ eventsObserved: 0,
275
+ eventsRetained: 0,
276
+ inputs: {},
277
+ payloadFiles: 0,
278
+ }
279
+ for (const completion of completions) {
280
+ for (const name of [
281
+ 'eventsObserved',
282
+ 'eventsRetained',
283
+ 'payloadFiles',
284
+ ]) {
285
+ aggregate[name] = addCount(aggregate[name], completion.counts[name])
286
+ }
287
+ for (const error of completion.errors) {
288
+ if (!aggregate.errors.includes(error)) aggregate.errors.push(error)
289
+ }
290
+ mergeInputs(aggregate.inputs, completion.inputs)
291
+ }
292
+ return aggregate
293
+ }
294
+
295
+ function getProcessArtifacts (artifacts, processId) {
296
+ let value = artifacts.get(processId)
297
+ if (!value) {
298
+ value = emptyProcessArtifacts()
299
+ artifacts.set(processId, value)
300
+ }
301
+ return value
302
+ }
303
+
304
+ function emptyProcessArtifacts () {
305
+ return { eventsRetained: 0, payloadFiles: 0 }
306
+ }
307
+
308
+ function mergeInputs (aggregate, inputs) {
309
+ for (const [name, input] of Object.entries(inputs)) {
310
+ aggregate[name] = {
311
+ status: aggregate[name]?.status === 'error' || input.status === 'error' ? 'error' : 'loaded',
312
+ }
313
+ }
314
+ }
315
+
316
+ function addCount (current, value) {
317
+ const total = current + value
318
+ if (!Number.isSafeInteger(total)) throw invalidCompletionError()
319
+ return total
320
+ }
321
+
322
+ function isCount (value) {
323
+ return Number.isSafeInteger(value) && value >= 0
324
+ }
325
+
326
+ function invalidCompletionError () {
327
+ return new Error('Invalid offline Test Optimization exporter completion record.')
328
+ }
329
+
330
+ function assertDirectory (directory, label) {
331
+ const stat = fs.lstatSync(directory)
332
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
333
+ throw new Error(`Offline validation ${label} must be a regular directory.`)
334
+ }
335
+ }
336
+
337
+ function assertDirectoryEntries (directory, allowed) {
338
+ if (fs.readdirSync(directory).some(entry => !allowed.has(entry))) {
339
+ throw new Error('Offline validation payload output contains an unexpected entry.')
340
+ }
341
+ }
342
+
343
+ function assertPathInside (parent, child) {
344
+ const relative = path.relative(fs.realpathSync(parent), fs.realpathSync(child))
345
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
346
+ throw new Error('Offline validation payload path resolves outside its output root.')
347
+ }
348
+ }
349
+
350
+ function exists (filename) {
351
+ try {
352
+ fs.lstatSync(filename)
353
+ return true
354
+ } catch (error) {
355
+ if (error.code === 'ENOENT') return false
356
+ throw error
357
+ }
358
+ }
359
+
360
+ function emptyOutput () {
361
+ return {
362
+ captureMode: undefined,
363
+ completionCount: 0,
364
+ events: [],
365
+ initialized: false,
366
+ inputs: {},
367
+ observedEventCount: 0,
368
+ payloadFileCount: 0,
369
+ retainedEventCount: 0,
370
+ sampled: false,
371
+ summary: undefined,
372
+ }
373
+ }
374
+
375
+ function assertRecognizedEventBudget (event, captureMode, processCount, counts) {
376
+ counts.total++
377
+ if (captureMode === 'sample') {
378
+ if (counts.total > processCount * MAX_SAMPLED_EVENTS_PER_PROCESS) {
379
+ throw new Error('Offline validation sampled output exceeds its run-wide recognized-event budget.')
380
+ }
381
+ return
382
+ }
383
+ if (event.type === 'test_module_end' && ++counts.modules > MAX_OUTPUT_MODULES) {
384
+ throw new Error(`Offline validation output exceeds ${MAX_OUTPUT_MODULES} test modules.`)
385
+ }
386
+ if (event.type === 'test_suite_end' && ++counts.suites > MAX_OUTPUT_SUITES) {
387
+ throw new Error(`Offline validation output exceeds ${MAX_OUTPUT_SUITES} test suites.`)
388
+ }
389
+ if (event.type === 'test' && ++counts.tests > MAX_OUTPUT_TESTS) {
390
+ throw new Error(`Offline validation output exceeds ${MAX_OUTPUT_TESTS} tests.`)
391
+ }
392
+ }
393
+
394
+ function isObject (value) {
395
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value))
396
+ }
397
+
398
+ module.exports = {
399
+ MAX_COMPLETION_FILES,
400
+ MAX_OUTPUT_BYTES,
401
+ MAX_OUTPUT_FILES,
402
+ MAX_OUTPUT_MODULES,
403
+ MAX_OUTPUT_SUITES,
404
+ MAX_OUTPUT_TESTS,
405
+ readOfflineOutput,
406
+ }
@@ -0,0 +1,72 @@
1
+ 'use strict'
2
+
3
+ function normalizeRequests (requests) {
4
+ const events = []
5
+ for (const request of requests) {
6
+ if (!request.url || !request.url.endsWith('/api/v2/citestcycle')) continue
7
+ const payloadEvents = request.payload && Array.isArray(request.payload.events) ? request.payload.events : []
8
+ for (const event of payloadEvents) {
9
+ events.push(normalizeEvent(event, request))
10
+ }
11
+ }
12
+ return events
13
+ }
14
+
15
+ function normalizeEvent (event, request) {
16
+ const content = event.content || {}
17
+ const meta = content.meta || {}
18
+ const metrics = content.metrics || {}
19
+ return {
20
+ type: event.type,
21
+ requestUrl: request.url,
22
+ name: content.name,
23
+ resource: content.resource,
24
+ service: content.service,
25
+ error: content.error,
26
+ meta,
27
+ metrics,
28
+ testName: meta['test.name'],
29
+ testSuite: meta['test.suite'],
30
+ testStatus: meta['test.status'],
31
+ testSourceFile: meta['test.source.file'],
32
+ retryReason: meta['test.retry_reason'],
33
+ isNew: meta['test.is_new'] === 'true' || metrics['test.is_new'] === 1,
34
+ isRetry: meta['test.is_retry'] === 'true' || metrics['test.is_retry'] === 1,
35
+ finalStatus: meta['test.final_status'],
36
+ earlyFlakeEnabled: meta['test.early_flake.enabled'] === 'true',
37
+ testManagementEnabled: meta['test.test_management.enabled'] === 'true',
38
+ isQuarantined: meta['test.test_management.is_quarantined'] === 'true',
39
+ isDisabled: meta['test.test_management.is_test_disabled'] === 'true',
40
+ isAttemptToFix: meta['test.test_management.is_attempt_to_fix'] === 'true',
41
+ attemptToFixPassed: meta['test.test_management.attempt_to_fix_passed'] === 'true',
42
+ }
43
+ }
44
+
45
+ function eventsOfType (events, type) {
46
+ return events.filter(event => event.type === type)
47
+ }
48
+
49
+ function findTestsByIdentity (events, identities, { ignoreSuite = false } = {}) {
50
+ const tests = eventsOfType(events, 'test')
51
+ return tests.filter(test => identities.some(identity => matchesIdentity(test, identity, ignoreSuite)))
52
+ }
53
+
54
+ function matchesIdentity (test, identity, ignoreSuite) {
55
+ if (identity.name && !sameOrEndsWith(test.testName, identity.name)) return false
56
+ if (identity.file && !sameOrEndsWith(test.testSourceFile, identity.file)) return false
57
+ if (!ignoreSuite && identity.suite && !sameOrEndsWith(test.testSuite, identity.suite)) return false
58
+ return Boolean(identity.name)
59
+ }
60
+
61
+ function sameOrEndsWith (actual, expected) {
62
+ if (!actual || !expected) return false
63
+ return actual === expected ||
64
+ actual.endsWith(expected) ||
65
+ expected.endsWith(actual)
66
+ }
67
+
68
+ module.exports = {
69
+ normalizeRequests,
70
+ eventsOfType,
71
+ findTestsByIdentity,
72
+ }