dd-trace 6.10.0 → 6.12.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/README.md +0 -17
- package/index.d.ts +128 -6
- package/initialize.mjs +28 -17
- package/openfeature.js +2 -2
- package/package.json +27 -23
- package/packages/datadog-esbuild/index.js +0 -34
- package/packages/datadog-instrumentations/src/anthropic.js +210 -15
- package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
- package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
- package/packages/datadog-instrumentations/src/bunyan.js +3 -16
- package/packages/datadog-instrumentations/src/cucumber.js +30 -15
- package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
- package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
- package/packages/datadog-instrumentations/src/cypress.js +2 -0
- package/packages/datadog-instrumentations/src/fastify.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +9 -4
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
- package/packages/datadog-instrumentations/src/jest.js +25 -17
- package/packages/datadog-instrumentations/src/mariadb.js +278 -21
- package/packages/datadog-instrumentations/src/mocha/main.js +539 -55
- package/packages/datadog-instrumentations/src/mocha/utils.js +135 -19
- package/packages/datadog-instrumentations/src/playwright-reporter.js +247 -0
- package/packages/datadog-instrumentations/src/playwright.js +182 -35
- package/packages/datadog-instrumentations/src/router.js +2 -0
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +6 -8
- package/packages/datadog-instrumentations/src/vitest-main.js +11 -3
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -8
- package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
- package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
- package/packages/datadog-plugin-bunyan/src/index.js +5 -8
- package/packages/datadog-plugin-cucumber/src/index.js +11 -0
- package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
- package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +81 -29
- package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
- package/packages/datadog-plugin-cypress/src/index.js +12 -6
- package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
- package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
- package/packages/datadog-plugin-electron/src/index.js +2 -0
- package/packages/datadog-plugin-fs/src/index.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +6 -0
- package/packages/datadog-plugin-mocha/src/index.js +56 -1
- package/packages/datadog-plugin-nats/src/index.js +1 -1
- package/packages/datadog-plugin-openai/src/tracing.js +41 -30
- package/packages/datadog-plugin-openai-agents/src/integration.js +1 -0
- package/packages/datadog-plugin-playwright/src/index.js +46 -13
- package/packages/datadog-plugin-vitest/src/index.js +6 -0
- package/packages/datadog-webpack/index.js +0 -11
- package/packages/dd-trace/index.js +0 -1
- package/packages/dd-trace/src/agent/info.js +6 -5
- package/packages/dd-trace/src/aiguard/client.js +100 -1
- package/packages/dd-trace/src/aiguard/errors.js +41 -0
- package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
- package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
- package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
- package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
- package/packages/dd-trace/src/aiguard/sdk.js +22 -278
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
- package/packages/dd-trace/src/appsec/reporter.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/dynamic-instrumentation/worker/index.js +1 -0
- 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 +29 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +27 -9
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +26 -8
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
- package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -4
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
- package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
- 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 +10 -4
- 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 +4 -0
- package/packages/dd-trace/src/config/index.js +13 -2
- package/packages/dd-trace/src/config/major-overrides.js +7 -0
- package/packages/dd-trace/src/config/supported-configurations.json +14 -0
- package/packages/dd-trace/src/datastreams/index.js +2 -0
- package/packages/dd-trace/src/datastreams/manager.js +9 -2
- package/packages/dd-trace/src/datastreams/processor.js +15 -13
- package/packages/dd-trace/src/datastreams/size.js +5 -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 +5 -11
- 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/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/writer.js +49 -9
- package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
- package/packages/dd-trace/src/exporters/common/request.js +5 -2
- package/packages/dd-trace/src/exporters/common/writer.js +11 -6
- package/packages/dd-trace/src/llmobs/experiments/client.js +167 -5
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +484 -92
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +302 -29
- package/packages/dd-trace/src/llmobs/experiments/index.js +75 -64
- package/packages/dd-trace/src/llmobs/experiments/noop.js +135 -7
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +101 -9
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
- package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
- package/packages/dd-trace/src/noop/proxy.js +3 -4
- package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
- package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
- package/packages/dd-trace/src/openfeature/index.js +7 -4
- package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
- package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
- package/packages/dd-trace/src/openfeature/writers/exposures.js +62 -14
- package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
- package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
- package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +22 -13
- package/packages/dd-trace/src/opentracing/tracer.js +5 -2
- package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
- package/packages/dd-trace/src/plugin_manager.js +3 -3
- package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/jest.js +4 -3
- package/packages/dd-trace/src/plugins/util/test.js +9 -9
- package/packages/dd-trace/src/plugins/util/web.js +3 -0
- package/packages/dd-trace/src/profiling/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
- package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
- package/packages/dd-trace/src/proxy.js +38 -29
- package/packages/dd-trace/src/ritm.js +9 -2
- package/packages/dd-trace/src/serverless.js +37 -0
- package/packages/dd-trace/src/span_format.js +1 -0
- package/packages/dd-trace/src/span_stats.js +30 -3
- package/packages/dd-trace/src/storage-channels.js +86 -0
- 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 +9 -2
- package/packages/dd-trace/src/tracer_metadata.js +14 -1
- package/packages/dd-trace/src/web-tags-cache.js +132 -0
- package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
- 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
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
- package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
- package/packages/dd-trace/src/feature-registry.js +0 -29
- package/packages/dd-trace/src/openfeature/register.js +0 -35
- package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const id = require('../../id')
|
|
4
|
+
const log = require('../../log')
|
|
4
5
|
|
|
5
|
-
const { API_BASE_PATH } = require('./client')
|
|
6
6
|
const { Row, ExperimentResult, ExperimentRun } = require('./result')
|
|
7
7
|
const {
|
|
8
|
-
buildExperimentTagObject,
|
|
9
8
|
buildSpanMetadata,
|
|
10
9
|
buildTags,
|
|
10
|
+
durationNs,
|
|
11
11
|
hasEntries,
|
|
12
12
|
inferMetricType,
|
|
13
13
|
normalizeEvaluators,
|
|
14
|
+
mergeTags,
|
|
14
15
|
normalizeJsonMetricValue,
|
|
16
|
+
recordTagsToObject,
|
|
15
17
|
sleep,
|
|
16
18
|
stringify,
|
|
19
|
+
timestampMs,
|
|
17
20
|
validateEvaluatorName,
|
|
18
21
|
} = require('./util')
|
|
19
22
|
|
|
20
23
|
// One span per experiment row (LLM Obs experiment span wire format).
|
|
21
|
-
function toSpan (row, metadata, ids, spanName, userTags) {
|
|
24
|
+
function toSpan (row, metadata, ids, spanName, userTags, recordTags) {
|
|
22
25
|
const meta = {
|
|
23
26
|
input: row.input ?? null,
|
|
24
27
|
output: row.output ?? null,
|
|
@@ -28,9 +31,23 @@ function toSpan (row, metadata, ids, spanName, userTags) {
|
|
|
28
31
|
meta.metadata = metadata
|
|
29
32
|
}
|
|
30
33
|
if (row.isError) {
|
|
31
|
-
meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: '' }
|
|
34
|
+
meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: row.errorStack ?? '' }
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
const tags = buildTags({
|
|
38
|
+
...userTags,
|
|
39
|
+
...recordTagsToObject(recordTags),
|
|
40
|
+
}, {
|
|
41
|
+
experiment_id: ids.experimentId,
|
|
42
|
+
run_id: ids.runId,
|
|
43
|
+
run_iteration: ids.runIteration,
|
|
44
|
+
project_id: ids.projectId,
|
|
45
|
+
dataset_id: ids.datasetId,
|
|
46
|
+
dataset_record_id: ids.datasetRecordId,
|
|
47
|
+
dataset_name: ids.datasetName,
|
|
48
|
+
experiment_name: ids.experimentName,
|
|
49
|
+
})
|
|
50
|
+
|
|
34
51
|
return {
|
|
35
52
|
span_id: row.spanId,
|
|
36
53
|
trace_id: row.traceId,
|
|
@@ -41,13 +58,7 @@ function toSpan (row, metadata, ids, spanName, userTags) {
|
|
|
41
58
|
duration: row.durationNs,
|
|
42
59
|
status: row.isError ? 'error' : 'ok',
|
|
43
60
|
meta,
|
|
44
|
-
tags
|
|
45
|
-
experiment_id: ids.experimentId,
|
|
46
|
-
run_id: ids.runId,
|
|
47
|
-
run_iteration: ids.runIteration,
|
|
48
|
-
dataset_id: ids.datasetId,
|
|
49
|
-
dataset_record_id: ids.datasetRecordId,
|
|
50
|
-
}),
|
|
61
|
+
tags,
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
@@ -91,11 +102,34 @@ function createFallbackSpanContext (startNs) {
|
|
|
91
102
|
return { spanId, traceId }
|
|
92
103
|
}
|
|
93
104
|
|
|
105
|
+
function normalizeError (error) {
|
|
106
|
+
if (error == null) {
|
|
107
|
+
return { errorType: null, errorMessage: null, errorStack: '' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof error === 'string') {
|
|
111
|
+
return { errorType: 'Error', errorMessage: error, errorStack: '' }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
errorType: error.type ?? error.name ?? 'Error',
|
|
116
|
+
errorMessage: error.message ?? String(error),
|
|
117
|
+
errorStack: error.stack ?? '',
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function errorMessage (error) {
|
|
122
|
+
if (error == null) return null
|
|
123
|
+
if (typeof error === 'string') return error
|
|
124
|
+
return error.message ?? String(error)
|
|
125
|
+
}
|
|
126
|
+
|
|
94
127
|
// Builder + run() orchestration: runs rows sequentially, emits one root span
|
|
95
128
|
// per dataset row, and posts spans + metrics to the experiments events API.
|
|
96
129
|
class Experiment {
|
|
97
130
|
#client
|
|
98
131
|
#llmobs
|
|
132
|
+
#external
|
|
99
133
|
#name
|
|
100
134
|
#description
|
|
101
135
|
#dataset
|
|
@@ -104,24 +138,37 @@ class Experiment {
|
|
|
104
138
|
#summaryEvaluators
|
|
105
139
|
#config
|
|
106
140
|
#tags
|
|
141
|
+
#metadata
|
|
142
|
+
#projectId
|
|
107
143
|
#experimentId
|
|
144
|
+
#runId
|
|
145
|
+
#runIteration
|
|
108
146
|
|
|
109
147
|
constructor (client, options = {}, llmobs) {
|
|
110
148
|
if (!options.name) throw new Error('Experiment name is required')
|
|
111
|
-
|
|
112
|
-
if (
|
|
149
|
+
this.#external = options.external === true
|
|
150
|
+
if (!this.#external) {
|
|
151
|
+
if (!options.dataset) throw new Error('Experiment dataset is required')
|
|
152
|
+
if (typeof options.task !== 'function') throw new Error('Experiment task is required')
|
|
153
|
+
}
|
|
113
154
|
|
|
114
155
|
this.#client = client
|
|
115
156
|
this.#llmobs = llmobs
|
|
116
157
|
this.#name = options.name
|
|
117
158
|
this.#description = options.description ?? ''
|
|
118
|
-
this.#dataset = options.dataset
|
|
159
|
+
this.#dataset = options.dataset ?? {}
|
|
119
160
|
this.#task = options.task
|
|
120
161
|
this.#evaluators = normalizeEvaluators(options.evaluators, 'row')
|
|
121
162
|
this.#summaryEvaluators = normalizeEvaluators(options.summaryEvaluators, 'summary')
|
|
122
163
|
this.#config = { ...options.config }
|
|
164
|
+
const filterTags = this.#dataset.filterTags?.() ?? []
|
|
165
|
+
if (filterTags.length > 0) this.#config.filtered_record_tags = filterTags
|
|
123
166
|
this.#tags = { ...options.tags }
|
|
167
|
+
this.#metadata = { ...options.metadata }
|
|
168
|
+
this.#projectId = null
|
|
124
169
|
this.#experimentId = null
|
|
170
|
+
this.#runId = null
|
|
171
|
+
this.#runIteration = null
|
|
125
172
|
}
|
|
126
173
|
|
|
127
174
|
name () {
|
|
@@ -137,7 +184,177 @@ class Experiment {
|
|
|
137
184
|
return `${this.#client.appBase}/llm/experiments/${this.#experimentId}`
|
|
138
185
|
}
|
|
139
186
|
|
|
187
|
+
/**
|
|
188
|
+
* @returns {Promise<Experiment>}
|
|
189
|
+
*/
|
|
190
|
+
async start () {
|
|
191
|
+
if (!this.#external) throw new Error('Experiment is not externally driven')
|
|
192
|
+
|
|
193
|
+
this.#projectId = await this.#client.ensureProjectId()
|
|
194
|
+
|
|
195
|
+
const dataset = await this.#ensureExternalDataset()
|
|
196
|
+
const attributes = {
|
|
197
|
+
name: this.#name,
|
|
198
|
+
project_id: this.#projectId,
|
|
199
|
+
dataset_id: dataset.id,
|
|
200
|
+
description: this.#description,
|
|
201
|
+
ensure_unique: true,
|
|
202
|
+
run_count: 1,
|
|
203
|
+
}
|
|
204
|
+
if (dataset.version != null) attributes.dataset_version = dataset.version
|
|
205
|
+
if (hasEntries(this.#config)) attributes.config = this.#config
|
|
206
|
+
|
|
207
|
+
const metadata = { ...this.#metadata }
|
|
208
|
+
if (hasEntries(this.#tags)) metadata.tags = buildTags(this.#tags, {})
|
|
209
|
+
if (hasEntries(metadata)) attributes.metadata = metadata
|
|
210
|
+
|
|
211
|
+
let created
|
|
212
|
+
try {
|
|
213
|
+
created = await this.#client.createExperiment(attributes)
|
|
214
|
+
} catch (err) {
|
|
215
|
+
throw new Error(`Failed to create experiment '${this.#name}': ${err.message}`)
|
|
216
|
+
}
|
|
217
|
+
this.#experimentId = created.experimentId
|
|
218
|
+
this.#runId = id().toString(16).padStart(16, '0')
|
|
219
|
+
this.#runIteration = 0
|
|
220
|
+
return this
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async #ensureExternalDataset () {
|
|
224
|
+
if (this.#dataset.id) {
|
|
225
|
+
return { id: this.#dataset.id, version: this.#dataset.version }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const name = this.#dataset.name ?? `${this.#name} dataset`
|
|
229
|
+
const description = this.#dataset.description ??
|
|
230
|
+
`Placeholder dataset for externally-driven experiment '${this.#name}'`
|
|
231
|
+
let response
|
|
232
|
+
try {
|
|
233
|
+
response = await this.#client.createDataset(this.#projectId, { name, description })
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw new Error(`Failed to create placeholder dataset '${name}': ${err.message}`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const id = response.id()
|
|
239
|
+
if (id === null) {
|
|
240
|
+
throw new Error(`Failed to create placeholder dataset '${name}': backend response is missing dataset id`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.#dataset = {
|
|
244
|
+
...this.#dataset,
|
|
245
|
+
id,
|
|
246
|
+
version: this.#dataset.version ?? response.version(),
|
|
247
|
+
}
|
|
248
|
+
return { id: this.#dataset.id, version: this.#dataset.version }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {object} input
|
|
253
|
+
* @returns {Promise<{experimentId: string, spanId: string, traceId: string, url: string | null}>}
|
|
254
|
+
*/
|
|
255
|
+
async submitSpan (input = {}) {
|
|
256
|
+
if (this.#experimentId === null || this.#projectId === null) {
|
|
257
|
+
throw new Error('Experiment has not been started')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const startMs = timestampMs(input.startedAt)
|
|
261
|
+
const startNs = Math.round(startMs * 1e6)
|
|
262
|
+
const { spanId, traceId } = createFallbackSpanContext(startNs)
|
|
263
|
+
const error = normalizeError(input.error)
|
|
264
|
+
const row = new Row({
|
|
265
|
+
spanId,
|
|
266
|
+
traceId,
|
|
267
|
+
startNs,
|
|
268
|
+
durationNs: durationNs(input, startMs),
|
|
269
|
+
input: input.input,
|
|
270
|
+
output: input.output,
|
|
271
|
+
expectedOutput: input.expectedOutput,
|
|
272
|
+
errorType: error.errorType,
|
|
273
|
+
errorMessage: error.errorMessage,
|
|
274
|
+
errorStack: error.errorStack,
|
|
275
|
+
evaluations: {},
|
|
276
|
+
evaluationErrors: {},
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const span = toSpan(row, input.metadata, {
|
|
280
|
+
experimentId: this.#experimentId,
|
|
281
|
+
projectId: this.#projectId,
|
|
282
|
+
datasetId: this.#dataset.id,
|
|
283
|
+
datasetRecordId: input.datasetRecordId,
|
|
284
|
+
runId: input.runId ?? this.#runId,
|
|
285
|
+
runIteration: input.runIteration ?? this.#runIteration,
|
|
286
|
+
}, input.name ?? this.#name, mergeTags(this.#tags, input.tags))
|
|
287
|
+
|
|
288
|
+
await this.#postEvents(this.#experimentId, [span], [])
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
experimentId: this.#experimentId,
|
|
292
|
+
spanId,
|
|
293
|
+
traceId,
|
|
294
|
+
url: this.url(),
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @param {{experimentId?: string, spanId: string, traceId?: string}} span
|
|
300
|
+
* @param {object[]} metrics
|
|
301
|
+
* @returns {Promise<void>}
|
|
302
|
+
*/
|
|
303
|
+
async submitEvaluationMetrics (span, metrics) {
|
|
304
|
+
if (this.#experimentId === null) {
|
|
305
|
+
throw new Error('Experiment has not been started')
|
|
306
|
+
}
|
|
307
|
+
if (!span?.spanId) {
|
|
308
|
+
throw new Error('Experiment span id is required')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const experimentId = span.experimentId ?? this.#experimentId
|
|
312
|
+
if (experimentId !== this.#experimentId) {
|
|
313
|
+
throw new Error(`Experiment span belongs to '${experimentId}', not '${this.#experimentId}'`)
|
|
314
|
+
}
|
|
315
|
+
if (!span.traceId) {
|
|
316
|
+
throw new Error('Experiment trace id is required')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const payload = []
|
|
320
|
+
for (const metric of metrics) {
|
|
321
|
+
validateEvaluatorName(metric.label)
|
|
322
|
+
const metricError = errorMessage(metric.error)
|
|
323
|
+
if (metric.value === undefined && metricError == null) {
|
|
324
|
+
log.warn('LLMObs experiments: skipping external metric %s because it has neither value nor error', metric.label)
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
payload.push(toMetric(
|
|
328
|
+
metric.label,
|
|
329
|
+
metric.value,
|
|
330
|
+
metricError,
|
|
331
|
+
span.spanId,
|
|
332
|
+
span.traceId,
|
|
333
|
+
timestampMs(metric.timestamp),
|
|
334
|
+
experimentId,
|
|
335
|
+
mergeTags(this.#tags, metric.tags),
|
|
336
|
+
metric.source ?? 'custom'
|
|
337
|
+
))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (payload.length === 0) return
|
|
341
|
+
await this.#postEvents(experimentId, [], payload)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {object} options
|
|
346
|
+
* @returns {Promise<void>}
|
|
347
|
+
*/
|
|
348
|
+
async close (options = {}) {
|
|
349
|
+
if (this.#experimentId === null) return
|
|
350
|
+
const error = errorMessage(options.error)
|
|
351
|
+
const status = options.status ?? (error === null ? 'completed' : 'failed')
|
|
352
|
+
await this.#updateStatus(this.#experimentId, status, error, false)
|
|
353
|
+
}
|
|
354
|
+
|
|
140
355
|
async run (options = {}) {
|
|
356
|
+
if (this.#external) throw new Error('Externally-driven experiments cannot run local tasks')
|
|
357
|
+
|
|
141
358
|
const {
|
|
142
359
|
maxRetries = 0,
|
|
143
360
|
retryDelay = (attempt) => 100 * (attempt + 1),
|
|
@@ -168,17 +385,16 @@ class Experiment {
|
|
|
168
385
|
}
|
|
169
386
|
const datasetVersion = this.#dataset.version()
|
|
170
387
|
if (datasetVersion !== null) attributes.dataset_version = datasetVersion
|
|
388
|
+
// eslint-disable-next-line no-restricted-syntax -- faster than tracking entries while copying arbitrary config
|
|
171
389
|
if (Object.keys(this.#config).length > 0) attributes.config = this.#config
|
|
172
390
|
|
|
173
391
|
let created
|
|
174
392
|
try {
|
|
175
|
-
created = await this.#client.
|
|
176
|
-
data: { type: 'experiments', attributes },
|
|
177
|
-
})
|
|
393
|
+
created = await this.#client.createExperiment(attributes)
|
|
178
394
|
} catch (err) {
|
|
179
395
|
throw new Error(`Failed to create experiment '${this.#name}': ${err.message}`)
|
|
180
396
|
}
|
|
181
|
-
this.#experimentId = created
|
|
397
|
+
this.#experimentId = created.experimentId
|
|
182
398
|
const experimentId = this.#experimentId
|
|
183
399
|
const runId = id().toString(16).padStart(16, '0')
|
|
184
400
|
const runIteration = 0
|
|
@@ -249,9 +465,11 @@ class Experiment {
|
|
|
249
465
|
projectId,
|
|
250
466
|
datasetId,
|
|
251
467
|
datasetRecordId,
|
|
468
|
+
datasetName: this.#dataset.name(),
|
|
469
|
+
experimentName: this.#name,
|
|
252
470
|
runId,
|
|
253
471
|
runIteration,
|
|
254
|
-
}, this.#task.name || this.#name, this.#tags))
|
|
472
|
+
}, this.#task.name || this.#name, this.#tags, record.tags))
|
|
255
473
|
}
|
|
256
474
|
}
|
|
257
475
|
|
|
@@ -316,7 +534,7 @@ class Experiment {
|
|
|
316
534
|
dataset_name: this.#dataset.name(),
|
|
317
535
|
experiment_name: this.#name,
|
|
318
536
|
}
|
|
319
|
-
const tags =
|
|
537
|
+
const tags = mergeTags(this.#tags, { ...recordTagsToObject(record.tags), ...autoTags })
|
|
320
538
|
|
|
321
539
|
const execute = () => this.#runWithRetries(
|
|
322
540
|
() => this.#task(record.input, this.#config, record.metadata),
|
|
@@ -454,24 +672,79 @@ class Experiment {
|
|
|
454
672
|
async #postEvents (experimentId, spans, metrics) {
|
|
455
673
|
const attributes = { metrics }
|
|
456
674
|
if (spans.length > 0) attributes.spans = spans
|
|
457
|
-
await this.#client.
|
|
458
|
-
data: { type: 'experiments', attributes },
|
|
459
|
-
})
|
|
675
|
+
await this.#client.postExperimentEvents(experimentId, attributes)
|
|
460
676
|
}
|
|
461
677
|
|
|
462
|
-
async #updateStatus (experimentId, status, error) {
|
|
678
|
+
async #updateStatus (experimentId, status, error, suppressErrors = true) {
|
|
463
679
|
if (!experimentId) return
|
|
464
680
|
// The experiment-update model has no status field, so this is a direct PATCH.
|
|
465
681
|
const attributes = { status }
|
|
466
682
|
if (error !== null) attributes.error = error
|
|
467
683
|
try {
|
|
468
|
-
await this.#client.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
} catch {
|
|
684
|
+
await this.#client.updateExperiment(experimentId, attributes)
|
|
685
|
+
} catch (err) {
|
|
686
|
+
if (!suppressErrors) throw err
|
|
472
687
|
// Status update is best-effort; never let it mask the real result/error.
|
|
473
688
|
}
|
|
474
689
|
}
|
|
475
690
|
}
|
|
476
691
|
|
|
477
|
-
|
|
692
|
+
class ExternalExperiment {
|
|
693
|
+
#experiment
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* @param {Pick<Experiment, 'name' | 'experimentId' | 'url' | 'submitSpan' | 'submitEvaluationMetrics' |
|
|
697
|
+
* 'close'>} experiment
|
|
698
|
+
*/
|
|
699
|
+
constructor (experiment) {
|
|
700
|
+
this.#experiment = experiment
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* @returns {string}
|
|
705
|
+
*/
|
|
706
|
+
name () {
|
|
707
|
+
return this.#experiment.name()
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* @returns {string | null}
|
|
712
|
+
*/
|
|
713
|
+
experimentId () {
|
|
714
|
+
return this.#experiment.experimentId()
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* @returns {string | null}
|
|
719
|
+
*/
|
|
720
|
+
url () {
|
|
721
|
+
return this.#experiment.url()
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* @param {object} [input]
|
|
726
|
+
* @returns {Promise<object>}
|
|
727
|
+
*/
|
|
728
|
+
submitSpan (input) {
|
|
729
|
+
return this.#experiment.submitSpan(input)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* @param {object} span
|
|
734
|
+
* @param {object[]} metrics
|
|
735
|
+
* @returns {Promise<void>}
|
|
736
|
+
*/
|
|
737
|
+
submitEvaluationMetrics (span, metrics) {
|
|
738
|
+
return this.#experiment.submitEvaluationMetrics(span, metrics)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* @param {object} [options]
|
|
743
|
+
* @returns {Promise<void>}
|
|
744
|
+
*/
|
|
745
|
+
close (options) {
|
|
746
|
+
return this.#experiment.close(options)
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
module.exports = { Experiment, ExternalExperiment, normalizeEvaluators, validateEvaluatorName }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const log = require('../../log')
|
|
4
|
-
const { ExperimentsClient
|
|
4
|
+
const { ExperimentsClient } = require('./client')
|
|
5
5
|
const { Dataset, DatasetRecord } = require('./dataset')
|
|
6
|
-
const { Experiment } = require('./experiment')
|
|
6
|
+
const { Experiment, ExternalExperiment } = require('./experiment')
|
|
7
|
+
const { validateTagsList } = require('./util')
|
|
7
8
|
const NoopExperiments = require('./noop')
|
|
8
9
|
|
|
9
10
|
// Poll `attempt` with exponential backoff until it returns true or the time
|
|
@@ -26,17 +27,27 @@ async function retryWithBackoff (attempt, { maxTotalMs = 30_000, baseDelayMs = 2
|
|
|
26
27
|
// experiments against the LLM Obs backend using the tracer's own config.
|
|
27
28
|
class Experiments {
|
|
28
29
|
#client
|
|
30
|
+
#config
|
|
29
31
|
#llmobs
|
|
30
32
|
#projectName
|
|
31
33
|
|
|
32
34
|
constructor (config, llmobs) {
|
|
35
|
+
this.#config = config
|
|
33
36
|
this.#llmobs = llmobs
|
|
34
37
|
this.#projectName = config.llmobs?.mlApp || config.service
|
|
35
|
-
this.#client =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
this.#client = this.#clientForProject(this.#projectName)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} projectName
|
|
43
|
+
* @returns {ExperimentsClient}
|
|
44
|
+
*/
|
|
45
|
+
#clientForProject (projectName) {
|
|
46
|
+
return new ExperimentsClient({
|
|
47
|
+
apiKey: this.#config.DD_API_KEY,
|
|
48
|
+
appKey: this.#config.DD_APP_KEY,
|
|
49
|
+
site: this.#config.site,
|
|
50
|
+
projectName,
|
|
40
51
|
})
|
|
41
52
|
}
|
|
42
53
|
|
|
@@ -56,7 +67,9 @@ class Experiments {
|
|
|
56
67
|
if (recordIds.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
57
68
|
recordIds.add(record.id)
|
|
58
69
|
}
|
|
59
|
-
dataset.addRecord(
|
|
70
|
+
dataset.addRecord(
|
|
71
|
+
new DatasetRecord(record.inputData, record.expectedOutput, record.metadata, record.id, record.tags)
|
|
72
|
+
)
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
return dataset
|
|
@@ -64,73 +77,48 @@ class Experiments {
|
|
|
64
77
|
|
|
65
78
|
// Pull an existing dataset by name (with its records). Polls with exponential
|
|
66
79
|
// backoff to absorb read-after-write lag; pass `expectedRecordCount` to also
|
|
67
|
-
// wait until that many records are readable.
|
|
80
|
+
// wait until that many records are readable. Pass `tags` to filter records by
|
|
81
|
+
// dataset record tags.
|
|
68
82
|
async pullDataset (name, options = {}) {
|
|
69
|
-
const { expectedRecordCount, maxWaitMs = 30_000, version } = options
|
|
83
|
+
const { expectedRecordCount, maxWaitMs = 30_000, tags, version } = options
|
|
84
|
+
const filterTags = validateTagsList(tags)
|
|
70
85
|
const projectId = await this.#client.ensureProjectId()
|
|
71
86
|
|
|
72
|
-
let
|
|
73
|
-
let description = ''
|
|
87
|
+
let pulledDataset = null
|
|
74
88
|
let records = []
|
|
75
|
-
|
|
76
|
-
let datasetVersion = version ?? null
|
|
89
|
+
const datasetVersion = version ?? null
|
|
77
90
|
let latestVersion = null
|
|
78
91
|
let lastError = ''
|
|
79
92
|
|
|
80
93
|
const succeeded = await retryWithBackoff(async () => {
|
|
81
94
|
try {
|
|
82
|
-
if (
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (const item of datasets) {
|
|
90
|
-
if (item?.attributes?.name === name) {
|
|
91
|
-
datasetId = String(item?.id ?? '')
|
|
92
|
-
description = String(item?.attributes?.description ?? '')
|
|
93
|
-
latestVersion = item?.attributes?.current_version ?? null
|
|
94
|
-
datasetVersion = version ?? latestVersion
|
|
95
|
-
break
|
|
96
|
-
}
|
|
95
|
+
if (pulledDataset === null) {
|
|
96
|
+
const datasets = await this.#client.listDatasets(projectId, { name })
|
|
97
|
+
for (const dataset of datasets) {
|
|
98
|
+
if (dataset.name() === name) {
|
|
99
|
+
pulledDataset = dataset
|
|
100
|
+
latestVersion = dataset.latestVersion()
|
|
101
|
+
break
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
|
-
if (
|
|
104
|
+
if (pulledDataset === null) return false
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const recs = []
|
|
103
|
-
const ids = []
|
|
104
108
|
let cursor = ''
|
|
105
109
|
// Follow the meta.after / page[cursor] pagination until the last page.
|
|
106
110
|
for (;;) {
|
|
107
|
-
const query = new URLSearchParams()
|
|
108
|
-
if (cursor) query.set('page[cursor]', cursor)
|
|
109
|
-
if (datasetVersion !== null) query.set('filter[version]', String(datasetVersion))
|
|
110
111
|
// eslint-disable-next-line no-await-in-loop
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const attrs = item?.attributes ?? item
|
|
119
|
-
const recordId = String(item?.id ?? attrs?.id ?? '')
|
|
120
|
-
recs.push(new DatasetRecord(
|
|
121
|
-
attrs?.input ?? null,
|
|
122
|
-
attrs?.expected_output ?? null,
|
|
123
|
-
attrs?.metadata ?? {},
|
|
124
|
-
recordId === '' ? null : recordId
|
|
125
|
-
))
|
|
126
|
-
ids.push(recordId)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
cursor = resp?.meta?.after ?? ''
|
|
112
|
+
const page = await this.#client.listDatasetRecords(projectId, pulledDataset.id(), {
|
|
113
|
+
cursor,
|
|
114
|
+
tags: filterTags,
|
|
115
|
+
version: datasetVersion,
|
|
116
|
+
})
|
|
117
|
+
for (const record of page.records) recs.push(record)
|
|
118
|
+
cursor = page.after
|
|
130
119
|
if (!cursor) break
|
|
131
120
|
}
|
|
132
121
|
records = recs
|
|
133
|
-
recordIds = ids
|
|
134
122
|
lastError = ''
|
|
135
123
|
|
|
136
124
|
return expectedRecordCount == null || recs.length >= expectedRecordCount
|
|
@@ -140,10 +128,10 @@ class Experiments {
|
|
|
140
128
|
}
|
|
141
129
|
}, { maxTotalMs: maxWaitMs })
|
|
142
130
|
|
|
143
|
-
if (
|
|
131
|
+
if (pulledDataset === null && lastError) {
|
|
144
132
|
throw new Error(`Failed to list datasets in project '${this.#projectName}': ${lastError}`)
|
|
145
133
|
}
|
|
146
|
-
if (
|
|
134
|
+
if (pulledDataset === null) {
|
|
147
135
|
throw new Error(`Dataset '${name}' not found in project '${this.#projectName}' (after ${maxWaitMs}ms)`)
|
|
148
136
|
}
|
|
149
137
|
if (!succeeded && lastError) {
|
|
@@ -156,16 +144,22 @@ class Experiments {
|
|
|
156
144
|
)
|
|
157
145
|
}
|
|
158
146
|
|
|
147
|
+
for (const record of records) {
|
|
148
|
+
if (record.id === null || record.id === undefined || record.id === '') {
|
|
149
|
+
throw new Error(`Failed to pull dataset '${name}': backend returned a record without an id`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
159
153
|
return Dataset.fromExisting(
|
|
160
154
|
this.#client,
|
|
161
155
|
name,
|
|
162
|
-
description,
|
|
163
|
-
|
|
156
|
+
pulledDataset.description(),
|
|
157
|
+
pulledDataset.id(),
|
|
164
158
|
projectId,
|
|
165
159
|
records,
|
|
166
|
-
recordIds,
|
|
167
160
|
datasetVersion,
|
|
168
|
-
latestVersion
|
|
161
|
+
latestVersion,
|
|
162
|
+
filterTags
|
|
169
163
|
)
|
|
170
164
|
}
|
|
171
165
|
|
|
@@ -173,6 +167,22 @@ class Experiments {
|
|
|
173
167
|
experiment (options) {
|
|
174
168
|
return new Experiment(this.#client, options, this.#llmobs)
|
|
175
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Start an externally-driven experiment for eval frameworks that already own
|
|
173
|
+
* task execution. Call submitSpan() once per completed row, then
|
|
174
|
+
* submitEvaluationMetrics() with the generated span id.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} options
|
|
177
|
+
* @returns {Promise<ExternalExperiment>}
|
|
178
|
+
*/
|
|
179
|
+
startExperiment (options) {
|
|
180
|
+
const client = options?.projectName === undefined || options.projectName === this.#projectName
|
|
181
|
+
? this.#client
|
|
182
|
+
: this.#clientForProject(options.projectName)
|
|
183
|
+
return new Experiment(client, { ...options, external: true }).start()
|
|
184
|
+
.then(experiment => new ExternalExperiment(experiment))
|
|
185
|
+
}
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
// Factory used by the LLMObs SDK: returns a real Experiments instance when
|
|
@@ -186,12 +196,13 @@ function createExperiments (config, llmobs) {
|
|
|
186
196
|
return new NoopExperiments('DD_API_KEY and DD_APP_KEY are required for experiments')
|
|
187
197
|
}
|
|
188
198
|
if (!config.llmobs?.mlApp && !config.service) {
|
|
189
|
-
|
|
190
|
-
return new NoopExperiments(
|
|
191
|
-
'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
|
|
199
|
+
const reason = 'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
|
|
192
200
|
'tracer.init()) to name the LLM Obs project, or DD_SERVICE (or service in tracer.init()) as a fallback, ' +
|
|
193
201
|
'then retry'
|
|
194
|
-
)
|
|
202
|
+
const experiments = new Experiments(config, llmobs)
|
|
203
|
+
return new NoopExperiments(reason, {
|
|
204
|
+
startExperiment: (options) => experiments.startExperiment(options),
|
|
205
|
+
})
|
|
195
206
|
}
|
|
196
207
|
return new Experiments(config, llmobs)
|
|
197
208
|
}
|