dd-trace 5.122.0 → 5.124.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 +5 -0
- package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
- package/index.d.ts +144 -25
- package/package.json +26 -25
- package/packages/datadog-instrumentations/src/bunyan.js +4 -2
- package/packages/datadog-instrumentations/src/cucumber.js +4 -4
- package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/jest.js +140 -18
- package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
- package/packages/datadog-instrumentations/src/mariadb.js +7 -0
- package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
- package/packages/datadog-instrumentations/src/pino.js +15 -4
- package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
- package/packages/datadog-instrumentations/src/playwright.js +67 -53
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +13 -12
- package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
- package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
- package/packages/datadog-instrumentations/src/winston.js +6 -3
- package/packages/datadog-plugin-cypress/src/support.js +1 -0
- package/packages/datadog-plugin-http/src/client.js +2 -15
- package/packages/datadog-plugin-http2/src/client.js +2 -15
- package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
- package/packages/datadog-plugin-playwright/src/index.js +13 -1
- package/packages/datadog-plugin-undici/src/index.js +2 -15
- package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
- package/packages/dd-trace/src/aiguard/noop.js +9 -1
- package/packages/dd-trace/src/aiguard/redaction.js +183 -0
- package/packages/dd-trace/src/aiguard/sdk.js +7 -2
- package/packages/dd-trace/src/aiguard/tags.js +2 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
- package/packages/dd-trace/src/appsec/reporter.js +2 -0
- package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
- package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
- package/packages/dd-trace/src/carrier.js +107 -48
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
- package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
- package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
- package/packages/dd-trace/src/config/index.js +2 -1
- package/packages/dd-trace/src/config/supported-configurations.json +27 -0
- package/packages/dd-trace/src/debugger/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
- package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
- package/packages/dd-trace/src/debugger/index.js +4 -2
- package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
- package/packages/dd-trace/src/dogstatsd.js +45 -18
- package/packages/dd-trace/src/encode/agentless-json.js +5 -4
- package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
- package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
- package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
- package/packages/dd-trace/src/evp_proxy/path.js +22 -0
- package/packages/dd-trace/src/exporters/agent/index.js +22 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
- package/packages/dd-trace/src/exporters/common/request.js +5 -2
- package/packages/dd-trace/src/exporters/common/writer.js +24 -2
- package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
- package/packages/dd-trace/src/external-logger/src/index.js +4 -3
- package/packages/dd-trace/src/flush.js +102 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +10 -2
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +301 -14
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +440 -112
- package/packages/dd-trace/src/llmobs/experiments/index.js +68 -44
- package/packages/dd-trace/src/llmobs/experiments/noop.js +103 -12
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +81 -4
- package/packages/dd-trace/src/llmobs/index.js +80 -17
- package/packages/dd-trace/src/llmobs/noop.js +2 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
- package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
- package/packages/dd-trace/src/noop/span.js +5 -0
- package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
- package/packages/dd-trace/src/openfeature/index.js +7 -4
- package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
- package/packages/dd-trace/src/openfeature/writers/exposures.js +70 -15
- package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
- package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
- package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +429 -352
- package/packages/dd-trace/src/opentracing/span.js +28 -0
- package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
- package/packages/dd-trace/src/plugins/util/test.js +74 -21
- package/packages/dd-trace/src/plugins/util/web.js +2 -65
- package/packages/dd-trace/src/profiling/oom.js +4 -3
- package/packages/dd-trace/src/profiling/profiler.js +7 -0
- package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
- package/packages/dd-trace/src/proxy.js +36 -8
- package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
- package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
- package/packages/dd-trace/src/serverless/vercel.js +122 -0
- package/packages/dd-trace/src/serverless.js +37 -20
- package/packages/dd-trace/src/span_stats.js +40 -3
- package/packages/dd-trace/src/telemetry/metrics.js +62 -8
- package/packages/dd-trace/src/telemetry/send-data.js +1 -0
- package/packages/dd-trace/src/tracer.js +22 -0
- package/packages/dd-trace/src/web-tags-cache.js +67 -13
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/https-proxy-agent/LICENSE +22 -0
- package/vendor/dist/https-proxy-agent/index.js +4 -0
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/proxy-from-env/LICENSE +20 -0
- package/vendor/dist/proxy-from-env/index.js +1 -0
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../../log')
|
|
4
4
|
const { ExperimentsClient } = require('./client')
|
|
5
|
-
const { Dataset
|
|
5
|
+
const { Dataset } = require('./dataset')
|
|
6
6
|
const { Experiment, ExternalExperiment } = require('./experiment')
|
|
7
|
+
const { validateTagsList } = require('./util')
|
|
7
8
|
const NoopExperiments = require('./noop')
|
|
8
9
|
|
|
10
|
+
const DEFAULT_PROJECT_NAME = 'default-project'
|
|
11
|
+
|
|
9
12
|
// Poll `attempt` with exponential backoff until it returns true or the time
|
|
10
13
|
// budget is spent. Used for eventually-consistent reads (pullDataset).
|
|
11
14
|
async function retryWithBackoff (attempt, { maxTotalMs = 30_000, baseDelayMs = 250, maxDelayMs = 8000 } = {}) {
|
|
@@ -32,8 +35,8 @@ class Experiments {
|
|
|
32
35
|
|
|
33
36
|
constructor (config, llmobs) {
|
|
34
37
|
this.#config = config
|
|
35
|
-
this.#llmobs = llmobs
|
|
36
|
-
this.#projectName = config.llmobs?.
|
|
38
|
+
this.#llmobs = config.llmobs?.mlApp || config.service ? llmobs : undefined
|
|
39
|
+
this.#projectName = config.llmobs?.projectName || DEFAULT_PROJECT_NAME
|
|
37
40
|
this.#client = this.#clientForProject(this.#projectName)
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -50,50 +53,54 @@ class Experiments {
|
|
|
50
53
|
})
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @param {string | undefined} projectName
|
|
58
|
+
* @returns {ExperimentsClient}
|
|
59
|
+
*/
|
|
60
|
+
#clientForOperation (projectName) {
|
|
61
|
+
if (projectName !== undefined && projectName !== this.#projectName) {
|
|
62
|
+
return this.#clientForProject(projectName)
|
|
63
|
+
}
|
|
64
|
+
if (this.#client === undefined) this.#client = this.#clientForProject(projectName)
|
|
65
|
+
return this.#client
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
// Create a local dataset buffer. Pushed remotely on first experiment run.
|
|
54
69
|
createDataset (name, descriptionOrOptions = '') {
|
|
55
70
|
const options = typeof descriptionOrOptions === 'string'
|
|
56
71
|
? { description: descriptionOrOptions }
|
|
57
72
|
: (descriptionOrOptions ?? {})
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
if ((options.records) != null)
|
|
61
|
-
for (const record of options.records) {
|
|
62
|
-
if (record.id !== undefined && (typeof record.id !== 'string' || record.id.length === 0)) {
|
|
63
|
-
throw new Error('record id must be a non-empty string')
|
|
64
|
-
}
|
|
65
|
-
if (record.id !== undefined) {
|
|
66
|
-
if (recordIds.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
67
|
-
recordIds.add(record.id)
|
|
68
|
-
}
|
|
69
|
-
dataset.addRecord(new DatasetRecord(record.inputData, record.expectedOutput, record.metadata, record.id))
|
|
70
|
-
}
|
|
71
|
-
}
|
|
73
|
+
const client = this.#clientForOperation(options.projectName)
|
|
74
|
+
const dataset = new Dataset(client, name, options.description ?? '')
|
|
75
|
+
if ((options.records) != null) dataset.addRecords(options.records)
|
|
72
76
|
return dataset
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
// Pull an existing dataset by name (with its records). Polls with exponential
|
|
76
80
|
// backoff to absorb read-after-write lag; pass `expectedRecordCount` to also
|
|
77
|
-
// wait until that many records are readable.
|
|
81
|
+
// wait until that many records are readable. Pass `tags` to filter records by
|
|
82
|
+
// dataset record tags.
|
|
78
83
|
async pullDataset (name, options = {}) {
|
|
79
|
-
const { expectedRecordCount, maxWaitMs = 30_000, version } = options
|
|
80
|
-
const
|
|
84
|
+
const { expectedRecordCount, maxWaitMs = 30_000, projectName, tags, version } = options
|
|
85
|
+
const filterTags = validateTagsList(tags)
|
|
86
|
+
const client = this.#clientForOperation(projectName)
|
|
87
|
+
const resolvedProjectName = projectName ?? this.#projectName
|
|
88
|
+
const projectId = await client.ensureProjectId()
|
|
81
89
|
|
|
82
90
|
let pulledDataset = null
|
|
83
91
|
let records = []
|
|
84
|
-
|
|
92
|
+
const datasetVersion = version ?? null
|
|
85
93
|
let latestVersion = null
|
|
86
94
|
let lastError = ''
|
|
87
95
|
|
|
88
96
|
const succeeded = await retryWithBackoff(async () => {
|
|
89
97
|
try {
|
|
90
98
|
if (pulledDataset === null) {
|
|
91
|
-
const datasets = await
|
|
99
|
+
const datasets = await client.listDatasets(projectId, { name })
|
|
92
100
|
for (const dataset of datasets) {
|
|
93
101
|
if (dataset.name() === name) {
|
|
94
102
|
pulledDataset = dataset
|
|
95
103
|
latestVersion = dataset.latestVersion()
|
|
96
|
-
datasetVersion = version ?? latestVersion
|
|
97
104
|
break
|
|
98
105
|
}
|
|
99
106
|
}
|
|
@@ -105,8 +112,9 @@ class Experiments {
|
|
|
105
112
|
// Follow the meta.after / page[cursor] pagination until the last page.
|
|
106
113
|
for (;;) {
|
|
107
114
|
// eslint-disable-next-line no-await-in-loop
|
|
108
|
-
const page = await
|
|
115
|
+
const page = await client.listDatasetRecords(projectId, pulledDataset.id(), {
|
|
109
116
|
cursor,
|
|
117
|
+
tags: filterTags,
|
|
110
118
|
version: datasetVersion,
|
|
111
119
|
})
|
|
112
120
|
for (const record of page.records) recs.push(record)
|
|
@@ -124,13 +132,13 @@ class Experiments {
|
|
|
124
132
|
}, { maxTotalMs: maxWaitMs })
|
|
125
133
|
|
|
126
134
|
if (pulledDataset === null && lastError) {
|
|
127
|
-
throw new Error(`Failed to list datasets in project '${
|
|
135
|
+
throw new Error(`Failed to list datasets in project '${resolvedProjectName}': ${lastError}`)
|
|
128
136
|
}
|
|
129
137
|
if (pulledDataset === null) {
|
|
130
|
-
throw new Error(`Dataset '${name}' not found in project '${
|
|
138
|
+
throw new Error(`Dataset '${name}' not found in project '${resolvedProjectName}' (after ${maxWaitMs}ms)`)
|
|
131
139
|
}
|
|
132
140
|
if (!succeeded && lastError) {
|
|
133
|
-
throw new Error(`Failed to fetch records for dataset '${name}' in project '${
|
|
141
|
+
throw new Error(`Failed to fetch records for dataset '${name}' in project '${resolvedProjectName}': ${lastError}`)
|
|
134
142
|
}
|
|
135
143
|
if (!succeeded && expectedRecordCount != null) {
|
|
136
144
|
throw new Error(
|
|
@@ -139,21 +147,45 @@ class Experiments {
|
|
|
139
147
|
)
|
|
140
148
|
}
|
|
141
149
|
|
|
150
|
+
for (const record of records) {
|
|
151
|
+
if (record.id === null || record.id === undefined || record.id === '') {
|
|
152
|
+
throw new Error(`Failed to pull dataset '${name}': backend returned a record without an id`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
return Dataset.fromExisting(
|
|
143
|
-
|
|
157
|
+
client,
|
|
144
158
|
name,
|
|
145
159
|
pulledDataset.description(),
|
|
146
160
|
pulledDataset.id(),
|
|
147
161
|
projectId,
|
|
148
162
|
records,
|
|
149
163
|
datasetVersion,
|
|
150
|
-
latestVersion
|
|
164
|
+
latestVersion,
|
|
165
|
+
filterTags
|
|
151
166
|
)
|
|
152
167
|
}
|
|
153
168
|
|
|
154
|
-
// Build an experiment
|
|
169
|
+
// Build an experiment with a dataset, task, evaluators, and optional project/config/tags.
|
|
155
170
|
experiment (options) {
|
|
156
|
-
|
|
171
|
+
const datasetProjectName = options?.dataset?.projectName?.()
|
|
172
|
+
if (options?.projectName !== undefined &&
|
|
173
|
+
datasetProjectName !== undefined &&
|
|
174
|
+
options.projectName !== datasetProjectName) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Experiment project '${options.projectName}' does not match dataset project '${datasetProjectName}'`
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
const projectName = options?.projectName ?? datasetProjectName
|
|
180
|
+
const client = this.#clientForOperation(projectName)
|
|
181
|
+
const usesDatasetOverride = datasetProjectName !== undefined && datasetProjectName !== this.#projectName
|
|
182
|
+
const resolvedProjectName = projectName ?? this.#config.llmobs?.projectName
|
|
183
|
+
const experimentOptions = options?.projectName === undefined &&
|
|
184
|
+
(usesDatasetOverride || this.#config.llmobs?.projectName !== undefined) &&
|
|
185
|
+
resolvedProjectName !== undefined
|
|
186
|
+
? { ...options, projectName: resolvedProjectName }
|
|
187
|
+
: options
|
|
188
|
+
return new Experiment(client, experimentOptions, this.#llmobs)
|
|
157
189
|
}
|
|
158
190
|
|
|
159
191
|
/**
|
|
@@ -165,10 +197,11 @@ class Experiments {
|
|
|
165
197
|
* @returns {Promise<ExternalExperiment>}
|
|
166
198
|
*/
|
|
167
199
|
startExperiment (options) {
|
|
168
|
-
const client = options?.projectName
|
|
169
|
-
|
|
170
|
-
: this.#
|
|
171
|
-
|
|
200
|
+
const client = this.#clientForOperation(options?.projectName)
|
|
201
|
+
const experimentOptions = options?.projectName === undefined && this.#config.llmobs?.projectName !== undefined
|
|
202
|
+
? { ...options, projectName: this.#config.llmobs.projectName }
|
|
203
|
+
: options
|
|
204
|
+
return new Experiment(client, { ...experimentOptions, external: true }).start()
|
|
172
205
|
.then(experiment => new ExternalExperiment(experiment))
|
|
173
206
|
}
|
|
174
207
|
}
|
|
@@ -183,15 +216,6 @@ function createExperiments (config, llmobs) {
|
|
|
183
216
|
log.warn('LLMObs experiments: missing api and/or app keys, set DD_API_KEY and DD_APP_KEY')
|
|
184
217
|
return new NoopExperiments('DD_API_KEY and DD_APP_KEY are required for experiments')
|
|
185
218
|
}
|
|
186
|
-
if (!config.llmobs?.mlApp && !config.service) {
|
|
187
|
-
const reason = 'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
|
|
188
|
-
'tracer.init()) to name the LLM Obs project, or DD_SERVICE (or service in tracer.init()) as a fallback, ' +
|
|
189
|
-
'then retry'
|
|
190
|
-
const experiments = new Experiments(config, llmobs)
|
|
191
|
-
return new NoopExperiments(reason, {
|
|
192
|
-
startExperiment: (options) => experiments.startExperiment(options),
|
|
193
|
-
})
|
|
194
|
-
}
|
|
195
219
|
return new Experiments(config, llmobs)
|
|
196
220
|
}
|
|
197
221
|
|
|
@@ -7,24 +7,60 @@ const NOOP_EXPERIMENT_ID = '00000000-0000-0000-0000-000000000000'
|
|
|
7
7
|
const NOOP_SPAN_ID = '0000000000000000'
|
|
8
8
|
const NOOP_TRACE_ID = '00000000000000000000000000000000'
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} DatasetRecordNew
|
|
12
|
+
* @property {string} [id]
|
|
13
|
+
* @property {unknown} inputData
|
|
14
|
+
* @property {unknown} [expectedOutput]
|
|
15
|
+
* @property {Record<string, unknown>} [metadata]
|
|
16
|
+
* @property {string[]} [tags]
|
|
17
|
+
*/
|
|
18
|
+
|
|
10
19
|
class NoopDataset {
|
|
11
20
|
#name
|
|
12
21
|
#description
|
|
13
22
|
#records
|
|
23
|
+
#filterTags
|
|
14
24
|
|
|
15
25
|
constructor (name = '', options = {}) {
|
|
16
26
|
this.#name = name
|
|
17
27
|
this.#description = typeof options === 'string' ? options : (options.description ?? '')
|
|
28
|
+
this.#filterTags = typeof options === 'string' ? [] : [...(options.filterTags ?? [])]
|
|
18
29
|
this.#records = (typeof options === 'string' ? [] : (options.records ?? [])).map(record => ({
|
|
19
30
|
id: record.id ?? null,
|
|
20
31
|
input: record.inputData,
|
|
21
32
|
expectedOutput: record.expectedOutput ?? null,
|
|
22
33
|
metadata: record.metadata ?? {},
|
|
34
|
+
tags: [...(record.tags ?? [])],
|
|
23
35
|
}))
|
|
24
36
|
}
|
|
25
37
|
|
|
26
|
-
addRecord (input, expectedOutput, metadata) {
|
|
27
|
-
this.#records.push({
|
|
38
|
+
addRecord (input, expectedOutput, metadata, tags) {
|
|
39
|
+
this.#records.push({
|
|
40
|
+
id: null,
|
|
41
|
+
input,
|
|
42
|
+
expectedOutput: expectedOutput ?? null,
|
|
43
|
+
metadata: metadata ?? {},
|
|
44
|
+
tags: [...(tags ?? [])],
|
|
45
|
+
})
|
|
46
|
+
return this
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add multiple records to a dataset.
|
|
51
|
+
* @param {DatasetRecordNew[]} records
|
|
52
|
+
* @returns {NoopDataset} This dataset for chaining.
|
|
53
|
+
*/
|
|
54
|
+
addRecords (records) {
|
|
55
|
+
for (const record of records) {
|
|
56
|
+
this.#records.push({
|
|
57
|
+
id: record.id ?? null,
|
|
58
|
+
input: record.inputData,
|
|
59
|
+
expectedOutput: record.expectedOutput ?? null,
|
|
60
|
+
metadata: record.metadata ?? {},
|
|
61
|
+
tags: [...(record.tags ?? [])],
|
|
62
|
+
})
|
|
63
|
+
}
|
|
28
64
|
return this
|
|
29
65
|
}
|
|
30
66
|
|
|
@@ -46,6 +82,49 @@ class NoopDataset {
|
|
|
46
82
|
return Promise.resolve({ pushedCount: 0, totalCount: 0 })
|
|
47
83
|
}
|
|
48
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Add tags to a dataset record.
|
|
87
|
+
* @param {number} index Dataset record index.
|
|
88
|
+
* @param {string[]} tags Tags in key:value format.
|
|
89
|
+
* @returns {NoopDataset} This dataset for chaining.
|
|
90
|
+
*/
|
|
91
|
+
addTags (index, tags) {
|
|
92
|
+
const record = this.#records[index]
|
|
93
|
+
if (!record) return this
|
|
94
|
+
const normalizedTags = tags ?? []
|
|
95
|
+
record.tags = [...new Set([...(record.tags ?? []), ...normalizedTags])].sort()
|
|
96
|
+
return this
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove tags from a dataset record.
|
|
101
|
+
* @param {number} index Dataset record index.
|
|
102
|
+
* @param {string[]} tags Tags in key:value format.
|
|
103
|
+
* @returns {NoopDataset} This dataset for chaining.
|
|
104
|
+
*/
|
|
105
|
+
removeTags (index, tags) {
|
|
106
|
+
const record = this.#records[index]
|
|
107
|
+
if (!record) return this
|
|
108
|
+
const normalizedTags = tags ?? []
|
|
109
|
+
const removed = new Set(normalizedTags)
|
|
110
|
+
record.tags = (record.tags ?? []).filter(tag => !removed.has(tag)).sort()
|
|
111
|
+
return this
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Replace all tags on a dataset record.
|
|
116
|
+
* @param {number} index Dataset record index.
|
|
117
|
+
* @param {string[]} tags Tags in key:value format.
|
|
118
|
+
* @returns {NoopDataset} This dataset for chaining.
|
|
119
|
+
*/
|
|
120
|
+
replaceTags (index, tags) {
|
|
121
|
+
const record = this.#records[index]
|
|
122
|
+
if (!record) return this
|
|
123
|
+
const normalizedTags = tags ?? []
|
|
124
|
+
record.tags = [...normalizedTags]
|
|
125
|
+
return this
|
|
126
|
+
}
|
|
127
|
+
|
|
49
128
|
name () {
|
|
50
129
|
return this.#name
|
|
51
130
|
}
|
|
@@ -62,6 +141,10 @@ class NoopDataset {
|
|
|
62
141
|
return null
|
|
63
142
|
}
|
|
64
143
|
|
|
144
|
+
projectName () {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
65
148
|
version () {
|
|
66
149
|
return null
|
|
67
150
|
}
|
|
@@ -70,6 +153,14 @@ class NoopDataset {
|
|
|
70
153
|
return null
|
|
71
154
|
}
|
|
72
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Return the tags used to filter this dataset.
|
|
158
|
+
* @returns {string[]} Dataset record filter tags.
|
|
159
|
+
*/
|
|
160
|
+
filterTags () {
|
|
161
|
+
return [...this.#filterTags]
|
|
162
|
+
}
|
|
163
|
+
|
|
73
164
|
records () {
|
|
74
165
|
return [...this.#records]
|
|
75
166
|
}
|
|
@@ -105,7 +196,13 @@ class NoopExperiment {
|
|
|
105
196
|
}
|
|
106
197
|
|
|
107
198
|
run () {
|
|
108
|
-
return Promise.resolve({
|
|
199
|
+
return Promise.resolve({
|
|
200
|
+
experimentId: null,
|
|
201
|
+
rows: [],
|
|
202
|
+
summaryEvaluations: {},
|
|
203
|
+
runs: [],
|
|
204
|
+
url: null,
|
|
205
|
+
})
|
|
109
206
|
}
|
|
110
207
|
|
|
111
208
|
/**
|
|
@@ -140,11 +237,9 @@ class NoopExperiment {
|
|
|
140
237
|
// throwing, so intentionally disabled experiments remain graceful.
|
|
141
238
|
class NoopExperiments {
|
|
142
239
|
#reason
|
|
143
|
-
#startExperiment
|
|
144
240
|
|
|
145
|
-
constructor (reason
|
|
241
|
+
constructor (reason) {
|
|
146
242
|
this.#reason = reason || 'LLMObs experiments are not available'
|
|
147
|
-
this.#startExperiment = options.startExperiment
|
|
148
243
|
}
|
|
149
244
|
|
|
150
245
|
#warn () {
|
|
@@ -156,9 +251,9 @@ class NoopExperiments {
|
|
|
156
251
|
return new NoopDataset(name, options)
|
|
157
252
|
}
|
|
158
253
|
|
|
159
|
-
pullDataset (name) {
|
|
254
|
+
pullDataset (name, options = {}) {
|
|
160
255
|
this.#warn()
|
|
161
|
-
return Promise.resolve(new NoopDataset(name))
|
|
256
|
+
return Promise.resolve(new NoopDataset(name, { filterTags: options.tags }))
|
|
162
257
|
}
|
|
163
258
|
|
|
164
259
|
experiment (options = {}) {
|
|
@@ -171,10 +266,6 @@ class NoopExperiments {
|
|
|
171
266
|
* @returns {Promise<ExternalExperiment>}
|
|
172
267
|
*/
|
|
173
268
|
startExperiment (options = {}) {
|
|
174
|
-
if (this.#startExperiment !== undefined && options.projectName) {
|
|
175
|
-
return this.#startExperiment(options)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
269
|
this.#warn()
|
|
179
270
|
return Promise.resolve(new ExternalExperiment(new NoopExperiment(options.name, true)))
|
|
180
271
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { randomUUID } = require('node:crypto')
|
|
4
|
+
|
|
3
5
|
const log = require('../../log')
|
|
4
6
|
|
|
5
7
|
// Matches the backend and dd-trace-py evaluator metric label contract.
|
|
@@ -22,6 +24,43 @@ function hasEntries (value) {
|
|
|
22
24
|
return false
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
function validateTagsList (tags) {
|
|
28
|
+
if (tags == null) return []
|
|
29
|
+
if (!Array.isArray(tags)) throw new TypeError('Tags must be an array of strings')
|
|
30
|
+
for (const tag of tags) {
|
|
31
|
+
if (typeof tag !== 'string') throw new TypeError('Each tag must be a string')
|
|
32
|
+
if (tag.indexOf(':') <= 0) {
|
|
33
|
+
throw new Error(`Tag '${tag}' is malformed. Tags must be in 'key:value' format (e.g., 'env:prod').`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return [...tags]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function tagOperationsAreEmpty (operations) {
|
|
40
|
+
return operations == null || (
|
|
41
|
+
!Object.hasOwn(operations, 'replace') &&
|
|
42
|
+
!Object.hasOwn(operations, 'add') &&
|
|
43
|
+
!Object.hasOwn(operations, 'remove')
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {unknown} value
|
|
49
|
+
* @param {string} name
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
function normalizePositiveInteger (value, name) {
|
|
53
|
+
if (!Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer`)
|
|
54
|
+
return value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
function generateRunId () {
|
|
61
|
+
return randomUUID()
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
/**
|
|
26
65
|
* @param {string} name
|
|
27
66
|
*/
|
|
@@ -165,16 +204,20 @@ function stringify (value) {
|
|
|
165
204
|
* @returns {string[]}
|
|
166
205
|
*/
|
|
167
206
|
function buildTags (userTags, autoTags) {
|
|
168
|
-
const
|
|
207
|
+
const tagsByKey = new Map()
|
|
169
208
|
if ((userTags) != null) {
|
|
170
209
|
for (const [key, value] of Object.entries(userTags)) {
|
|
171
|
-
|
|
210
|
+
const values = Array.isArray(value) ? value : [value]
|
|
211
|
+
tagsByKey.set(key, values.map(item => `${key}:${item}`))
|
|
172
212
|
}
|
|
173
213
|
}
|
|
174
214
|
for (const [key, value] of Object.entries(autoTags)) {
|
|
175
|
-
if (value !== undefined && value !== null && value !== '')
|
|
215
|
+
if (value !== undefined && value !== null && value !== '') tagsByKey.set(key, [`${key}:${value}`])
|
|
176
216
|
}
|
|
177
|
-
|
|
217
|
+
|
|
218
|
+
const tags = []
|
|
219
|
+
for (const values of tagsByKey.values()) tags.push(...values)
|
|
220
|
+
return tags
|
|
178
221
|
}
|
|
179
222
|
|
|
180
223
|
/**
|
|
@@ -186,6 +229,35 @@ function mergeTags (baseTags, overrideTags) {
|
|
|
186
229
|
return { ...baseTags, ...overrideTags }
|
|
187
230
|
}
|
|
188
231
|
|
|
232
|
+
/**
|
|
233
|
+
* @param {string[] | undefined} tags
|
|
234
|
+
* @returns {Record<string, string | string[]>}
|
|
235
|
+
*/
|
|
236
|
+
function recordTagsToObject (tags) {
|
|
237
|
+
const result = {}
|
|
238
|
+
if (!Array.isArray(tags)) return result
|
|
239
|
+
for (const tag of tags) {
|
|
240
|
+
const separator = tag.indexOf(':')
|
|
241
|
+
if (separator <= 0) continue
|
|
242
|
+
|
|
243
|
+
const key = tag.slice(0, separator)
|
|
244
|
+
const value = tag.slice(separator + 1)
|
|
245
|
+
if (!Object.hasOwn(result, key)) {
|
|
246
|
+
Object.defineProperty(result, key, {
|
|
247
|
+
configurable: true,
|
|
248
|
+
enumerable: true,
|
|
249
|
+
value,
|
|
250
|
+
writable: true,
|
|
251
|
+
})
|
|
252
|
+
} else if (Array.isArray(result[key])) {
|
|
253
|
+
result[key].push(value)
|
|
254
|
+
} else {
|
|
255
|
+
result[key] = [result[key], value]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return result
|
|
259
|
+
}
|
|
260
|
+
|
|
189
261
|
/**
|
|
190
262
|
* @param {number} ms
|
|
191
263
|
* @returns {Promise<void>}
|
|
@@ -244,13 +316,18 @@ module.exports = {
|
|
|
244
316
|
buildSpanMetadata,
|
|
245
317
|
buildTags,
|
|
246
318
|
durationNs,
|
|
319
|
+
generateRunId,
|
|
247
320
|
hasEntries,
|
|
248
321
|
inferMetricType,
|
|
249
322
|
mergeTags,
|
|
250
323
|
normalizeEvaluators,
|
|
251
324
|
normalizeJsonMetricValue,
|
|
325
|
+
normalizePositiveInteger,
|
|
326
|
+
recordTagsToObject,
|
|
252
327
|
sleep,
|
|
253
328
|
stringify,
|
|
329
|
+
tagOperationsAreEmpty,
|
|
254
330
|
timestampMs,
|
|
255
331
|
validateEvaluatorName,
|
|
332
|
+
validateTagsList,
|
|
256
333
|
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
const { channel } = require('dc-polyfill')
|
|
4
4
|
|
|
5
5
|
const { readDatadogTags, writeDatadogTags } = require('../carrier')
|
|
6
|
+
const { registerTelemetryFlusher } = require('../flush')
|
|
6
7
|
const log = require('../log')
|
|
8
|
+
const { createServerlessDeliveryTracker } = require('../serverless')
|
|
7
9
|
const { DD_MAJOR } = require('../../../../version')
|
|
8
10
|
const startupLogs = require('../startup-log')
|
|
9
11
|
const {
|
|
@@ -57,6 +59,8 @@ let spanWriter
|
|
|
57
59
|
/** @type {LLMObsEvalMetricsWriter | null} */
|
|
58
60
|
let evalWriter
|
|
59
61
|
|
|
62
|
+
let unregisterTelemetryFlusher
|
|
63
|
+
|
|
60
64
|
/** @type {import('../config/config-base')} */
|
|
61
65
|
let globalTracerConfig
|
|
62
66
|
|
|
@@ -66,23 +70,36 @@ let globalTracerConfig
|
|
|
66
70
|
function enable (config) {
|
|
67
71
|
globalTracerConfig = config
|
|
68
72
|
|
|
73
|
+
const retiredSpanWriter = spanWriter
|
|
74
|
+
const retiredEvalWriter = evalWriter
|
|
75
|
+
const isReinitializing = Boolean(retiredSpanWriter || retiredEvalWriter)
|
|
76
|
+
unregisterTelemetryFlusher?.()
|
|
77
|
+
retireWriters(retiredSpanWriter, retiredEvalWriter)
|
|
78
|
+
|
|
69
79
|
const startTime = performance.now()
|
|
70
80
|
// create writers and eval writer append and flush channels
|
|
71
81
|
// span writer append is handled by the span processor
|
|
72
82
|
evalWriter = new LLMObsEvalMetricsWriter(config)
|
|
73
83
|
spanWriter = new LLMObsSpanWriter(config)
|
|
84
|
+
const currentEvalWriter = evalWriter
|
|
85
|
+
const currentSpanWriter = spanWriter
|
|
86
|
+
unregisterTelemetryFlusher = registerTelemetryFlusher(done => {
|
|
87
|
+
flushWriters(done, currentSpanWriter, currentEvalWriter)
|
|
88
|
+
})
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
if (!isReinitializing) {
|
|
91
|
+
evalMetricAppendCh.subscribe(handleEvalMetricAppend)
|
|
92
|
+
flushCh.subscribe(handleFlush)
|
|
93
|
+
registerUserSpanProcessorCh.subscribe(handleRegisterProcessor)
|
|
94
|
+
}
|
|
78
95
|
|
|
79
96
|
// span processing
|
|
80
97
|
spanProcessor = new LLMObsSpanProcessor(config)
|
|
81
98
|
spanProcessor.setWriter(spanWriter)
|
|
82
|
-
spanFinishCh.subscribe(handleSpanProcess)
|
|
99
|
+
if (!isReinitializing) spanFinishCh.subscribe(handleSpanProcess)
|
|
83
100
|
|
|
84
101
|
// distributed tracing for llmobs
|
|
85
|
-
injectCh.subscribe(handleLLMObsInjection)
|
|
102
|
+
if (!isReinitializing) injectCh.subscribe(handleLLMObsInjection)
|
|
86
103
|
|
|
87
104
|
setAgentStrategy(config, useAgentless => {
|
|
88
105
|
if (useAgentless && !(config.DD_API_KEY && config.site)) {
|
|
@@ -94,8 +111,10 @@ function enable (config) {
|
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
// A disable can happen while transport selection is still pending. Keep
|
|
115
|
+
// configuring these writers so their queued lifecycle flushes can drain.
|
|
116
|
+
currentEvalWriter.setAgentless(useAgentless)
|
|
117
|
+
currentSpanWriter.setAgentless(useAgentless)
|
|
99
118
|
|
|
100
119
|
telemetry.recordLLMObsEnabled(startTime, config)
|
|
101
120
|
log.debug('[LLMObs] Enabled LLM Observability with configuration: %o', config.llmobs)
|
|
@@ -109,16 +128,39 @@ function disable () {
|
|
|
109
128
|
if (injectCh.hasSubscribers) injectCh.unsubscribe(handleLLMObsInjection)
|
|
110
129
|
if (registerUserSpanProcessorCh.hasSubscribers) registerUserSpanProcessorCh.unsubscribe(handleRegisterProcessor)
|
|
111
130
|
|
|
112
|
-
spanWriter
|
|
113
|
-
evalWriter
|
|
131
|
+
const retiredSpanWriter = spanWriter
|
|
132
|
+
const retiredEvalWriter = evalWriter
|
|
114
133
|
spanProcessor?.setWriter(null)
|
|
134
|
+
unregisterTelemetryFlusher?.()
|
|
135
|
+
unregisterTelemetryFlusher = undefined
|
|
115
136
|
|
|
116
137
|
spanWriter = null
|
|
117
138
|
evalWriter = null
|
|
118
139
|
|
|
140
|
+
retireWriters(retiredSpanWriter, retiredEvalWriter)
|
|
141
|
+
|
|
119
142
|
log.debug('[LLMObs] Disabled LLM Observability')
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Keeps retired writers reachable until their destroy-triggered deliveries complete.
|
|
147
|
+
* @param {LLMObsSpanWriter | null} retiredSpanWriter
|
|
148
|
+
* @param {LLMObsEvalMetricsWriter | null} retiredEvalWriter
|
|
149
|
+
* @returns {void}
|
|
150
|
+
*/
|
|
151
|
+
function retireWriters (retiredSpanWriter, retiredEvalWriter) {
|
|
152
|
+
const retiredWriters = [retiredSpanWriter, retiredEvalWriter].filter(Boolean)
|
|
153
|
+
if (retiredWriters.length === 0) return
|
|
154
|
+
let remainingWriters = retiredWriters.length
|
|
155
|
+
const unregisterRetiredFlusher = registerTelemetryFlusher(done => {
|
|
156
|
+
flushWriters(done, retiredSpanWriter, retiredEvalWriter)
|
|
157
|
+
})
|
|
158
|
+
function onWriterDestroyed () {
|
|
159
|
+
if (--remainingWriters === 0) unregisterRetiredFlusher?.()
|
|
160
|
+
}
|
|
161
|
+
for (const writer of retiredWriters) writer.destroy(onWriterDestroyed)
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
// since LLMObs traces can extend between services and be the same trace,
|
|
123
165
|
// we need to propagate the parent id, mlApp, session id, and sampling rate/decision.
|
|
124
166
|
function handleLLMObsInjection ({ carrier }) {
|
|
@@ -192,15 +234,36 @@ function handleLLMObsInjection ({ carrier }) {
|
|
|
192
234
|
if (tags !== existing) writeDatadogTags(carrier, tags)
|
|
193
235
|
}
|
|
194
236
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Flushes the specified LLMObs writers and joins deliveries active at the boundary.
|
|
239
|
+
* @param {Function} [done]
|
|
240
|
+
* @param {LLMObsSpanWriter | null} [currentSpanWriter]
|
|
241
|
+
* @param {LLMObsEvalMetricsWriter | null} [currentEvalWriter]
|
|
242
|
+
* @returns {boolean} `true` when a writer throws synchronously.
|
|
243
|
+
*/
|
|
244
|
+
function flushWriters (done, currentSpanWriter = spanWriter, currentEvalWriter = evalWriter) {
|
|
245
|
+
let failed = false
|
|
246
|
+
const deliveryTracker = createServerlessDeliveryTracker()
|
|
247
|
+
const flush = writer => {
|
|
248
|
+
try {
|
|
249
|
+
if (deliveryTracker && writer) deliveryTracker.track(complete => writer.flush(complete))
|
|
250
|
+
// Non-serverless flushes retain the existing writer behavior.
|
|
251
|
+
else writer?.flush()
|
|
252
|
+
} catch (error) {
|
|
253
|
+
failed = true
|
|
254
|
+
log.warn('Failed to flush LLMObs writer:', error.message)
|
|
255
|
+
}
|
|
203
256
|
}
|
|
257
|
+
|
|
258
|
+
flush(currentSpanWriter)
|
|
259
|
+
flush(currentEvalWriter)
|
|
260
|
+
deliveryTracker?.waitForIdle(done)
|
|
261
|
+
if (!deliveryTracker) done?.()
|
|
262
|
+
return failed
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function handleFlush () {
|
|
266
|
+
const err = flushWriters() ? 'writer_flush_error' : ''
|
|
204
267
|
telemetry.recordUserFlush(err)
|
|
205
268
|
}
|
|
206
269
|
|