dd-trace 6.13.0 → 6.14.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/LICENSE-3rdparty.csv +3 -1
- package/ci/init.js +8 -3
- package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
- package/ci/test-optimization-validation/ci-remediation.js +6 -6
- package/ci/test-optimization-validation/command-blocker.js +7 -5
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
- package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
- package/ci/test-optimization-validation/package-check.js +1 -1
- package/ci/test-optimization-validation/report-writer.js +7 -5
- package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
- package/ci/vitest-no-worker-init-setup.mjs +13 -13
- package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
- package/package.json +13 -10
- package/packages/datadog-esbuild/index.js +3 -1
- package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
- package/packages/datadog-instrumentations/src/fs.js +24 -20
- package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
- package/packages/datadog-instrumentations/src/graphql.js +16 -16
- package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +265 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +13 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +251 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +11 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
- package/packages/datadog-instrumentations/src/jest.js +25 -20
- package/packages/datadog-instrumentations/src/knex.js +76 -0
- package/packages/datadog-instrumentations/src/mariadb.js +6 -0
- package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
- package/packages/datadog-instrumentations/src/mocha/main.js +25 -15
- package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
- package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
- package/packages/datadog-instrumentations/src/net.js +14 -17
- package/packages/datadog-instrumentations/src/next.js +212 -62
- package/packages/datadog-instrumentations/src/openai.js +42 -40
- package/packages/datadog-instrumentations/src/pg.js +15 -0
- package/packages/datadog-instrumentations/src/playwright.js +109 -13
- package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
- package/packages/datadog-instrumentations/src/sequelize.js +94 -2
- package/packages/datadog-instrumentations/src/undici.js +53 -4
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +37 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
- package/packages/datadog-instrumentations/src/vitest-worker.js +71 -14
- package/packages/datadog-instrumentations/src/webdriverio.js +965 -6
- package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
- package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
- package/packages/datadog-plugin-amqp10/src/util.js +2 -1
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
- package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
- package/packages/datadog-plugin-cucumber/src/index.js +0 -2
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
- package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
- package/packages/datadog-plugin-graphql/src/execute.js +998 -196
- package/packages/datadog-plugin-graphql/src/index.js +27 -7
- package/packages/datadog-plugin-graphql/src/jit.js +136 -0
- package/packages/datadog-plugin-graphql/src/request.js +18 -27
- package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
- package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
- package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
- package/packages/datadog-plugin-graphql/src/utils.js +208 -120
- package/packages/datadog-plugin-graphql/src/validate.js +10 -23
- package/packages/datadog-plugin-jest/src/index.js +0 -6
- package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
- package/packages/datadog-plugin-mocha/src/index.js +403 -75
- package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
- package/packages/datadog-plugin-mysql/src/index.js +41 -15
- package/packages/datadog-plugin-net/src/tcp.js +3 -1
- package/packages/datadog-plugin-next/src/index.js +63 -29
- package/packages/datadog-plugin-playwright/src/index.js +167 -27
- package/packages/datadog-plugin-undici/src/index.js +27 -41
- package/packages/datadog-plugin-vitest/src/index.js +6 -12
- package/packages/dd-trace/src/aiguard/evaluation.js +2 -1
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +8 -1
- package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
- package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
- package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
- package/packages/dd-trace/src/appsec/graphql.js +6 -2
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
- package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +11 -36
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/lambda.js +44 -26
- package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +5 -173
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +394 -225
- package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
- package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
- package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
- package/packages/dd-trace/src/ci-visibility/final-flush.js +2 -1
- package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
- package/packages/dd-trace/src/ci-visibility/requests/video-request.js +176 -0
- package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
- package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
- package/packages/dd-trace/src/config/index.js +73 -13
- package/packages/dd-trace/src/config/parsers.js +11 -1
- package/packages/dd-trace/src/config/stable.js +3 -3
- package/packages/dd-trace/src/config/supported-configurations.json +20 -2
- package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
- package/packages/dd-trace/src/debugger/config.js +12 -1
- package/packages/dd-trace/src/debugger/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/request-options.js +33 -0
- package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -40
- package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
- package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
- package/packages/dd-trace/src/debugger/index.js +23 -3
- package/packages/dd-trace/src/dogstatsd.js +510 -76
- package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
- package/packages/dd-trace/src/encode/0.4.js +5 -5
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
- package/packages/dd-trace/src/evp_proxy/direct.js +27 -23
- package/packages/dd-trace/src/exporters/agent/index.js +2 -1
- package/packages/dd-trace/src/exporters/agent/writer.js +24 -7
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
- package/packages/dd-trace/src/exporters/agentless/writer.js +100 -118
- package/packages/dd-trace/src/exporters/common/agents.js +28 -6
- package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
- package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
- package/packages/dd-trace/src/exporters/common/request.js +31 -11
- package/packages/dd-trace/src/exporters/common/url.js +40 -1
- package/packages/dd-trace/src/exporters/common/writer.js +9 -7
- package/packages/dd-trace/src/flush.js +6 -3
- package/packages/dd-trace/src/id.js +14 -0
- package/packages/dd-trace/src/index.js +5 -2
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +2 -1
- package/packages/dd-trace/src/llmobs/index.js +32 -32
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
- package/packages/dd-trace/src/llmobs/util.js +0 -33
- package/packages/dd-trace/src/llmobs/writers/base.js +49 -32
- package/packages/dd-trace/src/log/index.js +7 -1
- package/packages/dd-trace/src/{ci-visibility/log-submission → log-submission}/log-submission-plugin.js +48 -19
- package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
- package/packages/dd-trace/src/opentelemetry/logs/index.js +5 -24
- package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +15 -22
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +14 -4
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +10 -0
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
- package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
- package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +93 -18
- package/packages/dd-trace/src/payload-tagging/index.js +2 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
- package/packages/dd-trace/src/plugins/tracing.js +12 -10
- package/packages/dd-trace/src/plugins/util/ci.js +3 -1
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
- package/packages/dd-trace/src/plugins/util/test.js +83 -20
- package/packages/dd-trace/src/plugins/util/url.js +2 -2
- package/packages/dd-trace/src/plugins/util/web.js +4 -9
- package/packages/dd-trace/src/profiling/config.js +1 -0
- package/packages/dd-trace/src/profiling/oom.js +7 -2
- package/packages/dd-trace/src/profiling/profiler.js +23 -13
- package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
- package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
- package/packages/dd-trace/src/proxy.js +75 -5
- package/packages/dd-trace/src/remote_config/index.js +319 -101
- package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
- package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -1
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -1
- package/packages/dd-trace/src/sampling_rule.js +36 -9
- package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +4 -8
- package/packages/dd-trace/src/serverless/vercel.js +53 -20
- package/packages/dd-trace/src/serverless.js +10 -3
- package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
- package/packages/dd-trace/src/span_stats.js +24 -28
- package/packages/dd-trace/src/startup-log.js +3 -2
- package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
- package/packages/dd-trace/src/telemetry/send-data.js +18 -16
- package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
- package/packages/dd-trace/src/tracer.js +4 -5
- package/packages/dd-trace/src/tracer_metadata.js +11 -3
- 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/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
- package/packages/dd-trace/src/encode/agentless-json.js +0 -210
|
@@ -50,13 +50,25 @@ function getLockPath (cacheKey) {
|
|
|
50
50
|
return path.join(tmpdir(), `dd-${cacheKey}.lock`)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Checks whether a filesystem cache timestamp can be safely compared with the current time.
|
|
55
|
+
*
|
|
56
|
+
* @param {unknown} timestamp - Timestamp read from a cache or lock file.
|
|
57
|
+
* @param {number} now - Current Unix timestamp in milliseconds.
|
|
58
|
+
* @returns {timestamp is number}
|
|
59
|
+
*/
|
|
60
|
+
function isValidTimestamp (timestamp, now) {
|
|
61
|
+
return Number.isSafeInteger(timestamp) && timestamp >= 0 && timestamp <= now
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
/**
|
|
54
65
|
* Attempts to read cached data from the filesystem.
|
|
55
66
|
*
|
|
56
67
|
* @param {string} cacheKey
|
|
68
|
+
* @param {number} [cacheTtlMs] - Maximum cache age in milliseconds.
|
|
57
69
|
* @returns {{ data: unknown } | undefined}
|
|
58
70
|
*/
|
|
59
|
-
function readFromCache (cacheKey) {
|
|
71
|
+
function readFromCache (cacheKey, cacheTtlMs = CACHE_TTL_MS) {
|
|
60
72
|
const cachePath = getCachePath(cacheKey)
|
|
61
73
|
try {
|
|
62
74
|
const raw = fs.readFileSync(cachePath, 'utf8')
|
|
@@ -66,8 +78,14 @@ function readFromCache (cacheKey) {
|
|
|
66
78
|
return
|
|
67
79
|
}
|
|
68
80
|
const { timestamp, data } = parsed
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
const now = Date.now()
|
|
82
|
+
if (!isValidTimestamp(timestamp, now)) {
|
|
83
|
+
log.debug('%s cache file has an invalid timestamp, ignoring', cacheKey)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const age = now - timestamp
|
|
87
|
+
if (age > cacheTtlMs) {
|
|
88
|
+
log.debug('%s cache expired (age: %d ms)', cacheKey, age)
|
|
71
89
|
return
|
|
72
90
|
}
|
|
73
91
|
log.debug('%s cache hit', cacheKey)
|
|
@@ -101,7 +119,7 @@ function writeToCache (cacheKey, data) {
|
|
|
101
119
|
* Attempts to acquire an exclusive lock using O_CREAT|O_EXCL.
|
|
102
120
|
*
|
|
103
121
|
* @param {string} cacheKey
|
|
104
|
-
* @returns {boolean}
|
|
122
|
+
* @returns {boolean|undefined}
|
|
105
123
|
*/
|
|
106
124
|
function tryAcquireLock (cacheKey) {
|
|
107
125
|
const lockPath = getLockPath(cacheKey)
|
|
@@ -110,8 +128,9 @@ function tryAcquireLock (cacheKey) {
|
|
|
110
128
|
fs.writeSync(fd, String(Date.now()))
|
|
111
129
|
fs.closeSync(fd)
|
|
112
130
|
return true
|
|
113
|
-
} catch {
|
|
114
|
-
return false
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (err.code === 'EEXIST') return false
|
|
133
|
+
log.debug('%s lock cannot be created, bypassing cache: %s', cacheKey, err.message)
|
|
115
134
|
}
|
|
116
135
|
}
|
|
117
136
|
|
|
@@ -119,9 +138,15 @@ function tryAcquireLock (cacheKey) {
|
|
|
119
138
|
* Removes the lock file.
|
|
120
139
|
*
|
|
121
140
|
* @param {string} cacheKey
|
|
141
|
+
* @returns {boolean}
|
|
122
142
|
*/
|
|
123
143
|
function releaseLock (cacheKey) {
|
|
124
|
-
try {
|
|
144
|
+
try {
|
|
145
|
+
fs.unlinkSync(getLockPath(cacheKey))
|
|
146
|
+
return true
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return err.code === 'ENOENT'
|
|
149
|
+
}
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
/**
|
|
@@ -165,7 +190,9 @@ function startLockHeartbeat (cacheKey) {
|
|
|
165
190
|
function isLockStale (cacheKey) {
|
|
166
191
|
try {
|
|
167
192
|
const content = fs.readFileSync(getLockPath(cacheKey), 'utf8')
|
|
168
|
-
|
|
193
|
+
const timestamp = Number(content)
|
|
194
|
+
const now = Date.now()
|
|
195
|
+
return !isValidTimestamp(timestamp, now) || now - timestamp > CACHE_LOCK_TIMEOUT_MS
|
|
169
196
|
} catch {
|
|
170
197
|
return true
|
|
171
198
|
}
|
|
@@ -175,30 +202,37 @@ function isLockStale (cacheKey) {
|
|
|
175
202
|
* Polls until the cache file appears or the timeout is reached.
|
|
176
203
|
*
|
|
177
204
|
* @param {string} cacheKey
|
|
178
|
-
* @param {Function} fetchFn - function(done) that fetches from the API
|
|
205
|
+
* @param {Function} fetchFn - function(cacheKey, done) that fetches from the API
|
|
179
206
|
* @param {Function} done - callback(err, ...results)
|
|
207
|
+
* @param {number} cacheTtlMs - Maximum cache age in milliseconds.
|
|
180
208
|
*/
|
|
181
|
-
function waitForCache (cacheKey, fetchFn, done) {
|
|
209
|
+
function waitForCache (cacheKey, fetchFn, done, cacheTtlMs) {
|
|
182
210
|
const poll = () => {
|
|
183
|
-
const cached = readFromCache(cacheKey)
|
|
211
|
+
const cached = readFromCache(cacheKey, cacheTtlMs)
|
|
184
212
|
if (cached) {
|
|
185
213
|
return done(null, cached.data)
|
|
186
214
|
}
|
|
187
215
|
if (isLockStale(cacheKey)) {
|
|
188
216
|
log.debug('%s lock is stale, attempting takeover', cacheKey)
|
|
189
|
-
releaseLock(cacheKey)
|
|
190
|
-
|
|
217
|
+
if (!releaseLock(cacheKey)) {
|
|
218
|
+
return fetchFn(null, done)
|
|
219
|
+
}
|
|
220
|
+
const isLockOwner = tryAcquireLock(cacheKey)
|
|
221
|
+
if (isLockOwner === undefined) {
|
|
222
|
+
return fetchFn(null, done)
|
|
223
|
+
}
|
|
224
|
+
if (!isLockOwner) {
|
|
191
225
|
return setTimeout(poll, CACHE_LOCK_POLL_MS)
|
|
192
226
|
}
|
|
193
227
|
|
|
194
|
-
const cachedAfterTakeover = readFromCache(cacheKey)
|
|
228
|
+
const cachedAfterTakeover = readFromCache(cacheKey, cacheTtlMs)
|
|
195
229
|
if (cachedAfterTakeover) {
|
|
196
230
|
releaseLock(cacheKey)
|
|
197
231
|
return done(null, cachedAfterTakeover.data)
|
|
198
232
|
}
|
|
199
233
|
|
|
200
234
|
const stopHeartbeat = startLockHeartbeat(cacheKey)
|
|
201
|
-
return fetchFn((err, ...results) => {
|
|
235
|
+
return fetchFn(cacheKey, (err, ...results) => {
|
|
202
236
|
stopHeartbeat()
|
|
203
237
|
done(err, ...results)
|
|
204
238
|
})
|
|
@@ -218,14 +252,15 @@ function waitForCache (cacheKey, fetchFn, done) {
|
|
|
218
252
|
* @param {Function} fetchFn - function(cacheKey, done) that performs the API request.
|
|
219
253
|
* Must call writeToCache(cacheKey, data) on success before calling done(null, data).
|
|
220
254
|
* @param {Function} done - callback(err, ...results)
|
|
255
|
+
* @param {number} [cacheTtlMs] - Maximum cache age in milliseconds.
|
|
221
256
|
*/
|
|
222
|
-
function withCache (cacheKey, fetchFn, done) {
|
|
257
|
+
function withCache (cacheKey, fetchFn, done, cacheTtlMs = CACHE_TTL_MS) {
|
|
223
258
|
if (!isCacheEnabled()) {
|
|
224
259
|
return fetchFn(null, done)
|
|
225
260
|
}
|
|
226
261
|
|
|
227
262
|
// Fast path: cache hit
|
|
228
|
-
const cached = readFromCache(cacheKey)
|
|
263
|
+
const cached = readFromCache(cacheKey, cacheTtlMs)
|
|
229
264
|
if (cached) {
|
|
230
265
|
return done(null, cached.data)
|
|
231
266
|
}
|
|
@@ -233,9 +268,13 @@ function withCache (cacheKey, fetchFn, done) {
|
|
|
233
268
|
// Try to become the fetcher (lock owner)
|
|
234
269
|
const isLockOwner = tryAcquireLock(cacheKey)
|
|
235
270
|
|
|
271
|
+
if (isLockOwner === undefined) {
|
|
272
|
+
return fetchFn(null, done)
|
|
273
|
+
}
|
|
274
|
+
|
|
236
275
|
if (!isLockOwner) {
|
|
237
276
|
log.debug('%s lock held by another process, waiting for cache', cacheKey)
|
|
238
|
-
return waitForCache(cacheKey,
|
|
277
|
+
return waitForCache(cacheKey, fetchFn, done, cacheTtlMs)
|
|
239
278
|
}
|
|
240
279
|
|
|
241
280
|
// This process owns the lock — start heartbeat and fetch
|
|
@@ -14,7 +14,6 @@ const {
|
|
|
14
14
|
TELEMETRY_GIT_REQUESTS_SETTINGS_ERRORS,
|
|
15
15
|
TELEMETRY_GIT_REQUESTS_SETTINGS_RESPONSE,
|
|
16
16
|
} = require('../telemetry')
|
|
17
|
-
const { writeSettingsToCache } = require('../test-optimization-cache')
|
|
18
17
|
const { MAX_RETRIES, validateSettingsResponse } = require('../test-optimization-http-cache-schema')
|
|
19
18
|
const request = require('./request')
|
|
20
19
|
|
|
@@ -286,8 +285,6 @@ function getLibraryConfiguration ({
|
|
|
286
285
|
|
|
287
286
|
incrementCountMetric(TELEMETRY_GIT_REQUESTS_SETTINGS_RESPONSE, settings)
|
|
288
287
|
|
|
289
|
-
writeSettingsToCache(settings)
|
|
290
|
-
|
|
291
288
|
done(null, settings)
|
|
292
289
|
} catch (err) {
|
|
293
290
|
done(err)
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { readFileSync } = require('node:fs')
|
|
3
|
+
const { createReadStream, readFileSync, statSync } = require('node:fs')
|
|
4
4
|
const { extname } = require('node:path')
|
|
5
5
|
|
|
6
6
|
const getConfig = require('../../config')
|
|
7
7
|
const { EVP_SUBDOMAIN_HEADER_NAME } = require('../../evp_proxy/constants')
|
|
8
8
|
const { joinEVPProxyPath } = require('../../evp_proxy/path')
|
|
9
9
|
const log = require('../../log')
|
|
10
|
+
const { getAgent, getMediaAgent } = require('../exporters/agents')
|
|
10
11
|
const request = require('../exporters/request')
|
|
12
|
+
const videoRequest = require('./video-request')
|
|
11
13
|
|
|
12
14
|
const UPLOAD_TIMEOUT_MS = 30_000
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
+
const TEST_RUN_MEDIA_ENDPOINT_PREFIX = '/api/v2/ci/test-runs/'
|
|
16
|
+
const TEST_SUITE_MEDIA_ENDPOINT_PREFIX = '/api/v2/ci/test-suites/'
|
|
17
|
+
const MEDIA_ENDPOINT_SUFFIX = '/media'
|
|
18
|
+
const MAX_VIDEO_SIZE_BYTES = 200 * 1024 * 1024
|
|
15
19
|
const UINT64_MAX = 18_446_744_073_709_551_615n
|
|
16
20
|
|
|
17
|
-
function getContentType (filePath) {
|
|
18
|
-
const extension = extname(filePath).toLowerCase()
|
|
21
|
+
function getContentType (filePath, kind) {
|
|
22
|
+
const extension = filePath ? extname(filePath).toLowerCase() : ''
|
|
23
|
+
if (kind === 'video') {
|
|
24
|
+
return extension === '.mp4' ? 'video/mp4' : 'video/webm'
|
|
25
|
+
}
|
|
19
26
|
if (extension === '.gif') {
|
|
20
27
|
return 'image/gif'
|
|
21
28
|
}
|
|
@@ -28,135 +35,221 @@ function getContentType (filePath) {
|
|
|
28
35
|
return 'image/png'
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
function
|
|
32
|
-
if (!/^[1-9]\d*$/.test(
|
|
38
|
+
function isValidId (value) {
|
|
39
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
33
40
|
return false
|
|
34
41
|
}
|
|
35
|
-
return BigInt(
|
|
42
|
+
return BigInt(value) <= UINT64_MAX
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
|
-
* Renders
|
|
40
|
-
* the upload's query string. The Agent's evp_proxy validates the forwarded query against a
|
|
41
|
-
* restrictive charset and rejects a raw Cypress filename (spaces, parens, non-ASCII), so the
|
|
42
|
-
* filename part is hex-encoded to [0-9a-f]; the decimal trace id and ':' separator are already in
|
|
43
|
-
* the allowed set and stay readable. Deterministic, so a retried upload reproduces the same key
|
|
44
|
-
* and the backend's UUIDv5 overwrite-on-retry holds.
|
|
46
|
+
* Renders an idempotency key into a value safe to carry in the upload's query string.
|
|
45
47
|
*
|
|
46
|
-
* @param {string} idempotencyKey -
|
|
47
|
-
* @returns {string}
|
|
48
|
+
* @param {string} idempotencyKey - Raw per-artifact idempotency key
|
|
49
|
+
* @returns {string} Query-safe, deterministic representation of the key
|
|
48
50
|
*/
|
|
49
51
|
function toIdempotencyQueryValue (idempotencyKey) {
|
|
50
52
|
const separatorIndex = idempotencyKey.indexOf(':')
|
|
51
53
|
if (separatorIndex === -1) {
|
|
52
54
|
return Buffer.from(idempotencyKey, 'utf8').toString('hex')
|
|
53
55
|
}
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
return `${
|
|
56
|
+
const idPart = idempotencyKey.slice(0, separatorIndex)
|
|
57
|
+
const artifactPart = idempotencyKey.slice(separatorIndex + 1)
|
|
58
|
+
return `${idPart}:${Buffer.from(artifactPart, 'utf8').toString('hex')}`
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
* Uploads
|
|
61
|
-
* The trace id is included in the request path and the body is the raw image bytes.
|
|
62
|
-
*
|
|
63
|
-
* The media service requires two values from the tracer, sent as query params so they
|
|
64
|
-
* survive the Agent's evp_proxy (which forwards only an allow-listed header set and would
|
|
65
|
-
* otherwise strip the metadata, leaving the backend with an empty idempotency key):
|
|
66
|
-
* - `idempotencyKey`: stable per artifact and reused on retry, so a retried
|
|
67
|
-
* upload overwrites the same stored object instead of creating a duplicate.
|
|
68
|
-
* - `capturedAtMs`: the capture time in epoch milliseconds, stamped once at
|
|
69
|
-
* capture and resent unchanged on retry (it is part of the stored object key).
|
|
62
|
+
* Uploads one screenshot or video to the Test Optimization media intake.
|
|
70
63
|
*
|
|
71
64
|
* @param {object} options - Upload options
|
|
72
|
-
* @param {string} options.filePath - Path to the
|
|
73
|
-
* @param {
|
|
65
|
+
* @param {string} [options.filePath] - Path to the media file
|
|
66
|
+
* @param {Buffer} [options.content] - In-memory screenshot content
|
|
67
|
+
* @param {'screenshot'|'video'} options.kind - Media kind
|
|
68
|
+
* @param {string} [options.traceId] - Test trace id for test-scoped media
|
|
69
|
+
* @param {string} [options.testSessionId] - Test session id for suite-scoped media
|
|
70
|
+
* @param {string} [options.testSuiteId] - Test suite id for suite-scoped media
|
|
74
71
|
* @param {string} options.idempotencyKey - Stable per-artifact key, reused on retry
|
|
75
72
|
* @param {number} options.capturedAtMs - Capture time in epoch milliseconds
|
|
76
|
-
* @param {URL} options.url -
|
|
73
|
+
* @param {URL} options.url - Base URL for the media upload
|
|
77
74
|
* @param {boolean} [options.isEvpProxy] - Whether to upload through the Agent's evp_proxy
|
|
78
|
-
* @param {string} [options.evpProxyPrefix] -
|
|
75
|
+
* @param {string} [options.evpProxyPrefix] - evp_proxy path prefix
|
|
79
76
|
* @param {number} [options.deadline] - Absolute finalization deadline in epoch milliseconds
|
|
80
77
|
* @param {AbortSignal} [options.signal] - Signal used to cancel the upload
|
|
81
78
|
* @param {Function} callback - Callback function (err)
|
|
79
|
+
* @returns {void}
|
|
82
80
|
*/
|
|
83
|
-
function
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
function uploadTestMedia (options, callback) {
|
|
82
|
+
const {
|
|
83
|
+
filePath,
|
|
84
|
+
content,
|
|
85
|
+
kind,
|
|
86
|
+
traceId,
|
|
87
|
+
testSessionId,
|
|
88
|
+
testSuiteId,
|
|
89
|
+
idempotencyKey,
|
|
90
|
+
capturedAtMs,
|
|
91
|
+
url,
|
|
92
|
+
isEvpProxy,
|
|
93
|
+
evpProxyPrefix,
|
|
94
|
+
deadline,
|
|
95
|
+
signal,
|
|
96
|
+
} = options
|
|
87
97
|
const { DD_API_KEY } = getConfig()
|
|
98
|
+
const isSuiteScoped = testSessionId !== undefined || testSuiteId !== undefined
|
|
99
|
+
let basePath
|
|
88
100
|
|
|
89
|
-
if (
|
|
90
|
-
return callback(new Error('A
|
|
101
|
+
if (kind !== 'screenshot' && kind !== 'video') {
|
|
102
|
+
return callback(new Error('A supported media kind is required for test media upload'))
|
|
103
|
+
}
|
|
104
|
+
if (isSuiteScoped) {
|
|
105
|
+
if (!isValidId(testSessionId) || !isValidId(testSuiteId)) {
|
|
106
|
+
return callback(new Error(
|
|
107
|
+
'Non-zero decimal uint64 test_session_id and test_suite_id are required for test suite media upload'
|
|
108
|
+
))
|
|
109
|
+
}
|
|
110
|
+
basePath = `${TEST_SUITE_MEDIA_ENDPOINT_PREFIX}${testSessionId}/${testSuiteId}${MEDIA_ENDPOINT_SUFFIX}`
|
|
111
|
+
} else {
|
|
112
|
+
if (!isValidId(traceId)) {
|
|
113
|
+
return callback(new Error('A non-zero decimal uint64 trace_id is required for test media upload'))
|
|
114
|
+
}
|
|
115
|
+
basePath = `${TEST_RUN_MEDIA_ENDPOINT_PREFIX}${traceId}${MEDIA_ENDPOINT_SUFFIX}`
|
|
91
116
|
}
|
|
92
117
|
if (!DD_API_KEY && !isEvpProxy) {
|
|
93
|
-
return callback(new Error('DD_API_KEY is required for test
|
|
118
|
+
return callback(new Error('DD_API_KEY is required for test media upload'))
|
|
94
119
|
}
|
|
95
120
|
if (!idempotencyKey) {
|
|
96
|
-
return callback(new Error('An idempotency key is required for test
|
|
121
|
+
return callback(new Error('An idempotency key is required for test media upload'))
|
|
97
122
|
}
|
|
98
123
|
if (!Number.isInteger(capturedAtMs) || capturedAtMs <= 0) {
|
|
99
|
-
return callback(new Error('A positive captured-at timestamp (epoch ms) is required for test
|
|
124
|
+
return callback(new Error('A positive captured-at timestamp (epoch ms) is required for test media upload'))
|
|
100
125
|
}
|
|
101
126
|
|
|
127
|
+
let fileSize
|
|
102
128
|
let screenshotContent
|
|
103
129
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
if (kind === 'video') {
|
|
131
|
+
fileSize = statSync(filePath).size
|
|
132
|
+
} else if (content === undefined) {
|
|
133
|
+
screenshotContent = readFileSync(filePath)
|
|
134
|
+
fileSize = screenshotContent.length
|
|
135
|
+
} else {
|
|
136
|
+
if (!Buffer.isBuffer(content)) {
|
|
137
|
+
return callback(new Error('In-memory screenshot content must be a Buffer'))
|
|
138
|
+
}
|
|
139
|
+
screenshotContent = content
|
|
140
|
+
fileSize = content.length
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const action = kind === 'video' ? 'inspect video' : 'read screenshot'
|
|
144
|
+
return callback(new Error(`Failed to ${action} at ${filePath}: ${error.message}`))
|
|
107
145
|
}
|
|
108
|
-
if (
|
|
109
|
-
return callback(new Error(
|
|
146
|
+
if (fileSize === 0) {
|
|
147
|
+
return callback(new Error(`${kind === 'video' ? 'Video' : 'Screenshot'} at ${filePath} is empty`))
|
|
148
|
+
}
|
|
149
|
+
if (kind === 'video' && fileSize > MAX_VIDEO_SIZE_BYTES) {
|
|
150
|
+
const error = new Error(
|
|
151
|
+
`Video at ${filePath} is ${fileSize} bytes and exceeds the ${MAX_VIDEO_SIZE_BYTES}-byte upload limit`
|
|
152
|
+
)
|
|
153
|
+
log.warn('Skipping test video upload: %s', error.message)
|
|
154
|
+
return callback(error)
|
|
110
155
|
}
|
|
111
156
|
|
|
112
|
-
// Metadata
|
|
113
|
-
// non-allow-listed headers, so header-borne metadata reached the backend empty. The key is
|
|
114
|
-
// rendered proxy-safe (see toIdempotencyQueryValue) because evp_proxy also validates the
|
|
115
|
-
// forwarded query against a restrictive charset. capturedAtMs is a plain integer and part of the
|
|
116
|
-
// stored object key, so it (and the key) must stay stable across retries.
|
|
157
|
+
// Metadata must use query params because the Agent's evp_proxy strips non-allow-listed headers.
|
|
117
158
|
const query = new URLSearchParams({
|
|
118
159
|
idempotency_key: toIdempotencyQueryValue(idempotencyKey),
|
|
119
160
|
captured_at_ms: String(capturedAtMs),
|
|
120
161
|
}).toString()
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const contentType = getContentType(filePath)
|
|
124
|
-
const options = {
|
|
162
|
+
const contentType = getContentType(filePath, kind)
|
|
163
|
+
const requestOptions = {
|
|
125
164
|
method: 'POST',
|
|
126
165
|
headers: {
|
|
166
|
+
'Content-Length': fileSize,
|
|
127
167
|
'Content-Type': contentType,
|
|
128
168
|
},
|
|
129
169
|
path: `${basePath}?${query}`,
|
|
130
170
|
timeout: UPLOAD_TIMEOUT_MS,
|
|
131
171
|
url,
|
|
172
|
+
agent: kind === 'video' ? getMediaAgent(url) : getAgent(url),
|
|
132
173
|
deadline,
|
|
174
|
+
retryUntilDeadline: false,
|
|
133
175
|
signal,
|
|
134
176
|
}
|
|
135
177
|
|
|
136
178
|
if (isEvpProxy) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
options.path = `${joinEVPProxyPath(evpProxyPrefix, basePath)}?${query}`
|
|
140
|
-
options.headers[EVP_SUBDOMAIN_HEADER_NAME] = 'api'
|
|
179
|
+
requestOptions.path = `${joinEVPProxyPath(evpProxyPrefix, basePath)}?${query}`
|
|
180
|
+
requestOptions.headers[EVP_SUBDOMAIN_HEADER_NAME] = 'api'
|
|
141
181
|
} else {
|
|
142
|
-
|
|
182
|
+
requestOptions.headers['DD-API-KEY'] = DD_API_KEY
|
|
143
183
|
}
|
|
144
184
|
|
|
145
|
-
log.debug(
|
|
185
|
+
log.debug(
|
|
186
|
+
'Uploading test %s %s to %s',
|
|
187
|
+
kind,
|
|
188
|
+
filePath || 'from memory',
|
|
189
|
+
new URL(requestOptions.path, url).href
|
|
190
|
+
)
|
|
146
191
|
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
log.error('Error uploading test
|
|
150
|
-
return callback(
|
|
192
|
+
const onResponse = (error, response, statusCode) => {
|
|
193
|
+
if (error) {
|
|
194
|
+
log.error('Error uploading test %s: %s', kind, error.message)
|
|
195
|
+
return callback(error)
|
|
151
196
|
}
|
|
152
197
|
if (statusCode === undefined) {
|
|
153
|
-
const uploadError = new Error(
|
|
154
|
-
log.error('Error uploading test
|
|
198
|
+
const uploadError = new Error(`Test ${kind} upload request was dropped before it was sent`)
|
|
199
|
+
log.error('Error uploading test %s: %s', kind, uploadError.message)
|
|
155
200
|
return callback(uploadError)
|
|
156
201
|
}
|
|
157
|
-
log.debug('Test
|
|
202
|
+
log.debug('Test %s uploaded successfully (status: %d)', kind, statusCode)
|
|
158
203
|
callback(null)
|
|
159
|
-
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (kind === 'video') {
|
|
207
|
+
requestOptions.transport = videoRequest
|
|
208
|
+
request(() => createReadStream(filePath), requestOptions, onResponse)
|
|
209
|
+
} else {
|
|
210
|
+
request(screenshotContent, requestOptions, onResponse)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Uploads a test-scoped failure screenshot.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} options - Upload options
|
|
218
|
+
* @param {Function} callback - Callback function (err)
|
|
219
|
+
* @returns {void}
|
|
220
|
+
*/
|
|
221
|
+
function uploadTestScreenshot (options, callback) {
|
|
222
|
+
uploadTestMedia({ ...options, kind: 'screenshot' }, callback)
|
|
160
223
|
}
|
|
161
224
|
|
|
162
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Uploads a test-scoped failure video.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} options - Upload options
|
|
229
|
+
* @param {Function} callback - Callback function (err)
|
|
230
|
+
* @returns {void}
|
|
231
|
+
*/
|
|
232
|
+
function uploadTestVideo (options, callback) {
|
|
233
|
+
uploadTestMedia({ ...options, kind: 'video' }, callback)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Uploads a suite-scoped failure video.
|
|
238
|
+
*
|
|
239
|
+
* @param {object} options - Upload options
|
|
240
|
+
* @param {Function} callback - Callback function (err)
|
|
241
|
+
* @returns {void}
|
|
242
|
+
*/
|
|
243
|
+
function uploadTestSuiteVideo (options, callback) {
|
|
244
|
+
uploadTestMedia({ ...options, kind: 'video' }, callback)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
MAX_VIDEO_SIZE_BYTES,
|
|
249
|
+
MEDIA_ENDPOINT_SUFFIX,
|
|
250
|
+
TEST_RUN_MEDIA_ENDPOINT_PREFIX,
|
|
251
|
+
TEST_SUITE_MEDIA_ENDPOINT_PREFIX,
|
|
252
|
+
uploadTestScreenshot,
|
|
253
|
+
uploadTestSuiteVideo,
|
|
254
|
+
uploadTestVideo,
|
|
255
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const http = require('node:http')
|
|
4
|
+
const https = require('node:https')
|
|
5
|
+
|
|
6
|
+
const { storage } = require('../../../../datadog-core')
|
|
7
|
+
const log = require('../../log')
|
|
8
|
+
const { markEndpointReached } = require('../../exporters/common/retry')
|
|
9
|
+
const { isLoopbackHost, parseUrl } = require('../../exporters/common/url')
|
|
10
|
+
|
|
11
|
+
const legacyStorage = storage('legacy')
|
|
12
|
+
const MAX_ACTIVE_REQUESTS = 16
|
|
13
|
+
|
|
14
|
+
let activeRequests = 0
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Streams one video request attempt. Test Optimization retry policy stays in the exporter request wrapper.
|
|
18
|
+
*
|
|
19
|
+
* @param {import('node:stream').Readable} body - Video stream for this attempt
|
|
20
|
+
* @param {object} options - HTTP request options
|
|
21
|
+
* @param {(error: Error|null, result?: string|null, statusCode?: number,
|
|
22
|
+
* headers?: import('node:http').IncomingHttpHeaders) => void} callback
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
function requestVideo (body, options, callback) {
|
|
26
|
+
if (activeRequests >= MAX_ACTIVE_REQUESTS) {
|
|
27
|
+
if (!body.destroyed) body.destroy()
|
|
28
|
+
const error = new log.NoTransmitError('Maximum active video request count reached.')
|
|
29
|
+
error.code = 'ERR_DD_VIDEO_REQUEST_LIMIT'
|
|
30
|
+
callback(error)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let requestOptions
|
|
35
|
+
try {
|
|
36
|
+
requestOptions = getRequestOptions(options)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!body.destroyed) body.destroy()
|
|
39
|
+
callback(error)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
activeRequests++
|
|
44
|
+
legacyStorage.run({ noop: true }, () => {
|
|
45
|
+
const timeout = options.timeout || 2000
|
|
46
|
+
const client = requestOptions.protocol === 'https:' ? https : http
|
|
47
|
+
let finished = false
|
|
48
|
+
let req = null
|
|
49
|
+
|
|
50
|
+
const complete = (error, result, statusCode, headers) => {
|
|
51
|
+
if (finished) return
|
|
52
|
+
finished = true
|
|
53
|
+
activeRequests--
|
|
54
|
+
options.signal?.removeEventListener('abort', onAbort)
|
|
55
|
+
body.unpipe(req)
|
|
56
|
+
if (!body.destroyed) body.destroy()
|
|
57
|
+
if (req && !req.destroyed && (error || !req.writableFinished)) req.destroy()
|
|
58
|
+
callback(error, result, statusCode, headers)
|
|
59
|
+
}
|
|
60
|
+
const onAbort = () => complete(getAbortError(options.signal))
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
req = client.request(requestOptions, res => {
|
|
64
|
+
markEndpointReached(requestOptions)
|
|
65
|
+
const chunks = []
|
|
66
|
+
|
|
67
|
+
res.setTimeout(timeout)
|
|
68
|
+
res.on('data', chunk => chunks.push(chunk))
|
|
69
|
+
res.once('aborted', () => complete(Object.assign(new Error('Response aborted'), { code: 'ECONNRESET' })))
|
|
70
|
+
res.once('error', complete)
|
|
71
|
+
res.once('timeout', () => complete(Object.assign(new Error('Response timed out'), { code: 'ETIMEDOUT' })))
|
|
72
|
+
res.once('end', () => {
|
|
73
|
+
const result = Buffer.concat(chunks).toString()
|
|
74
|
+
if (res.statusCode >= 200 && res.statusCode <= 299) {
|
|
75
|
+
complete(null, result, res.statusCode, res.headers)
|
|
76
|
+
} else {
|
|
77
|
+
complete(buildError(res, result, requestOptions), null, res.statusCode, res.headers)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
} catch (error) {
|
|
82
|
+
activeRequests--
|
|
83
|
+
if (!body.destroyed) body.destroy()
|
|
84
|
+
callback(error)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
req.once('error', complete)
|
|
89
|
+
req.setTimeout(timeout, () => complete(Object.assign(new Error('Request timed out'), { code: 'ETIMEDOUT' })))
|
|
90
|
+
body.once('error', error => {
|
|
91
|
+
error.retryable = false
|
|
92
|
+
complete(error)
|
|
93
|
+
})
|
|
94
|
+
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
95
|
+
if (options.signal?.aborted) return onAbort()
|
|
96
|
+
body.pipe(req)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {object} options
|
|
102
|
+
* @returns {object}
|
|
103
|
+
*/
|
|
104
|
+
function getRequestOptions (options) {
|
|
105
|
+
const requestOptions = {
|
|
106
|
+
...options,
|
|
107
|
+
headers: options.headers ? { ...options.headers } : {},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.url) {
|
|
111
|
+
const url = parseUrl(options.url)
|
|
112
|
+
if (url.protocol === 'unix:') {
|
|
113
|
+
requestOptions.socketPath = url.pathname
|
|
114
|
+
} else {
|
|
115
|
+
requestOptions.protocol = url.protocol
|
|
116
|
+
requestOptions.hostname = url.hostname
|
|
117
|
+
requestOptions.port = url.port
|
|
118
|
+
requestOptions.path ??= url.path
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const hasApiKey = requestOptions.headers['dd-api-key'] !== undefined ||
|
|
123
|
+
requestOptions.headers['DD-API-KEY'] !== undefined
|
|
124
|
+
if (hasApiKey && requestOptions.protocol === 'http:' && !isLoopbackHost(requestOptions.hostname)) {
|
|
125
|
+
log.error(
|
|
126
|
+
'Not sending the Datadog API key over a non-TLS connection to %s. Configure an https intake URL.',
|
|
127
|
+
requestOptions.hostname
|
|
128
|
+
)
|
|
129
|
+
delete requestOptions.headers['dd-api-key']
|
|
130
|
+
delete requestOptions.headers['DD-API-KEY']
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
delete requestOptions.deadline
|
|
134
|
+
delete requestOptions.retry
|
|
135
|
+
delete requestOptions.retryUntilDeadline
|
|
136
|
+
delete requestOptions.signal
|
|
137
|
+
delete requestOptions.url
|
|
138
|
+
return requestOptions
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {AbortSignal} signal
|
|
143
|
+
* @returns {Error}
|
|
144
|
+
*/
|
|
145
|
+
function getAbortError (signal) {
|
|
146
|
+
return signal?.reason || Object.assign(new Error('Request aborted'), { code: 'ABORT_ERR' })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {import('node:http').IncomingMessage} res
|
|
151
|
+
* @param {string} responseData
|
|
152
|
+
* @param {object} options
|
|
153
|
+
* @returns {Error}
|
|
154
|
+
*/
|
|
155
|
+
function buildError (res, responseData, options) {
|
|
156
|
+
let errorMessage = ''
|
|
157
|
+
try {
|
|
158
|
+
const port = options.port ? `:${options.port}` : ''
|
|
159
|
+
const baseUrl = `${options.protocol || 'http:'}//${options.hostname || 'localhost'}${port}`
|
|
160
|
+
const fullUrl = new URL(options.path, baseUrl).href
|
|
161
|
+
errorMessage = `Error from ${fullUrl}: ${res.statusCode} ${http.STATUS_CODES[res.statusCode]}.`
|
|
162
|
+
} catch {
|
|
163
|
+
// ignore
|
|
164
|
+
}
|
|
165
|
+
if (responseData) errorMessage += ` Response from the endpoint: "${responseData}"`
|
|
166
|
+
|
|
167
|
+
const error = new log.NoTransmitError(errorMessage)
|
|
168
|
+
error.status = res.statusCode
|
|
169
|
+
return error
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Object.defineProperty(requestVideo, 'writable', {
|
|
173
|
+
get () { return activeRequests < MAX_ACTIVE_REQUESTS },
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
module.exports = requestVideo
|