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.
- package/README.electron.md +7 -0
- package/README.md +17 -0
- package/ci/diagnose.js +2100 -0
- package/ci/init.js +23 -2
- package/ci/runbook.md +198 -0
- package/ci/test-optimization-validation/approval-artifacts.js +143 -0
- package/ci/test-optimization-validation/approval.js +299 -0
- package/ci/test-optimization-validation/artifact-id.js +20 -0
- package/ci/test-optimization-validation/bounded-json.js +121 -0
- package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
- package/ci/test-optimization-validation/ci-discovery.js +181 -0
- package/ci/test-optimization-validation/ci-remediation.js +210 -0
- package/ci/test-optimization-validation/cli.js +903 -0
- package/ci/test-optimization-validation/command-blocker.js +81 -0
- package/ci/test-optimization-validation/command-output-policy.js +206 -0
- package/ci/test-optimization-validation/command-runner.js +707 -0
- package/ci/test-optimization-validation/command-suitability.js +471 -0
- package/ci/test-optimization-validation/executable-approval.js +48 -0
- package/ci/test-optimization-validation/executable.js +480 -0
- package/ci/test-optimization-validation/generated-file-policy.js +63 -0
- package/ci/test-optimization-validation/generated-files.js +351 -0
- package/ci/test-optimization-validation/generated-verifier.js +196 -0
- package/ci/test-optimization-validation/init-probe-preload.js +163 -0
- package/ci/test-optimization-validation/init-probe.js +246 -0
- package/ci/test-optimization-validation/late-initialization.js +63 -0
- package/ci/test-optimization-validation/local-command.js +163 -0
- package/ci/test-optimization-validation/manifest-loader.js +133 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
- package/ci/test-optimization-validation/manifest-schema.js +862 -0
- package/ci/test-optimization-validation/offline-fixtures.js +327 -0
- package/ci/test-optimization-validation/offline-output.js +406 -0
- package/ci/test-optimization-validation/payload-normalizer.js +72 -0
- package/ci/test-optimization-validation/plan-writer.js +1120 -0
- package/ci/test-optimization-validation/preflight-runner.js +114 -0
- package/ci/test-optimization-validation/redaction.js +331 -0
- package/ci/test-optimization-validation/report-writer.js +1508 -0
- package/ci/test-optimization-validation/safe-files.js +203 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
- package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
- package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
- package/ci/test-optimization-validation/setup-runner.js +97 -0
- package/ci/test-optimization-validation/static-diagnosis.js +159 -0
- package/ci/test-optimization-validation/test-output.js +129 -0
- package/ci/test-optimization-validation-manifest.schema.json +1 -0
- package/ci/validate-test-optimization.js +3 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +121 -8
- package/init.js +18 -0
- package/initialize.mjs +1 -1
- package/loader-hook.mjs +28 -18
- package/openfeature.d.ts +1 -0
- package/openfeature.js +4 -0
- package/package.json +10 -7
- package/packages/datadog-instrumentations/src/ai.js +8 -2
- package/packages/datadog-instrumentations/src/child_process.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
- package/packages/datadog-instrumentations/src/cucumber.js +20 -7
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
- package/packages/datadog-instrumentations/src/jest.js +26 -0
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
- package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
- package/packages/datadog-instrumentations/src/mongodb.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +24 -0
- package/packages/datadog-instrumentations/src/nyc.js +0 -2
- package/packages/datadog-instrumentations/src/oracledb.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +159 -22
- package/packages/datadog-instrumentations/src/process.js +3 -0
- package/packages/datadog-instrumentations/src/router.js +18 -8
- package/packages/datadog-instrumentations/src/selenium.js +52 -26
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
- package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
- package/packages/datadog-instrumentations/src/ws.js +2 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
- package/packages/datadog-plugin-cypress/src/index.js +6 -1
- package/packages/datadog-plugin-cypress/src/support.js +92 -26
- package/packages/datadog-plugin-electron/src/ipc.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +87 -45
- package/packages/datadog-plugin-graphql/src/validate.js +2 -2
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +177 -64
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-ws/src/close.js +2 -1
- package/packages/datadog-plugin-ws/src/producer.js +2 -1
- package/packages/datadog-plugin-ws/src/receiver.js +2 -1
- package/packages/datadog-plugin-ws/src/server.js +2 -1
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
- package/packages/dd-trace/src/azure_metadata.js +2 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
- package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +66 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
- package/packages/dd-trace/src/exporters/common/request.js +59 -30
- package/packages/dd-trace/src/exporters/common/url.js +15 -1
- package/packages/dd-trace/src/feature-registry.js +10 -3
- package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
- package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
- package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
- package/packages/dd-trace/src/llmobs/noop.js +6 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
- package/packages/dd-trace/src/llmobs/sdk.js +17 -0
- package/packages/dd-trace/src/llmobs/tagger.js +68 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
- package/packages/dd-trace/src/llmobs/util.js +32 -0
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
- package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
- package/packages/dd-trace/src/openfeature/index.js +0 -2
- package/packages/dd-trace/src/openfeature/noop.js +1 -28
- package/packages/dd-trace/src/openfeature/register.js +16 -27
- package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
- package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
- package/packages/dd-trace/src/plugins/util/git.js +3 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
- package/packages/dd-trace/src/plugins/util/test.js +56 -4
- package/packages/dd-trace/src/proxy.js +84 -7
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -14
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +26 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
|
@@ -0,0 +1,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
|
+
}
|