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,369 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
|
|
7
|
+
const { MAX_OUTPUT_BYTES, msgpackToJson } = require('./msgpack-to-json')
|
|
8
|
+
const { projectTestCyclePayload } = require('./payload-projection')
|
|
9
|
+
|
|
10
|
+
const MAX_OUTPUT_FILES = 10_000
|
|
11
|
+
const MAX_SAMPLED_TESTS = 8
|
|
12
|
+
const MAX_SUMMARY_ERRORS = 20
|
|
13
|
+
const SUMMARY_PREFIX = 'DD_TEST_OPTIMIZATION_VALIDATION_V1 '
|
|
14
|
+
|
|
15
|
+
let payloadFileSequence = 0
|
|
16
|
+
|
|
17
|
+
class CiValidationSink {
|
|
18
|
+
#bytesWritten = 0
|
|
19
|
+
#captureMode
|
|
20
|
+
#completionDirectory
|
|
21
|
+
#errors = []
|
|
22
|
+
#eventsObserved = 0
|
|
23
|
+
#eventsRetained = 0
|
|
24
|
+
#fileCount = 0
|
|
25
|
+
#inputs = Object.create(null)
|
|
26
|
+
#outputRoot
|
|
27
|
+
#outputRootDirectory
|
|
28
|
+
#payloadsDirectory
|
|
29
|
+
#processId = crypto.randomBytes(16).toString('hex')
|
|
30
|
+
#sampledLifecycle = new Map()
|
|
31
|
+
#sampledTests = []
|
|
32
|
+
#summaryWritten = false
|
|
33
|
+
#testsDirectory
|
|
34
|
+
#testsObserved = 0
|
|
35
|
+
#testPayloadFileCount = 0
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a bounded payload-file sink under a validator-owned output root.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} outputRoot absolute validator-owned output directory
|
|
41
|
+
* @param {object} [options] sink options
|
|
42
|
+
* @param {'sample'|'strict'} [options.captureMode] evidence retention mode
|
|
43
|
+
*/
|
|
44
|
+
constructor (outputRoot, { captureMode = 'strict' } = {}) {
|
|
45
|
+
if (typeof outputRoot !== 'string' || !path.isAbsolute(outputRoot)) {
|
|
46
|
+
throw new Error('Offline Test Optimization validation output root must be absolute.')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!['sample', 'strict'].includes(captureMode)) {
|
|
50
|
+
throw new Error('Offline Test Optimization validation capture mode is invalid.')
|
|
51
|
+
}
|
|
52
|
+
this.#captureMode = captureMode
|
|
53
|
+
this.#outputRoot = outputRoot
|
|
54
|
+
this.#outputRootDirectory = captureDirectory(outputRoot, 'output root')
|
|
55
|
+
const payloadsRoot = path.join(outputRoot, 'payloads')
|
|
56
|
+
this.#payloadsDirectory = createDirectory(outputRoot, this.#outputRootDirectory, payloadsRoot, 'payloads')
|
|
57
|
+
this.#testsDirectory = createDirectory(
|
|
58
|
+
payloadsRoot,
|
|
59
|
+
this.#payloadsDirectory,
|
|
60
|
+
path.join(payloadsRoot, 'tests'),
|
|
61
|
+
'tests'
|
|
62
|
+
)
|
|
63
|
+
this.#completionDirectory = createDirectory(
|
|
64
|
+
outputRoot,
|
|
65
|
+
this.#outputRootDirectory,
|
|
66
|
+
path.join(outputRoot, 'completions'),
|
|
67
|
+
'completions'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Converts and writes one Test Optimization payload using the Bazel-compatible JSON layout.
|
|
73
|
+
*
|
|
74
|
+
* @param {Buffer} payload encoded Test Optimization payload
|
|
75
|
+
*/
|
|
76
|
+
writeTestCycle (payload) {
|
|
77
|
+
let decoded
|
|
78
|
+
try {
|
|
79
|
+
decoded = JSON.parse(msgpackToJson(payload).toString('utf8'))
|
|
80
|
+
} catch {
|
|
81
|
+
this.#fail('output_payload_decode_failed')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let projected
|
|
86
|
+
try {
|
|
87
|
+
projected = projectTestCyclePayload(decoded)
|
|
88
|
+
} catch {
|
|
89
|
+
this.#fail('output_payload_projection_failed')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.#eventsObserved += projected.events.length
|
|
94
|
+
if (this.#captureMode === 'sample') {
|
|
95
|
+
this.#retainSample(projected.events)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const json = Buffer.from(JSON.stringify(projected))
|
|
100
|
+
if (this.#writePayloadFile(json)) {
|
|
101
|
+
this.#eventsRetained += projected.events.length
|
|
102
|
+
this.#testPayloadFileCount++
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Records whether a control-plane fixture was loaded from the filesystem cache.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} name fixed fixture name
|
|
110
|
+
* @param {Error|undefined|null} error cache-load error
|
|
111
|
+
*/
|
|
112
|
+
writeInputResult (name, error) {
|
|
113
|
+
if (error) this.#addError(`invalid_${name}`)
|
|
114
|
+
this.#inputs[name] = { status: error ? 'error' : 'loaded' }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Records an exporter failure in the bounded validation summary.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} code stable failure code
|
|
121
|
+
*/
|
|
122
|
+
recordError (code) {
|
|
123
|
+
this.#fail(code)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Emits this process's single bounded stderr summary for validator aggregation.
|
|
128
|
+
*/
|
|
129
|
+
writeSummary () {
|
|
130
|
+
if (this.#summaryWritten) return
|
|
131
|
+
this.#summaryWritten = true
|
|
132
|
+
if (this.#captureMode === 'sample') this.#writeSampledEvents()
|
|
133
|
+
const summary = {
|
|
134
|
+
events: this.#eventsRetained,
|
|
135
|
+
payloadFiles: this.#testPayloadFileCount,
|
|
136
|
+
input: 'filesystem-cache',
|
|
137
|
+
inputs: this.#inputs,
|
|
138
|
+
errors: this.#errors,
|
|
139
|
+
}
|
|
140
|
+
if (!this.#writeCompletionRecord()) this.#fail('completion_write_failed')
|
|
141
|
+
process.stderr.write(`${SUMMARY_PREFIX}${JSON.stringify(summary)}\n`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Retains bounded first/late test evidence and the latest lifecycle event of each type.
|
|
146
|
+
*
|
|
147
|
+
* @param {object[]} events projected events
|
|
148
|
+
*/
|
|
149
|
+
#retainSample (events) {
|
|
150
|
+
for (const event of events) {
|
|
151
|
+
if (event.type !== 'test') {
|
|
152
|
+
this.#sampledLifecycle.set(event.type, event)
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
this.#testsObserved++
|
|
156
|
+
if (this.#sampledTests.length < MAX_SAMPLED_TESTS) {
|
|
157
|
+
this.#sampledTests.push(event)
|
|
158
|
+
} else {
|
|
159
|
+
const lateIndex = Math.floor(MAX_SAMPLED_TESTS / 2) +
|
|
160
|
+
(this.#testsObserved % Math.ceil(MAX_SAMPLED_TESTS / 2))
|
|
161
|
+
this.#sampledTests[lateIndex] = event
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Persists the final bounded CI-replay sample after all process-local events have been observed.
|
|
168
|
+
*/
|
|
169
|
+
#writeSampledEvents () {
|
|
170
|
+
const events = [...this.#sampledTests, ...this.#sampledLifecycle.values()]
|
|
171
|
+
if (events.length === 0) return
|
|
172
|
+
const payload = Buffer.from(JSON.stringify({ version: 1, events }))
|
|
173
|
+
if (this.#writePayloadFile(payload)) {
|
|
174
|
+
this.#eventsRetained = events.length
|
|
175
|
+
this.#testPayloadFileCount++
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Atomically publishes this process's bounded completion evidence.
|
|
181
|
+
*
|
|
182
|
+
* @returns {boolean} whether the record was published
|
|
183
|
+
*/
|
|
184
|
+
#writeCompletionRecord () {
|
|
185
|
+
const directoryPath = path.join(this.#outputRoot, 'completions')
|
|
186
|
+
const filename = path.join(directoryPath, `completion-${this.#processId}.json`)
|
|
187
|
+
const temporary = `${filename}.tmp`
|
|
188
|
+
const completion = Buffer.from(JSON.stringify({
|
|
189
|
+
version: 1,
|
|
190
|
+
processId: this.#processId,
|
|
191
|
+
captureMode: this.#captureMode,
|
|
192
|
+
counts: {
|
|
193
|
+
eventsObserved: this.#eventsObserved,
|
|
194
|
+
eventsRetained: this.#eventsRetained,
|
|
195
|
+
payloadFiles: this.#testPayloadFileCount,
|
|
196
|
+
},
|
|
197
|
+
inputs: this.#inputs,
|
|
198
|
+
errors: this.#errors,
|
|
199
|
+
}))
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
assertDirectoryUnchanged(this.#outputRoot, this.#outputRootDirectory, 'output root')
|
|
203
|
+
assertDirectoryUnchanged(directoryPath, this.#completionDirectory, 'completions')
|
|
204
|
+
writeNewFile(temporary, completion)
|
|
205
|
+
assertDirectoryUnchanged(directoryPath, this.#completionDirectory, 'completions')
|
|
206
|
+
fs.renameSync(temporary, filename)
|
|
207
|
+
return true
|
|
208
|
+
} catch {
|
|
209
|
+
removePartialFile(temporary, directoryPath, this.#completionDirectory)
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Writes one completed JSON payload to a unique file.
|
|
216
|
+
*
|
|
217
|
+
* @param {Buffer} payload JSON payload
|
|
218
|
+
* @returns {boolean} whether the payload was written
|
|
219
|
+
*/
|
|
220
|
+
#writePayloadFile (payload) {
|
|
221
|
+
if (this.#fileCount >= MAX_OUTPUT_FILES) {
|
|
222
|
+
this.#fail('output_file_limit_exceeded')
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
if (this.#bytesWritten + payload.length > MAX_OUTPUT_BYTES) {
|
|
226
|
+
this.#fail('output_byte_limit_exceeded')
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const directoryPath = path.join(this.#outputRoot, 'payloads', 'tests')
|
|
231
|
+
const sequence = ++payloadFileSequence
|
|
232
|
+
const timestamp = BigInt(Date.now()) * 1_000_000n + process.hrtime.bigint() % 1_000_000n
|
|
233
|
+
const filename = path.join(
|
|
234
|
+
directoryPath,
|
|
235
|
+
`tests-${this.#processId}-${timestamp}-${process.pid}-${sequence}.json`
|
|
236
|
+
)
|
|
237
|
+
let writeFailed = false
|
|
238
|
+
try {
|
|
239
|
+
assertDirectoryUnchanged(this.#outputRoot, this.#outputRootDirectory, 'output root')
|
|
240
|
+
assertDirectoryUnchanged(path.join(this.#outputRoot, 'payloads'), this.#payloadsDirectory, 'payloads')
|
|
241
|
+
assertDirectoryUnchanged(directoryPath, this.#testsDirectory, 'tests')
|
|
242
|
+
writeNewFile(filename, payload)
|
|
243
|
+
} catch {
|
|
244
|
+
writeFailed = true
|
|
245
|
+
}
|
|
246
|
+
if (writeFailed) {
|
|
247
|
+
this.#fail('output_write_failed')
|
|
248
|
+
removePartialFile(filename, directoryPath, this.#testsDirectory)
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.#bytesWritten += payload.length
|
|
253
|
+
this.#fileCount++
|
|
254
|
+
return true
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Records a sink failure and makes the process unsuccessful.
|
|
259
|
+
*
|
|
260
|
+
* @param {string} code stable failure code
|
|
261
|
+
*/
|
|
262
|
+
#fail (code) {
|
|
263
|
+
this.#addError(code)
|
|
264
|
+
process.exitCode = 1
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Adds one unique bounded summary error.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} code stable failure code
|
|
271
|
+
*/
|
|
272
|
+
#addError (code) {
|
|
273
|
+
if (this.#errors.length >= MAX_SUMMARY_ERRORS || this.#errors.includes(code)) return
|
|
274
|
+
this.#errors.push(code)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Writes one new private regular file and rejects hard links or non-files.
|
|
280
|
+
*
|
|
281
|
+
* @param {string} filename output filename
|
|
282
|
+
* @param {Buffer} payload output bytes
|
|
283
|
+
*/
|
|
284
|
+
function writeNewFile (filename, payload) {
|
|
285
|
+
const flags = fs.constants.O_WRONLY |
|
|
286
|
+
fs.constants.O_CREAT |
|
|
287
|
+
fs.constants.O_EXCL |
|
|
288
|
+
(fs.constants.O_NOFOLLOW || 0)
|
|
289
|
+
const file = fs.openSync(filename, flags, 0o600)
|
|
290
|
+
try {
|
|
291
|
+
const stat = fs.fstatSync(file)
|
|
292
|
+
if (!stat.isFile() || stat.nlink !== 1) {
|
|
293
|
+
throw new Error('Offline Test Optimization output is not a private regular file.')
|
|
294
|
+
}
|
|
295
|
+
fs.writeFileSync(file, payload)
|
|
296
|
+
} finally {
|
|
297
|
+
fs.closeSync(file)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Captures the identity of one existing non-symbolic directory.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} directory directory path
|
|
305
|
+
* @param {string} label directory label
|
|
306
|
+
* @returns {{dev: number, ino: number}} stable directory identity
|
|
307
|
+
*/
|
|
308
|
+
function captureDirectory (directory, label) {
|
|
309
|
+
const stat = fs.lstatSync(directory)
|
|
310
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
311
|
+
throw new Error(`Offline Test Optimization validation ${label} must be a regular directory.`)
|
|
312
|
+
}
|
|
313
|
+
return { dev: stat.dev, ino: stat.ino }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Creates or validates one child directory without accepting symbolic links.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} parent parent directory path
|
|
320
|
+
* @param {{dev: number, ino: number}} parentIdentity expected parent identity
|
|
321
|
+
* @param {string} directory child directory path
|
|
322
|
+
* @param {string} label directory label
|
|
323
|
+
* @returns {{dev: number, ino: number}} stable child identity
|
|
324
|
+
*/
|
|
325
|
+
function createDirectory (parent, parentIdentity, directory, label) {
|
|
326
|
+
assertDirectoryUnchanged(parent, parentIdentity, 'parent output')
|
|
327
|
+
try {
|
|
328
|
+
fs.mkdirSync(directory, { mode: 0o700 })
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error.code !== 'EEXIST') throw error
|
|
331
|
+
}
|
|
332
|
+
return captureDirectory(directory, `${label} output directory`)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Rejects a directory that changed after sink construction.
|
|
337
|
+
*
|
|
338
|
+
* @param {string} directory directory path
|
|
339
|
+
* @param {{dev: number, ino: number}} identity expected directory identity
|
|
340
|
+
* @param {string} label directory label
|
|
341
|
+
*/
|
|
342
|
+
function assertDirectoryUnchanged (directory, identity, label) {
|
|
343
|
+
const current = captureDirectory(directory, label)
|
|
344
|
+
if (current.dev !== identity.dev || current.ino !== identity.ino) {
|
|
345
|
+
throw new Error(`Offline Test Optimization validation ${label} changed during execution.`)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Removes a partially written final path without following symbolic links.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} filename partial payload path
|
|
353
|
+
* @param {string} directory expected parent directory
|
|
354
|
+
* @param {{dev: number, ino: number}} identity expected parent identity
|
|
355
|
+
*/
|
|
356
|
+
function removePartialFile (filename, directory, identity) {
|
|
357
|
+
try {
|
|
358
|
+
assertDirectoryUnchanged(directory, identity, 'partial-file parent')
|
|
359
|
+
const stat = fs.lstatSync(filename)
|
|
360
|
+
if (stat.isFile() || stat.isSymbolicLink()) fs.unlinkSync(filename)
|
|
361
|
+
} catch {}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
CiValidationSink,
|
|
366
|
+
MAX_OUTPUT_BYTES,
|
|
367
|
+
MAX_OUTPUT_FILES,
|
|
368
|
+
SUMMARY_PREFIX,
|
|
369
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { AgentlessCiVisibilityEncoder } = require('../../../encode/agentless-ci-visibility')
|
|
4
|
+
|
|
5
|
+
class CiValidationWriter {
|
|
6
|
+
/**
|
|
7
|
+
* Creates an offline writer that preserves CI Visibility event encoding.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} options writer options
|
|
10
|
+
* @param {object} options.sink bounded validation sink
|
|
11
|
+
* @param {object} options.tags tracer tags
|
|
12
|
+
*/
|
|
13
|
+
constructor ({ sink, tags }) {
|
|
14
|
+
const { 'runtime-id': runtimeId, env, service } = tags
|
|
15
|
+
this._sink = sink
|
|
16
|
+
this._encoder = new AgentlessCiVisibilityEncoder(this, { runtimeId, env, service })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encodes a CI Visibility trace without using an HTTP writer.
|
|
21
|
+
*
|
|
22
|
+
* @param {object[]} trace formatted trace
|
|
23
|
+
*/
|
|
24
|
+
append (trace) {
|
|
25
|
+
this._encoder.encode(trace)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Flushes encoded events synchronously to an offline payload file.
|
|
30
|
+
*
|
|
31
|
+
* @param {Function} [done] completion callback
|
|
32
|
+
*/
|
|
33
|
+
flush (done = () => {}) {
|
|
34
|
+
if (this._encoder.count() > 0) {
|
|
35
|
+
let payload
|
|
36
|
+
try {
|
|
37
|
+
payload = this._encoder.makePayload()
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
|
|
40
|
+
this._encoder.reset()
|
|
41
|
+
this._sink.recordError('output_payload_too_large')
|
|
42
|
+
done()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
this._sink.writeTestCycle(payload)
|
|
46
|
+
}
|
|
47
|
+
done()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds CI event metadata before the next payload is encoded.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} tags metadata grouped by event type
|
|
54
|
+
*/
|
|
55
|
+
addMetadataTags (tags) {
|
|
56
|
+
this._encoder.addMetadataTags(tags)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = CiValidationWriter
|
|
@@ -13,6 +13,7 @@ const { writeSettingsToCache } = require('../test-optimization-cache')
|
|
|
13
13
|
const { CACHE_MISS, TestOptimizationHttpCache } = require('../test-optimization-http-cache')
|
|
14
14
|
const { uploadCoverageReport: uploadCoverageReportRequest } = require('../requests/upload-coverage-report')
|
|
15
15
|
const { uploadTestScreenshot: uploadTestScreenshotRequest } = require('../requests/upload-test-screenshot')
|
|
16
|
+
const { parsers } = require('../../config/parsers')
|
|
16
17
|
const log = require('../../log')
|
|
17
18
|
const BufferingExporter = require('../../exporters/common/buffering-exporter')
|
|
18
19
|
const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags')
|
|
@@ -41,6 +42,7 @@ function getIsTestSessionTrace (trace) {
|
|
|
41
42
|
|
|
42
43
|
const GIT_UPLOAD_TIMEOUT = 60_000 // 60 seconds
|
|
43
44
|
const CAN_USE_CI_VIS_PROTOCOL_TIMEOUT = GIT_UPLOAD_TIMEOUT
|
|
45
|
+
const MAX_COVERAGE_REPORT_FLAGS = 32
|
|
44
46
|
|
|
45
47
|
function appendLogTag (tags, key, value) {
|
|
46
48
|
if (value !== undefined) {
|
|
@@ -71,13 +73,24 @@ function getLogTags (logMessage, { env, version }, gitRepositoryUrl, gitCommitSh
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
class CiVisibilityExporter extends BufferingExporter {
|
|
74
|
-
constructor (config) {
|
|
76
|
+
constructor (config, options = {}) {
|
|
75
77
|
super(config)
|
|
76
78
|
this._timer = undefined
|
|
77
79
|
this._coverageTimer = undefined
|
|
78
80
|
this._logsTimer = undefined
|
|
79
81
|
this._coverageBuffer = []
|
|
80
|
-
this._testOptimizationHttpCache = new TestOptimizationHttpCache()
|
|
82
|
+
this._testOptimizationHttpCache = options.testOptimizationHttpCache || new TestOptimizationHttpCache()
|
|
83
|
+
this._isTestOptimizationCacheOnly = options.cacheOnly === true
|
|
84
|
+
const coverageReportFlags = parsers.ARRAY(config?.testOptimization?.DD_CODE_COVERAGE_FLAGS)
|
|
85
|
+
if (coverageReportFlags?.length > MAX_COVERAGE_REPORT_FLAGS) {
|
|
86
|
+
log.warn(
|
|
87
|
+
'Maximum of %d coverage report flags allowed, but %d flags were provided. Omitting coverage report flags.',
|
|
88
|
+
MAX_COVERAGE_REPORT_FLAGS,
|
|
89
|
+
coverageReportFlags.length
|
|
90
|
+
)
|
|
91
|
+
} else if (coverageReportFlags?.length) {
|
|
92
|
+
this._coverageReportFlags = [...coverageReportFlags]
|
|
93
|
+
}
|
|
81
94
|
// The library can use new features like ITR and test suite level visibility
|
|
82
95
|
// AKA CI Vis Protocol
|
|
83
96
|
this._canUseCiVisProtocol = false
|
|
@@ -181,6 +194,9 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
181
194
|
const { skippableSuites, correlationId, coverage } = cachedSkippableSuites
|
|
182
195
|
return callback(null, skippableSuites, correlationId, coverage)
|
|
183
196
|
}
|
|
197
|
+
if (this._isTestOptimizationCacheOnly) {
|
|
198
|
+
return callback(this._getCacheOnlyError('skippable tests'), [])
|
|
199
|
+
}
|
|
184
200
|
|
|
185
201
|
this._gitUploadPromise.then(gitUploadError => {
|
|
186
202
|
if (gitUploadError) {
|
|
@@ -198,6 +214,9 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
198
214
|
if (cachedKnownTests !== CACHE_MISS) {
|
|
199
215
|
return callback(null, cachedKnownTests)
|
|
200
216
|
}
|
|
217
|
+
if (this._isTestOptimizationCacheOnly) {
|
|
218
|
+
return callback(this._getCacheOnlyError('known tests'))
|
|
219
|
+
}
|
|
201
220
|
getKnownTestsRequest(this.getRequestConfiguration(testConfiguration), callback)
|
|
202
221
|
}
|
|
203
222
|
|
|
@@ -209,6 +228,9 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
209
228
|
if (cachedTestManagementTests !== CACHE_MISS) {
|
|
210
229
|
return callback(null, cachedTestManagementTests)
|
|
211
230
|
}
|
|
231
|
+
if (this._isTestOptimizationCacheOnly) {
|
|
232
|
+
return callback(this._getCacheOnlyError('test management tests'))
|
|
233
|
+
}
|
|
212
234
|
getTestManagementTestsRequest(this.getRequestConfiguration(testConfiguration), callback)
|
|
213
235
|
}
|
|
214
236
|
|
|
@@ -241,6 +263,10 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
241
263
|
return callback(null, this._libraryConfig)
|
|
242
264
|
}
|
|
243
265
|
|
|
266
|
+
if (this._isTestOptimizationCacheOnly) {
|
|
267
|
+
return callback(this._getCacheOnlyError('settings'), {})
|
|
268
|
+
}
|
|
269
|
+
|
|
244
270
|
this.sendGitMetadata(repositoryUrl)
|
|
245
271
|
getLibraryConfigurationRequest(configuration, (err, libraryConfig) => {
|
|
246
272
|
/**
|
|
@@ -270,6 +296,17 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
270
296
|
})
|
|
271
297
|
}
|
|
272
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Returns the deterministic cache error for offline exporters.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} input required cache input
|
|
303
|
+
* @returns {Error} cache error
|
|
304
|
+
*/
|
|
305
|
+
_getCacheOnlyError (input) {
|
|
306
|
+
return this._testOptimizationHttpCache.getLastError?.() ||
|
|
307
|
+
new Error(`Offline Test Optimization validation requires a valid ${input} cache fixture.`)
|
|
308
|
+
}
|
|
309
|
+
|
|
273
310
|
// Takes into account potential kill switches
|
|
274
311
|
filterConfiguration (remoteConfiguration) {
|
|
275
312
|
if (!remoteConfiguration) {
|
|
@@ -487,7 +524,7 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
487
524
|
* @param {string} options.filePath - Path to the coverage report file
|
|
488
525
|
* @param {string} options.format - Format of the coverage report
|
|
489
526
|
* @param {object} options.testEnvironmentMetadata - Test environment metadata containing git/CI tags
|
|
490
|
-
* @param {
|
|
527
|
+
* @param {(error: Error|null) => void} callback - Callback function
|
|
491
528
|
*/
|
|
492
529
|
uploadCoverageReport ({ filePath, format, testEnvironmentMetadata }, callback) {
|
|
493
530
|
if (!this._codeCoverageReportUrl) {
|
|
@@ -497,6 +534,7 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
497
534
|
uploadCoverageReportRequest({
|
|
498
535
|
filePath,
|
|
499
536
|
format,
|
|
537
|
+
flags: this._coverageReportFlags,
|
|
500
538
|
testEnvironmentMetadata,
|
|
501
539
|
url: this._codeCoverageReportUrl,
|
|
502
540
|
isEvpProxy: !!this._isUsingEvpProxy,
|
|
@@ -22,11 +22,16 @@ function parseJsonResponse (rawJson) {
|
|
|
22
22
|
|
|
23
23
|
function parseSkippableSuitesResponse (
|
|
24
24
|
rawJson,
|
|
25
|
-
{
|
|
25
|
+
{
|
|
26
|
+
testLevel = 'suite',
|
|
27
|
+
isCoverageReportUploadEnabled = false,
|
|
28
|
+
validateRequiredFields = false,
|
|
29
|
+
validationMode = false,
|
|
30
|
+
} = {}
|
|
26
31
|
) {
|
|
27
32
|
const parsedResponse = parseJsonResponse(rawJson)
|
|
28
33
|
if (validateRequiredFields) {
|
|
29
|
-
validateSkippableTestsResponse(parsedResponse)
|
|
34
|
+
validateSkippableTestsResponse(parsedResponse, { validationMode })
|
|
30
35
|
}
|
|
31
36
|
const coverage = {}
|
|
32
37
|
for (const [filename, bitmap] of Object.entries(parsedResponse.meta?.coverage || {})) {
|
|
@@ -26,7 +26,7 @@ function parseJsonResponse (rawJson) {
|
|
|
26
26
|
function parseLibraryConfigurationResponse (rawJson, config = getConfig(), options = {}) {
|
|
27
27
|
const parsedResponse = parseJsonResponse(rawJson)
|
|
28
28
|
if (options.validateRequiredFields) {
|
|
29
|
-
validateSettingsResponse(parsedResponse)
|
|
29
|
+
validateSettingsResponse(parsedResponse, options)
|
|
30
30
|
}
|
|
31
31
|
const {
|
|
32
32
|
code_coverage: isCodeCoverageEnabled,
|
|
@@ -24,14 +24,15 @@ const UPLOAD_TIMEOUT_MS = 30_000
|
|
|
24
24
|
* @param {object} options - Upload options
|
|
25
25
|
* @param {string} options.filePath - Path to the coverage report file
|
|
26
26
|
* @param {string} options.format - Format of the coverage report (e.g., 'lcov', 'cobertura')
|
|
27
|
+
* @param {string[]} [options.flags] - Optional coverage report grouping flags
|
|
27
28
|
* @param {object} options.testEnvironmentMetadata - Test environment metadata containing git/CI tags
|
|
28
29
|
* @param {URL} options.url - The base URL for the coverage report upload
|
|
29
30
|
* @param {boolean} [options.isEvpProxy] - Whether to use EVP proxy for the upload
|
|
30
31
|
* @param {string} [options.evpProxyPrefix] - The EVP proxy prefix (e.g., '/evp_proxy/v4')
|
|
31
|
-
* @param {
|
|
32
|
+
* @param {(error: Error|null) => void} callback - Callback function
|
|
32
33
|
*/
|
|
33
34
|
function uploadCoverageReport (
|
|
34
|
-
{ filePath, format, testEnvironmentMetadata, url, isEvpProxy, evpProxyPrefix },
|
|
35
|
+
{ filePath, format, flags, testEnvironmentMetadata, url, isEvpProxy, evpProxyPrefix },
|
|
35
36
|
callback
|
|
36
37
|
) {
|
|
37
38
|
const { DD_API_KEY } = getConfig()
|
|
@@ -54,6 +55,9 @@ function uploadCoverageReport (
|
|
|
54
55
|
format,
|
|
55
56
|
...testEnvironmentMetadata,
|
|
56
57
|
}
|
|
58
|
+
if (flags?.length) {
|
|
59
|
+
eventPayload['report.flags'] = flags
|
|
60
|
+
}
|
|
57
61
|
|
|
58
62
|
// Create multipart form
|
|
59
63
|
const form = new FormData()
|
|
@@ -28,12 +28,16 @@ class TestApiManualPlugin extends CiPlugin {
|
|
|
28
28
|
const store = legacyStorage.getStore()
|
|
29
29
|
const testSpan = store && store.span
|
|
30
30
|
if (testSpan) {
|
|
31
|
+
const previousStore = testSpan._store === undefined
|
|
32
|
+
? undefined
|
|
33
|
+
: legacyStorage.getStore(testSpan._store)
|
|
31
34
|
testSpan.setTag(TEST_STATUS, status)
|
|
32
35
|
if (error) {
|
|
33
36
|
testSpan.setTag('error', error)
|
|
34
37
|
}
|
|
35
38
|
testSpan.finish()
|
|
36
39
|
finishAllTraceSpans(testSpan)
|
|
40
|
+
legacyStorage.enterWith(previousStore)
|
|
37
41
|
}
|
|
38
42
|
})
|
|
39
43
|
this.unconfiguredAddSub('dd-trace:ci:manual:test:addTags', (tags) => {
|
|
@@ -47,7 +47,7 @@ function parseJsonResponse (rawJson) {
|
|
|
47
47
|
function parseTestManagementTestsResponse (rawJson, options = {}) {
|
|
48
48
|
const parsedResponse = parseJsonResponse(rawJson)
|
|
49
49
|
if (options.validateRequiredFields) {
|
|
50
|
-
validateTestManagementTestsResponse(parsedResponse)
|
|
50
|
+
validateTestManagementTestsResponse(parsedResponse, options)
|
|
51
51
|
}
|
|
52
52
|
const { data: { attributes: { modules: testManagementTests } } } = parsedResponse
|
|
53
53
|
return testManagementTests
|