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
|
@@ -206,6 +206,15 @@
|
|
|
206
206
|
]
|
|
207
207
|
}
|
|
208
208
|
],
|
|
209
|
+
"DD_APPSEC_AGENTIC_ONBOARDING": [
|
|
210
|
+
{
|
|
211
|
+
"implementation": "A",
|
|
212
|
+
"type": "string",
|
|
213
|
+
"default": "",
|
|
214
|
+
"namespace": "appsec",
|
|
215
|
+
"description": "Set automatically by Datadog's agentic onboarding solution when it configures App & API Protection for a service. Reported verbatim in configuration telemetry only; it has no effect on tracer behavior."
|
|
216
|
+
}
|
|
217
|
+
],
|
|
209
218
|
"DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE": [
|
|
210
219
|
{
|
|
211
220
|
"implementation": "E",
|
|
@@ -614,6 +623,14 @@
|
|
|
614
623
|
"namespace": "testOptimization"
|
|
615
624
|
}
|
|
616
625
|
],
|
|
626
|
+
"DD_CODE_COVERAGE_FLAGS": [
|
|
627
|
+
{
|
|
628
|
+
"implementation": "A",
|
|
629
|
+
"type": "string",
|
|
630
|
+
"default": null,
|
|
631
|
+
"namespace": "testOptimization"
|
|
632
|
+
}
|
|
633
|
+
],
|
|
617
634
|
"DD_CODE_ORIGIN_FOR_SPANS_ENABLED": [
|
|
618
635
|
{
|
|
619
636
|
"implementation": "A",
|
|
@@ -827,6 +844,55 @@
|
|
|
827
844
|
"default": "false"
|
|
828
845
|
}
|
|
829
846
|
],
|
|
847
|
+
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE": [
|
|
848
|
+
{
|
|
849
|
+
"implementation": "A",
|
|
850
|
+
"type": "string",
|
|
851
|
+
"default": "agentless",
|
|
852
|
+
"allowed": "agentless|remote_config",
|
|
853
|
+
"description": "Experimental: Select where Feature Flagging loads Universal Flag Configuration. Supported values are agentless and remote_config.",
|
|
854
|
+
"namespace": "featureFlags",
|
|
855
|
+
"transform": "toLowerCase"
|
|
856
|
+
}
|
|
857
|
+
],
|
|
858
|
+
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL": [
|
|
859
|
+
{
|
|
860
|
+
"implementation": "A",
|
|
861
|
+
"type": "string",
|
|
862
|
+
"default": null,
|
|
863
|
+
"description": "Experimental: Override the agentless Feature Flagging UFC endpoint or base URL.",
|
|
864
|
+
"namespace": "featureFlags",
|
|
865
|
+
"sensitive": true
|
|
866
|
+
}
|
|
867
|
+
],
|
|
868
|
+
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS": [
|
|
869
|
+
{
|
|
870
|
+
"implementation": "A",
|
|
871
|
+
"type": "int",
|
|
872
|
+
"default": "30",
|
|
873
|
+
"description": "Experimental: Set the agentless Feature Flagging UFC polling interval in seconds, capped at one hour.",
|
|
874
|
+
"allowed": "[1-9]\\d*",
|
|
875
|
+
"namespace": "featureFlags"
|
|
876
|
+
}
|
|
877
|
+
],
|
|
878
|
+
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS": [
|
|
879
|
+
{
|
|
880
|
+
"implementation": "A",
|
|
881
|
+
"type": "int",
|
|
882
|
+
"default": "5",
|
|
883
|
+
"description": "Experimental: Set the agentless Feature Flagging UFC request timeout in seconds.",
|
|
884
|
+
"allowed": "[1-9]\\d*",
|
|
885
|
+
"namespace": "featureFlags"
|
|
886
|
+
}
|
|
887
|
+
],
|
|
888
|
+
"DD_FEATURE_FLAGS_ENABLED": [
|
|
889
|
+
{
|
|
890
|
+
"implementation": "A",
|
|
891
|
+
"type": "boolean",
|
|
892
|
+
"default": "true",
|
|
893
|
+
"namespace": "featureFlags"
|
|
894
|
+
}
|
|
895
|
+
],
|
|
830
896
|
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
|
|
831
897
|
{
|
|
832
898
|
"implementation": "B",
|
|
@@ -19,6 +19,8 @@ module.exports = function getExporter (name) {
|
|
|
19
19
|
return require('./ci-visibility/exporters/agentless')
|
|
20
20
|
case exporters.AGENT_PROXY:
|
|
21
21
|
return require('./ci-visibility/exporters/agent-proxy')
|
|
22
|
+
case exporters.CI_VALIDATION:
|
|
23
|
+
return require('./ci-visibility/exporters/ci-validation')
|
|
22
24
|
case exporters.JEST_WORKER:
|
|
23
25
|
case exporters.CUCUMBER_WORKER:
|
|
24
26
|
case exporters.MOCHA_WORKER:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { VERSION } = require('../../../../../version')
|
|
4
|
+
|
|
5
|
+
const LANGUAGE = 'nodejs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the canonical client-library identification headers.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} [language] - Client library language name.
|
|
11
|
+
* @param {string} [version] - Client library version.
|
|
12
|
+
* @returns {Record<string, string>} Client library identification headers.
|
|
13
|
+
*/
|
|
14
|
+
function getClientLibraryHeaders (language = LANGUAGE, version = VERSION) {
|
|
15
|
+
return {
|
|
16
|
+
'DD-Client-Library-Language': language,
|
|
17
|
+
'DD-Client-Library-Version': version,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { getClientLibraryHeaders }
|
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
const { Readable } = require('stream')
|
|
7
7
|
const http = require('http')
|
|
8
8
|
const https = require('https')
|
|
9
|
-
const net = require('net')
|
|
10
9
|
const zlib = require('zlib')
|
|
11
10
|
|
|
12
11
|
const { storage } = require('../../../../datadog-core')
|
|
13
12
|
const log = require('../../log')
|
|
14
|
-
const { parseUrl } = require('./url')
|
|
13
|
+
const { isLoopbackHost, parseUrl } = require('./url')
|
|
15
14
|
const docker = require('./docker')
|
|
16
15
|
const { httpAgent, httpsAgent } = require('./agents')
|
|
17
16
|
const {
|
|
@@ -27,21 +26,11 @@ const maxActiveBufferSize = 1024 * 1024 * 64
|
|
|
27
26
|
|
|
28
27
|
let activeBufferSize = 0
|
|
29
28
|
|
|
30
|
-
/**
|
|
31
|
-
* @param {string} hostname Host as resolved by {@link parseUrl}; IPv6 is unbracketed (`::1`).
|
|
32
|
-
*/
|
|
33
|
-
function isLoopbackHost (hostname) {
|
|
34
|
-
// The 127.0.0.0/8 block is loopback, but only when the host is an actual IPv4 literal: a
|
|
35
|
-
// hostname like `127.evil.com` shares the prefix yet resolves anywhere, so net.isIPv4 gates it.
|
|
36
|
-
return hostname === 'localhost' ||
|
|
37
|
-
hostname === '::1' ||
|
|
38
|
-
(hostname.startsWith('127.') && net.isIPv4(hostname))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
29
|
/**
|
|
42
30
|
* @param {Buffer|string|Readable|Array<Buffer|string>} data
|
|
43
31
|
* @param {object} options
|
|
44
|
-
* @param {(error: Error|null, result
|
|
32
|
+
* @param {(error: Error|null, result?: string|null, statusCode?: number,
|
|
33
|
+
* headers?: import('node:http').IncomingHttpHeaders) => void} callback
|
|
45
34
|
*/
|
|
46
35
|
function request (data, options, callback) {
|
|
47
36
|
if (!options.headers) {
|
|
@@ -106,34 +95,50 @@ function request (data, options, callback) {
|
|
|
106
95
|
|
|
107
96
|
options.agent = isSecure ? httpsAgent : httpAgent
|
|
108
97
|
|
|
109
|
-
|
|
98
|
+
/**
|
|
99
|
+
* @param {import('node:http').IncomingMessage} res
|
|
100
|
+
* @param {(error: Error|null, result?: string|null, statusCode?: number,
|
|
101
|
+
* headers?: import('node:http').IncomingHttpHeaders) => void} complete
|
|
102
|
+
* @param {(error: Error) => void} handleError
|
|
103
|
+
*/
|
|
104
|
+
const onResponse = (res, complete, handleError) => {
|
|
110
105
|
markEndpointReached(options)
|
|
111
106
|
|
|
112
107
|
const chunks = []
|
|
113
108
|
|
|
114
109
|
res.setTimeout(timeout)
|
|
115
110
|
|
|
111
|
+
res.once('aborted', () => {
|
|
112
|
+
handleError(Object.assign(new Error('Response aborted'), { code: 'ECONNRESET' }))
|
|
113
|
+
})
|
|
114
|
+
res.once('error', handleError)
|
|
115
|
+
res.once('timeout', () => {
|
|
116
|
+
const error = Object.assign(new Error('Response timed out'), { code: 'ETIMEDOUT' })
|
|
117
|
+
res.destroy(error)
|
|
118
|
+
handleError(error)
|
|
119
|
+
})
|
|
120
|
+
|
|
116
121
|
res.on('data', chunk => {
|
|
117
122
|
chunks.push(chunk)
|
|
118
123
|
})
|
|
119
124
|
|
|
120
125
|
res.once('end', () => {
|
|
121
|
-
finalize()
|
|
122
126
|
const buffer = Buffer.concat(chunks)
|
|
123
127
|
|
|
124
128
|
if (res.statusCode >= 200 && res.statusCode <= 299) {
|
|
125
|
-
const
|
|
129
|
+
const contentEncoding = res.headers['content-encoding']
|
|
130
|
+
const isGzip = typeof contentEncoding === 'string' && contentEncoding.toLowerCase() === 'gzip'
|
|
126
131
|
if (isGzip) {
|
|
127
132
|
zlib.gunzip(buffer, (err, result) => {
|
|
128
133
|
if (err) {
|
|
129
134
|
log.error('Could not gunzip response: %s', err.message)
|
|
130
|
-
|
|
135
|
+
complete(null, '', res.statusCode, res.headers)
|
|
131
136
|
} else {
|
|
132
|
-
|
|
137
|
+
complete(null, result.toString(), res.statusCode, res.headers)
|
|
133
138
|
}
|
|
134
139
|
})
|
|
135
140
|
} else {
|
|
136
|
-
|
|
141
|
+
complete(null, buffer.toString(), res.statusCode, res.headers)
|
|
137
142
|
}
|
|
138
143
|
} else {
|
|
139
144
|
let errorMessage = ''
|
|
@@ -154,7 +159,7 @@ function request (data, options, callback) {
|
|
|
154
159
|
const error = new log.NoTransmitError(errorMessage)
|
|
155
160
|
error.status = res.statusCode
|
|
156
161
|
|
|
157
|
-
|
|
162
|
+
complete(error, null, res.statusCode, res.headers)
|
|
158
163
|
}
|
|
159
164
|
})
|
|
160
165
|
}
|
|
@@ -162,6 +167,7 @@ function request (data, options, callback) {
|
|
|
162
167
|
// Retries always run via setTimeout so the AsyncLocalStorage store survives
|
|
163
168
|
// the gap before socket.connect(); ALS.run() does not call ALS.enterWith()
|
|
164
169
|
// outside AsyncContextFrame, so a synchronous re-entry would lose the store.
|
|
170
|
+
/** @param {number} attemptIndex */
|
|
165
171
|
const attempt = attemptIndex => {
|
|
166
172
|
if (!request.writable) {
|
|
167
173
|
log.debug('Maximum number of active requests reached: payload is discarded.')
|
|
@@ -172,28 +178,51 @@ function request (data, options, callback) {
|
|
|
172
178
|
|
|
173
179
|
legacyStorage.run({ noop: true }, () => {
|
|
174
180
|
let finished = false
|
|
181
|
+
let settled = false
|
|
175
182
|
const finalize = () => {
|
|
176
183
|
if (finished) return
|
|
177
184
|
finished = true
|
|
178
185
|
activeBufferSize -= options.headers['Content-Length'] ?? 0
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
/**
|
|
189
|
+
* @param {Error | null} error
|
|
190
|
+
* @param {string | null} [result]
|
|
191
|
+
* @param {number} [statusCode]
|
|
192
|
+
* @param {import('node:http').IncomingHttpHeaders} [headers]
|
|
193
|
+
*/
|
|
194
|
+
const complete = (error, result, statusCode, headers) => {
|
|
195
|
+
if (settled) return
|
|
196
|
+
settled = true
|
|
187
197
|
finalize()
|
|
188
|
-
|
|
198
|
+
callback(error, result, statusCode, headers)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {Error} error
|
|
203
|
+
*/
|
|
204
|
+
const handleError = (error) => {
|
|
205
|
+
if (settled) return
|
|
206
|
+
|
|
207
|
+
if (options.retry !== false &&
|
|
208
|
+
attemptIndex < getMaxAttempts(options) &&
|
|
209
|
+
isRetriableNetworkError(error)) {
|
|
210
|
+
settled = true
|
|
211
|
+
finalize()
|
|
189
212
|
// Unref so a pending retry never keeps the host process alive past
|
|
190
213
|
// its natural exit point; long-running apps still retry because the
|
|
191
214
|
// event loop is held open by their own work.
|
|
192
215
|
setTimeout(attempt, getRetryDelay(options, attemptIndex), attemptIndex + 1).unref?.()
|
|
193
216
|
} else {
|
|
194
|
-
|
|
217
|
+
complete(error)
|
|
195
218
|
}
|
|
196
|
-
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const req = client.request(options, (res) => onResponse(res, complete, handleError))
|
|
222
|
+
|
|
223
|
+
req.once('close', finalize)
|
|
224
|
+
req.once('timeout', finalize)
|
|
225
|
+
req.once('error', handleError)
|
|
197
226
|
|
|
198
227
|
req.setTimeout(timeout, () => {
|
|
199
228
|
try {
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const net = require('node:net')
|
|
4
|
+
|
|
3
5
|
const { urlToHttpOptions } = require('./url-to-http-options-polyfill')
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} hostname
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
function isLoopbackHost (hostname) {
|
|
12
|
+
// Gate the 127/8 prefix on an IPv4 literal so names such as 127.example.com cannot pass.
|
|
13
|
+
return hostname === 'localhost' ||
|
|
14
|
+
hostname === '::1' ||
|
|
15
|
+
hostname === '[::1]' ||
|
|
16
|
+
(hostname.startsWith('127.') && net.isIPv4(hostname))
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
/**
|
|
6
20
|
* Convert an agent/intake URL into Node http(s) request options.
|
|
7
21
|
*
|
|
@@ -31,4 +45,4 @@ function parseUrl (urlObjOrString) {
|
|
|
31
45
|
return url
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
module.exports = { parseUrl }
|
|
48
|
+
module.exports = { isLoopbackHost, parseUrl }
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{ enable: (config: import('./config/config-base')) => void, disable: () => void }} FeatureModule
|
|
5
|
+
* @typedef {new (tracer: import('./tracer'), config: import('./config/config-base')) => object} FeatureProvider
|
|
6
|
+
*/
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* @typedef {object} Feature
|
|
5
10
|
* @property {string} name
|
|
6
11
|
* @property {object} noop
|
|
7
|
-
* @property {() =>
|
|
8
|
-
* @property {
|
|
9
|
-
* @property {
|
|
12
|
+
* @property {() => FeatureModule} factory
|
|
13
|
+
* @property {(config: import('./config/config-base')) => boolean} isEnabled
|
|
14
|
+
* @property {() => FeatureProvider} provider
|
|
15
|
+
* @property {(rc: import('./remote_config'), config: import('./config/config-base'),
|
|
16
|
+
* proxy: import('./proxy')) => void} [remoteConfig]
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
/** @type {{ [name: string]: Feature }} */
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Control-plane HTTP client for LLM Obs Experiments. Uses the global `fetch`,
|
|
4
|
+
// so this module adds no new dependency; credentials and site come from config.
|
|
5
|
+
|
|
6
|
+
const API_BASE_PATH = '/api/v2/llm-obs/v1'
|
|
7
|
+
|
|
8
|
+
// Control-plane host for a Datadog site, e.g.
|
|
9
|
+
// datadoghq.com -> api.datadoghq.com
|
|
10
|
+
// us3.datadoghq.com -> api.us3.datadoghq.com
|
|
11
|
+
// datad0g.com (staging)-> api.datad0g.com
|
|
12
|
+
function apiHost (site) {
|
|
13
|
+
return `api.${site}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Web-app host for dashboard URLs. Single-level sites (datadoghq.com,
|
|
17
|
+
// ddog-gov.com) are served from the `app.` subdomain; regional sites
|
|
18
|
+
// (us3.datadoghq.com, ap1.datadoghq.com) are used as-is.
|
|
19
|
+
function appHost (site) {
|
|
20
|
+
return site.split('.').length === 2 ? `app.${site}` : site
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ExperimentsClient {
|
|
24
|
+
#apiKey
|
|
25
|
+
#appKey
|
|
26
|
+
#site
|
|
27
|
+
#projectName
|
|
28
|
+
#timeout
|
|
29
|
+
#cachedProjectId
|
|
30
|
+
|
|
31
|
+
constructor ({ apiKey, appKey, site, projectName, timeout = 30_000 } = {}) {
|
|
32
|
+
this.#apiKey = apiKey
|
|
33
|
+
this.#appKey = appKey
|
|
34
|
+
this.#site = site
|
|
35
|
+
this.#projectName = projectName
|
|
36
|
+
this.#timeout = timeout
|
|
37
|
+
this.#cachedProjectId = null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Whether the client has everything it needs to talk to the control plane.
|
|
41
|
+
get configured () {
|
|
42
|
+
return Boolean(this.#apiKey && this.#appKey && this.#site)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get site () {
|
|
46
|
+
return this.#site
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Dashboard URL base for the configured site, e.g. https://app.datadoghq.com
|
|
50
|
+
get appBase () {
|
|
51
|
+
return `https://${appHost(this.#site)}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Resolve the configured project's id (get-or-create), cached.
|
|
55
|
+
ensureProjectId () {
|
|
56
|
+
return this.getOrCreateProject(this.#projectName)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Low-level request. Builds https://api.<site><path>, attaches both keys, and
|
|
60
|
+
// returns the parsed JSON body. Throws with status + body on a non-2xx.
|
|
61
|
+
async request (method, path, body) {
|
|
62
|
+
const url = `https://${apiHost(this.#site)}${path}`
|
|
63
|
+
const headers = {
|
|
64
|
+
'DD-API-KEY': this.#apiKey,
|
|
65
|
+
'DD-APPLICATION-KEY': this.#appKey,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let payload
|
|
69
|
+
if (body !== undefined) {
|
|
70
|
+
payload = JSON.stringify(body)
|
|
71
|
+
headers['Content-Type'] = 'application/json'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let response
|
|
75
|
+
try {
|
|
76
|
+
response = await fetch(url, {
|
|
77
|
+
method,
|
|
78
|
+
headers,
|
|
79
|
+
body: payload,
|
|
80
|
+
signal: AbortSignal.timeout(this.#timeout),
|
|
81
|
+
})
|
|
82
|
+
} catch (err) {
|
|
83
|
+
throw new Error(`${method} ${path} failed: ${err.message}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const text = await response.text()
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`${method} ${path} failed: HTTP ${response.status} ${text}`)
|
|
89
|
+
}
|
|
90
|
+
return text ? JSON.parse(text) : {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Resolve the project id for `name`, creating it if absent. The create
|
|
94
|
+
// endpoint is get-or-create on name, so repeated calls return the same id.
|
|
95
|
+
// Cached after the first resolution.
|
|
96
|
+
async getOrCreateProject (name) {
|
|
97
|
+
if (this.#cachedProjectId) return this.#cachedProjectId
|
|
98
|
+
|
|
99
|
+
let response
|
|
100
|
+
try {
|
|
101
|
+
response = await this.request('POST', `${API_BASE_PATH}/projects`, {
|
|
102
|
+
data: { type: 'projects', attributes: { name } },
|
|
103
|
+
})
|
|
104
|
+
} catch (err) {
|
|
105
|
+
throw new Error(`Failed to create or get project '${name}': ${err.message}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.#cachedProjectId = response?.data?.id ?? null
|
|
109
|
+
return this.#cachedProjectId
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { ExperimentsClient, apiHost, appHost, API_BASE_PATH }
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { API_BASE_PATH } = require('./client')
|
|
4
|
+
|
|
5
|
+
// Immutable dataset record: { input, expectedOutput?, metadata? }.
|
|
6
|
+
class DatasetRecord {
|
|
7
|
+
constructor (input, expectedOutput = null, metadata = {}) {
|
|
8
|
+
this.input = input
|
|
9
|
+
this.expectedOutput = expectedOutput ?? null
|
|
10
|
+
this.metadata = metadata ?? {}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// A local buffer of dataset records, created remotely and pushed on first run
|
|
15
|
+
// (or eagerly via push()). Pushes are incremental.
|
|
16
|
+
class Dataset {
|
|
17
|
+
#client
|
|
18
|
+
#name
|
|
19
|
+
#description
|
|
20
|
+
#records
|
|
21
|
+
#recordIds
|
|
22
|
+
#id
|
|
23
|
+
#projectId
|
|
24
|
+
#pushedCount
|
|
25
|
+
|
|
26
|
+
constructor (client, name, description = '') {
|
|
27
|
+
this.#client = client
|
|
28
|
+
this.#name = name
|
|
29
|
+
this.#description = description
|
|
30
|
+
this.#records = []
|
|
31
|
+
this.#recordIds = []
|
|
32
|
+
this.#id = null
|
|
33
|
+
this.#projectId = null
|
|
34
|
+
this.#pushedCount = 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build a Dataset that already exists remotely (used by pullDataset).
|
|
38
|
+
static fromExisting (client, name, description, id, projectId, records, recordIds) {
|
|
39
|
+
const dataset = new Dataset(client, name, description)
|
|
40
|
+
dataset.#id = id
|
|
41
|
+
dataset.#projectId = projectId
|
|
42
|
+
dataset.#records.push(...records)
|
|
43
|
+
dataset.#recordIds.push(...recordIds)
|
|
44
|
+
dataset.#pushedCount = records.length
|
|
45
|
+
return dataset
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Append a record. Accepts a DatasetRecord or (input, expectedOutput?, metadata?).
|
|
49
|
+
addRecord (recordOrInput, expectedOutput, metadata) {
|
|
50
|
+
const record = recordOrInput instanceof DatasetRecord
|
|
51
|
+
? recordOrInput
|
|
52
|
+
: new DatasetRecord(recordOrInput, expectedOutput, metadata)
|
|
53
|
+
this.#records.push(record)
|
|
54
|
+
return this
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
name () {
|
|
58
|
+
return this.#name
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
records () {
|
|
62
|
+
return [...this.#records]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
recordIds () {
|
|
66
|
+
return [...this.#recordIds]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
id () {
|
|
70
|
+
return this.#id
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
projectId () {
|
|
74
|
+
return this.#projectId
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Dashboard URL for this dataset, or null until pushed/pulled.
|
|
78
|
+
url () {
|
|
79
|
+
if (this.#id === null) return null
|
|
80
|
+
return `${this.#client.appBase}/llm/datasets/${this.#id}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Eagerly create the dataset (if needed) and push any unpushed records.
|
|
84
|
+
async push () {
|
|
85
|
+
const projectId = await this.#client.ensureProjectId()
|
|
86
|
+
return this.ensureCreatedAndPushed(projectId)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create the remote dataset if needed, then push records added since the last
|
|
90
|
+
// push. Idempotent and incremental. Resolves to { pushedCount, totalCount } for
|
|
91
|
+
// the records attempted in this call, so callers can confirm the push landed.
|
|
92
|
+
async ensureCreatedAndPushed (projectId) {
|
|
93
|
+
if (this.#id === null) {
|
|
94
|
+
let response
|
|
95
|
+
try {
|
|
96
|
+
response = await this.#client.request('POST', `${API_BASE_PATH}/${projectId}/datasets`, {
|
|
97
|
+
data: { type: 'datasets', attributes: { name: this.#name, description: this.#description } },
|
|
98
|
+
})
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new Error(`Failed to create dataset '${this.#name}': ${err.message}`)
|
|
101
|
+
}
|
|
102
|
+
this.#id = response?.data?.id ?? null
|
|
103
|
+
this.#projectId = projectId
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this.#pushedCount >= this.#records.length) return { pushedCount: 0, totalCount: 0 }
|
|
107
|
+
|
|
108
|
+
const pending = this.#records.slice(this.#pushedCount)
|
|
109
|
+
const records = pending.map((rec) => {
|
|
110
|
+
const out = { input: rec.input }
|
|
111
|
+
if (rec.expectedOutput !== null && rec.expectedOutput !== undefined) {
|
|
112
|
+
out.expected_output = rec.expectedOutput
|
|
113
|
+
}
|
|
114
|
+
if (rec.metadata && Object.keys(rec.metadata).length > 0) {
|
|
115
|
+
out.metadata = rec.metadata
|
|
116
|
+
}
|
|
117
|
+
return out
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
let response
|
|
121
|
+
try {
|
|
122
|
+
response = await this.#client.request(
|
|
123
|
+
'POST',
|
|
124
|
+
`${API_BASE_PATH}/${projectId}/datasets/${this.#id}/records`,
|
|
125
|
+
{ data: { type: 'datasets', attributes: { records } } }
|
|
126
|
+
)
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new Error(`Failed to push records to dataset '${this.#name}': ${err.message}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The append-records response returns created records under a top-level
|
|
132
|
+
// `records` field, not the usual `data` envelope.
|
|
133
|
+
const created = response?.records
|
|
134
|
+
let pushedCount = 0
|
|
135
|
+
if (Array.isArray(created)) {
|
|
136
|
+
for (const node of created) {
|
|
137
|
+
const recordId = String(node?.id ?? '')
|
|
138
|
+
if (recordId !== '') pushedCount++
|
|
139
|
+
this.#recordIds.push(recordId)
|
|
140
|
+
}
|
|
141
|
+
for (let i = created.length; i < pending.length; i++) this.#recordIds.push('')
|
|
142
|
+
} else {
|
|
143
|
+
for (let i = 0; i < pending.length; i++) this.#recordIds.push('')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Advance by the snapshotted pending count, not the live records length,
|
|
147
|
+
// so records added while this push was in flight aren't skipped by the next push.
|
|
148
|
+
this.#pushedCount += pending.length
|
|
149
|
+
|
|
150
|
+
return { pushedCount, totalCount: pending.length }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { Dataset, DatasetRecord }
|