dd-trace 6.9.0 → 6.11.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.md +0 -17
- package/ci/vitest-no-worker-init-setup.mjs +112 -10
- package/index.d.ts +150 -3
- package/initialize.mjs +28 -17
- package/openfeature.js +2 -2
- package/package.json +14 -10
- 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/confluentinc-kafka-javascript.js +111 -6
- package/packages/datadog-instrumentations/src/cucumber.js +26 -11
- 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 +634 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
- package/packages/datadog-instrumentations/src/jest.js +26 -18
- package/packages/datadog-instrumentations/src/mariadb.js +278 -21
- package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
- package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
- package/packages/datadog-instrumentations/src/mysql.js +45 -15
- package/packages/datadog-instrumentations/src/mysql2.js +119 -18
- package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
- package/packages/datadog-instrumentations/src/pg.js +116 -92
- package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
- package/packages/datadog-instrumentations/src/playwright.js +149 -2
- package/packages/datadog-instrumentations/src/router.js +2 -0
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
- package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
- package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
- package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
- package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
- 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 +4 -13
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
- 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 +82 -30
- 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-google-cloud-pubsub/src/producer.js +31 -17
- package/packages/datadog-plugin-jest/src/index.js +11 -4
- package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
- package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
- package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
- package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
- package/packages/datadog-plugin-mocha/src/index.js +606 -40
- package/packages/datadog-plugin-mysql/src/index.js +37 -0
- package/packages/datadog-plugin-nats/src/index.js +1 -1
- package/packages/datadog-plugin-next/src/index.js +1 -19
- package/packages/datadog-plugin-openai/src/tracing.js +41 -30
- package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
- package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
- package/packages/datadog-plugin-pg/src/index.js +1 -0
- package/packages/datadog-plugin-playwright/src/index.js +42 -13
- package/packages/datadog-plugin-vitest/src/index.js +14 -20
- package/packages/datadog-plugin-ws/src/util.js +2 -1
- 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/api_security/normalized-route.js +453 -0
- package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
- package/packages/dd-trace/src/carrier.js +356 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
- 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 +22 -6
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
- 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 +2 -1
- 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-test-screenshot.js +6 -2
- package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -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 +24 -0
- package/packages/dd-trace/src/datastreams/context.js +6 -1
- package/packages/dd-trace/src/datastreams/index.js +2 -0
- package/packages/dd-trace/src/datastreams/manager.js +14 -3
- package/packages/dd-trace/src/datastreams/pathway.js +23 -22
- package/packages/dd-trace/src/datastreams/processor.js +16 -13
- package/packages/dd-trace/src/datastreams/size.js +5 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
- 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/writer.js +11 -6
- package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
- package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
- package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
- package/packages/dd-trace/src/llmobs/index.js +34 -2
- 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/llmobs/span_processor.js +16 -0
- package/packages/dd-trace/src/llmobs/tagger.js +96 -0
- package/packages/dd-trace/src/llmobs/util.js +78 -0
- package/packages/dd-trace/src/noop/proxy.js +3 -4
- package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
- package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
- 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 +126 -120
- package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
- 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 +5 -3
- package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
- package/packages/dd-trace/src/plugins/util/test.js +93 -14
- package/packages/dd-trace/src/plugins/util/web.js +56 -2
- 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/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/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,19 +1,21 @@
|
|
|
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,
|
|
15
16
|
sleep,
|
|
16
17
|
stringify,
|
|
18
|
+
timestampMs,
|
|
17
19
|
validateEvaluatorName,
|
|
18
20
|
} = require('./util')
|
|
19
21
|
|
|
@@ -28,7 +30,7 @@ function toSpan (row, metadata, ids, spanName, userTags) {
|
|
|
28
30
|
meta.metadata = metadata
|
|
29
31
|
}
|
|
30
32
|
if (row.isError) {
|
|
31
|
-
meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: '' }
|
|
33
|
+
meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: row.errorStack ?? '' }
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
return {
|
|
@@ -91,11 +93,34 @@ function createFallbackSpanContext (startNs) {
|
|
|
91
93
|
return { spanId, traceId }
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
function normalizeError (error) {
|
|
97
|
+
if (error == null) {
|
|
98
|
+
return { errorType: null, errorMessage: null, errorStack: '' }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof error === 'string') {
|
|
102
|
+
return { errorType: 'Error', errorMessage: error, errorStack: '' }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
errorType: error.type ?? error.name ?? 'Error',
|
|
107
|
+
errorMessage: error.message ?? String(error),
|
|
108
|
+
errorStack: error.stack ?? '',
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function errorMessage (error) {
|
|
113
|
+
if (error == null) return null
|
|
114
|
+
if (typeof error === 'string') return error
|
|
115
|
+
return error.message ?? String(error)
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
// Builder + run() orchestration: runs rows sequentially, emits one root span
|
|
95
119
|
// per dataset row, and posts spans + metrics to the experiments events API.
|
|
96
120
|
class Experiment {
|
|
97
121
|
#client
|
|
98
122
|
#llmobs
|
|
123
|
+
#external
|
|
99
124
|
#name
|
|
100
125
|
#description
|
|
101
126
|
#dataset
|
|
@@ -104,24 +129,35 @@ class Experiment {
|
|
|
104
129
|
#summaryEvaluators
|
|
105
130
|
#config
|
|
106
131
|
#tags
|
|
132
|
+
#metadata
|
|
133
|
+
#projectId
|
|
107
134
|
#experimentId
|
|
135
|
+
#runId
|
|
136
|
+
#runIteration
|
|
108
137
|
|
|
109
138
|
constructor (client, options = {}, llmobs) {
|
|
110
139
|
if (!options.name) throw new Error('Experiment name is required')
|
|
111
|
-
|
|
112
|
-
if (
|
|
140
|
+
this.#external = options.external === true
|
|
141
|
+
if (!this.#external) {
|
|
142
|
+
if (!options.dataset) throw new Error('Experiment dataset is required')
|
|
143
|
+
if (typeof options.task !== 'function') throw new Error('Experiment task is required')
|
|
144
|
+
}
|
|
113
145
|
|
|
114
146
|
this.#client = client
|
|
115
147
|
this.#llmobs = llmobs
|
|
116
148
|
this.#name = options.name
|
|
117
149
|
this.#description = options.description ?? ''
|
|
118
|
-
this.#dataset = options.dataset
|
|
150
|
+
this.#dataset = options.dataset ?? {}
|
|
119
151
|
this.#task = options.task
|
|
120
152
|
this.#evaluators = normalizeEvaluators(options.evaluators, 'row')
|
|
121
153
|
this.#summaryEvaluators = normalizeEvaluators(options.summaryEvaluators, 'summary')
|
|
122
154
|
this.#config = { ...options.config }
|
|
123
155
|
this.#tags = { ...options.tags }
|
|
156
|
+
this.#metadata = { ...options.metadata }
|
|
157
|
+
this.#projectId = null
|
|
124
158
|
this.#experimentId = null
|
|
159
|
+
this.#runId = null
|
|
160
|
+
this.#runIteration = null
|
|
125
161
|
}
|
|
126
162
|
|
|
127
163
|
name () {
|
|
@@ -137,7 +173,177 @@ class Experiment {
|
|
|
137
173
|
return `${this.#client.appBase}/llm/experiments/${this.#experimentId}`
|
|
138
174
|
}
|
|
139
175
|
|
|
176
|
+
/**
|
|
177
|
+
* @returns {Promise<Experiment>}
|
|
178
|
+
*/
|
|
179
|
+
async start () {
|
|
180
|
+
if (!this.#external) throw new Error('Experiment is not externally driven')
|
|
181
|
+
|
|
182
|
+
this.#projectId = await this.#client.ensureProjectId()
|
|
183
|
+
|
|
184
|
+
const dataset = await this.#ensureExternalDataset()
|
|
185
|
+
const attributes = {
|
|
186
|
+
name: this.#name,
|
|
187
|
+
project_id: this.#projectId,
|
|
188
|
+
dataset_id: dataset.id,
|
|
189
|
+
description: this.#description,
|
|
190
|
+
ensure_unique: true,
|
|
191
|
+
run_count: 1,
|
|
192
|
+
}
|
|
193
|
+
if (dataset.version != null) attributes.dataset_version = dataset.version
|
|
194
|
+
if (hasEntries(this.#config)) attributes.config = this.#config
|
|
195
|
+
|
|
196
|
+
const metadata = { ...this.#metadata }
|
|
197
|
+
if (hasEntries(this.#tags)) metadata.tags = buildTags(this.#tags, {})
|
|
198
|
+
if (hasEntries(metadata)) attributes.metadata = metadata
|
|
199
|
+
|
|
200
|
+
let created
|
|
201
|
+
try {
|
|
202
|
+
created = await this.#client.createExperiment(attributes)
|
|
203
|
+
} catch (err) {
|
|
204
|
+
throw new Error(`Failed to create experiment '${this.#name}': ${err.message}`)
|
|
205
|
+
}
|
|
206
|
+
this.#experimentId = created.experimentId
|
|
207
|
+
this.#runId = id().toString(16).padStart(16, '0')
|
|
208
|
+
this.#runIteration = 0
|
|
209
|
+
return this
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async #ensureExternalDataset () {
|
|
213
|
+
if (this.#dataset.id) {
|
|
214
|
+
return { id: this.#dataset.id, version: this.#dataset.version }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const name = this.#dataset.name ?? `${this.#name} dataset`
|
|
218
|
+
const description = this.#dataset.description ??
|
|
219
|
+
`Placeholder dataset for externally-driven experiment '${this.#name}'`
|
|
220
|
+
let response
|
|
221
|
+
try {
|
|
222
|
+
response = await this.#client.createDataset(this.#projectId, { name, description })
|
|
223
|
+
} catch (err) {
|
|
224
|
+
throw new Error(`Failed to create placeholder dataset '${name}': ${err.message}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const id = response.id()
|
|
228
|
+
if (id === null) {
|
|
229
|
+
throw new Error(`Failed to create placeholder dataset '${name}': backend response is missing dataset id`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.#dataset = {
|
|
233
|
+
...this.#dataset,
|
|
234
|
+
id,
|
|
235
|
+
version: this.#dataset.version ?? response.version(),
|
|
236
|
+
}
|
|
237
|
+
return { id: this.#dataset.id, version: this.#dataset.version }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {object} input
|
|
242
|
+
* @returns {Promise<{experimentId: string, spanId: string, traceId: string, url: string | null}>}
|
|
243
|
+
*/
|
|
244
|
+
async submitSpan (input = {}) {
|
|
245
|
+
if (this.#experimentId === null || this.#projectId === null) {
|
|
246
|
+
throw new Error('Experiment has not been started')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const startMs = timestampMs(input.startedAt)
|
|
250
|
+
const startNs = Math.round(startMs * 1e6)
|
|
251
|
+
const { spanId, traceId } = createFallbackSpanContext(startNs)
|
|
252
|
+
const error = normalizeError(input.error)
|
|
253
|
+
const row = new Row({
|
|
254
|
+
spanId,
|
|
255
|
+
traceId,
|
|
256
|
+
startNs,
|
|
257
|
+
durationNs: durationNs(input, startMs),
|
|
258
|
+
input: input.input,
|
|
259
|
+
output: input.output,
|
|
260
|
+
expectedOutput: input.expectedOutput,
|
|
261
|
+
errorType: error.errorType,
|
|
262
|
+
errorMessage: error.errorMessage,
|
|
263
|
+
errorStack: error.errorStack,
|
|
264
|
+
evaluations: {},
|
|
265
|
+
evaluationErrors: {},
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const span = toSpan(row, input.metadata, {
|
|
269
|
+
experimentId: this.#experimentId,
|
|
270
|
+
projectId: this.#projectId,
|
|
271
|
+
datasetId: this.#dataset.id,
|
|
272
|
+
datasetRecordId: input.datasetRecordId,
|
|
273
|
+
runId: input.runId ?? this.#runId,
|
|
274
|
+
runIteration: input.runIteration ?? this.#runIteration,
|
|
275
|
+
}, input.name ?? this.#name, mergeTags(this.#tags, input.tags))
|
|
276
|
+
|
|
277
|
+
await this.#postEvents(this.#experimentId, [span], [])
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
experimentId: this.#experimentId,
|
|
281
|
+
spanId,
|
|
282
|
+
traceId,
|
|
283
|
+
url: this.url(),
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @param {{experimentId?: string, spanId: string, traceId?: string}} span
|
|
289
|
+
* @param {object[]} metrics
|
|
290
|
+
* @returns {Promise<void>}
|
|
291
|
+
*/
|
|
292
|
+
async submitEvaluationMetrics (span, metrics) {
|
|
293
|
+
if (this.#experimentId === null) {
|
|
294
|
+
throw new Error('Experiment has not been started')
|
|
295
|
+
}
|
|
296
|
+
if (!span?.spanId) {
|
|
297
|
+
throw new Error('Experiment span id is required')
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const experimentId = span.experimentId ?? this.#experimentId
|
|
301
|
+
if (experimentId !== this.#experimentId) {
|
|
302
|
+
throw new Error(`Experiment span belongs to '${experimentId}', not '${this.#experimentId}'`)
|
|
303
|
+
}
|
|
304
|
+
if (!span.traceId) {
|
|
305
|
+
throw new Error('Experiment trace id is required')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const payload = []
|
|
309
|
+
for (const metric of metrics) {
|
|
310
|
+
validateEvaluatorName(metric.label)
|
|
311
|
+
const metricError = errorMessage(metric.error)
|
|
312
|
+
if (metric.value === undefined && metricError == null) {
|
|
313
|
+
log.warn('LLMObs experiments: skipping external metric %s because it has neither value nor error', metric.label)
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
payload.push(toMetric(
|
|
317
|
+
metric.label,
|
|
318
|
+
metric.value,
|
|
319
|
+
metricError,
|
|
320
|
+
span.spanId,
|
|
321
|
+
span.traceId,
|
|
322
|
+
timestampMs(metric.timestamp),
|
|
323
|
+
experimentId,
|
|
324
|
+
mergeTags(this.#tags, metric.tags),
|
|
325
|
+
metric.source ?? 'custom'
|
|
326
|
+
))
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (payload.length === 0) return
|
|
330
|
+
await this.#postEvents(experimentId, [], payload)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @param {object} options
|
|
335
|
+
* @returns {Promise<void>}
|
|
336
|
+
*/
|
|
337
|
+
async close (options = {}) {
|
|
338
|
+
if (this.#experimentId === null) return
|
|
339
|
+
const error = errorMessage(options.error)
|
|
340
|
+
const status = options.status ?? (error === null ? 'completed' : 'failed')
|
|
341
|
+
await this.#updateStatus(this.#experimentId, status, error, false)
|
|
342
|
+
}
|
|
343
|
+
|
|
140
344
|
async run (options = {}) {
|
|
345
|
+
if (this.#external) throw new Error('Externally-driven experiments cannot run local tasks')
|
|
346
|
+
|
|
141
347
|
const {
|
|
142
348
|
maxRetries = 0,
|
|
143
349
|
retryDelay = (attempt) => 100 * (attempt + 1),
|
|
@@ -168,17 +374,16 @@ class Experiment {
|
|
|
168
374
|
}
|
|
169
375
|
const datasetVersion = this.#dataset.version()
|
|
170
376
|
if (datasetVersion !== null) attributes.dataset_version = datasetVersion
|
|
377
|
+
// eslint-disable-next-line no-restricted-syntax -- faster than tracking entries while copying arbitrary config
|
|
171
378
|
if (Object.keys(this.#config).length > 0) attributes.config = this.#config
|
|
172
379
|
|
|
173
380
|
let created
|
|
174
381
|
try {
|
|
175
|
-
created = await this.#client.
|
|
176
|
-
data: { type: 'experiments', attributes },
|
|
177
|
-
})
|
|
382
|
+
created = await this.#client.createExperiment(attributes)
|
|
178
383
|
} catch (err) {
|
|
179
384
|
throw new Error(`Failed to create experiment '${this.#name}': ${err.message}`)
|
|
180
385
|
}
|
|
181
|
-
this.#experimentId = created
|
|
386
|
+
this.#experimentId = created.experimentId
|
|
182
387
|
const experimentId = this.#experimentId
|
|
183
388
|
const runId = id().toString(16).padStart(16, '0')
|
|
184
389
|
const runIteration = 0
|
|
@@ -316,7 +521,7 @@ class Experiment {
|
|
|
316
521
|
dataset_name: this.#dataset.name(),
|
|
317
522
|
experiment_name: this.#name,
|
|
318
523
|
}
|
|
319
|
-
const tags =
|
|
524
|
+
const tags = mergeTags(this.#tags, autoTags)
|
|
320
525
|
|
|
321
526
|
const execute = () => this.#runWithRetries(
|
|
322
527
|
() => this.#task(record.input, this.#config, record.metadata),
|
|
@@ -454,24 +659,79 @@ class Experiment {
|
|
|
454
659
|
async #postEvents (experimentId, spans, metrics) {
|
|
455
660
|
const attributes = { metrics }
|
|
456
661
|
if (spans.length > 0) attributes.spans = spans
|
|
457
|
-
await this.#client.
|
|
458
|
-
data: { type: 'experiments', attributes },
|
|
459
|
-
})
|
|
662
|
+
await this.#client.postExperimentEvents(experimentId, attributes)
|
|
460
663
|
}
|
|
461
664
|
|
|
462
|
-
async #updateStatus (experimentId, status, error) {
|
|
665
|
+
async #updateStatus (experimentId, status, error, suppressErrors = true) {
|
|
463
666
|
if (!experimentId) return
|
|
464
667
|
// The experiment-update model has no status field, so this is a direct PATCH.
|
|
465
668
|
const attributes = { status }
|
|
466
669
|
if (error !== null) attributes.error = error
|
|
467
670
|
try {
|
|
468
|
-
await this.#client.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
} catch {
|
|
671
|
+
await this.#client.updateExperiment(experimentId, attributes)
|
|
672
|
+
} catch (err) {
|
|
673
|
+
if (!suppressErrors) throw err
|
|
472
674
|
// Status update is best-effort; never let it mask the real result/error.
|
|
473
675
|
}
|
|
474
676
|
}
|
|
475
677
|
}
|
|
476
678
|
|
|
477
|
-
|
|
679
|
+
class ExternalExperiment {
|
|
680
|
+
#experiment
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* @param {Pick<Experiment, 'name' | 'experimentId' | 'url' | 'submitSpan' | 'submitEvaluationMetrics' |
|
|
684
|
+
* 'close'>} experiment
|
|
685
|
+
*/
|
|
686
|
+
constructor (experiment) {
|
|
687
|
+
this.#experiment = experiment
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* @returns {string}
|
|
692
|
+
*/
|
|
693
|
+
name () {
|
|
694
|
+
return this.#experiment.name()
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @returns {string | null}
|
|
699
|
+
*/
|
|
700
|
+
experimentId () {
|
|
701
|
+
return this.#experiment.experimentId()
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* @returns {string | null}
|
|
706
|
+
*/
|
|
707
|
+
url () {
|
|
708
|
+
return this.#experiment.url()
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* @param {object} [input]
|
|
713
|
+
* @returns {Promise<object>}
|
|
714
|
+
*/
|
|
715
|
+
submitSpan (input) {
|
|
716
|
+
return this.#experiment.submitSpan(input)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* @param {object} span
|
|
721
|
+
* @param {object[]} metrics
|
|
722
|
+
* @returns {Promise<void>}
|
|
723
|
+
*/
|
|
724
|
+
submitEvaluationMetrics (span, metrics) {
|
|
725
|
+
return this.#experiment.submitEvaluationMetrics(span, metrics)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* @param {object} [options]
|
|
730
|
+
* @returns {Promise<void>}
|
|
731
|
+
*/
|
|
732
|
+
close (options) {
|
|
733
|
+
return this.#experiment.close(options)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
module.exports = { Experiment, ExternalExperiment, normalizeEvaluators, validateEvaluatorName }
|
|
@@ -1,9 +1,9 @@
|
|
|
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
7
|
const NoopExperiments = require('./noop')
|
|
8
8
|
|
|
9
9
|
// Poll `attempt` with exponential backoff until it returns true or the time
|
|
@@ -26,17 +26,27 @@ async function retryWithBackoff (attempt, { maxTotalMs = 30_000, baseDelayMs = 2
|
|
|
26
26
|
// experiments against the LLM Obs backend using the tracer's own config.
|
|
27
27
|
class Experiments {
|
|
28
28
|
#client
|
|
29
|
+
#config
|
|
29
30
|
#llmobs
|
|
30
31
|
#projectName
|
|
31
32
|
|
|
32
33
|
constructor (config, llmobs) {
|
|
34
|
+
this.#config = config
|
|
33
35
|
this.#llmobs = llmobs
|
|
34
36
|
this.#projectName = config.llmobs?.mlApp || config.service
|
|
35
|
-
this.#client =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
this.#client = this.#clientForProject(this.#projectName)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} projectName
|
|
42
|
+
* @returns {ExperimentsClient}
|
|
43
|
+
*/
|
|
44
|
+
#clientForProject (projectName) {
|
|
45
|
+
return new ExperimentsClient({
|
|
46
|
+
apiKey: this.#config.DD_API_KEY,
|
|
47
|
+
appKey: this.#config.DD_APP_KEY,
|
|
48
|
+
site: this.#config.site,
|
|
49
|
+
projectName,
|
|
40
50
|
})
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -69,68 +79,41 @@ class Experiments {
|
|
|
69
79
|
const { expectedRecordCount, maxWaitMs = 30_000, version } = options
|
|
70
80
|
const projectId = await this.#client.ensureProjectId()
|
|
71
81
|
|
|
72
|
-
let
|
|
73
|
-
let description = ''
|
|
82
|
+
let pulledDataset = null
|
|
74
83
|
let records = []
|
|
75
|
-
let recordIds = []
|
|
76
84
|
let datasetVersion = version ?? null
|
|
77
85
|
let latestVersion = null
|
|
78
86
|
let lastError = ''
|
|
79
87
|
|
|
80
88
|
const succeeded = await retryWithBackoff(async () => {
|
|
81
89
|
try {
|
|
82
|
-
if (
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
}
|
|
90
|
+
if (pulledDataset === null) {
|
|
91
|
+
const datasets = await this.#client.listDatasets(projectId, { name })
|
|
92
|
+
for (const dataset of datasets) {
|
|
93
|
+
if (dataset.name() === name) {
|
|
94
|
+
pulledDataset = dataset
|
|
95
|
+
latestVersion = dataset.latestVersion()
|
|
96
|
+
datasetVersion = version ?? latestVersion
|
|
97
|
+
break
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
if (
|
|
100
|
+
if (pulledDataset === null) return false
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
const recs = []
|
|
103
|
-
const ids = []
|
|
104
104
|
let cursor = ''
|
|
105
105
|
// Follow the meta.after / page[cursor] pagination until the last page.
|
|
106
106
|
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
107
|
// eslint-disable-next-line no-await-in-loop
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
for (const item of recordData) {
|
|
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 ?? ''
|
|
108
|
+
const page = await this.#client.listDatasetRecords(projectId, pulledDataset.id(), {
|
|
109
|
+
cursor,
|
|
110
|
+
version: datasetVersion,
|
|
111
|
+
})
|
|
112
|
+
for (const record of page.records) recs.push(record)
|
|
113
|
+
cursor = page.after
|
|
130
114
|
if (!cursor) break
|
|
131
115
|
}
|
|
132
116
|
records = recs
|
|
133
|
-
recordIds = ids
|
|
134
117
|
lastError = ''
|
|
135
118
|
|
|
136
119
|
return expectedRecordCount == null || recs.length >= expectedRecordCount
|
|
@@ -140,10 +123,10 @@ class Experiments {
|
|
|
140
123
|
}
|
|
141
124
|
}, { maxTotalMs: maxWaitMs })
|
|
142
125
|
|
|
143
|
-
if (
|
|
126
|
+
if (pulledDataset === null && lastError) {
|
|
144
127
|
throw new Error(`Failed to list datasets in project '${this.#projectName}': ${lastError}`)
|
|
145
128
|
}
|
|
146
|
-
if (
|
|
129
|
+
if (pulledDataset === null) {
|
|
147
130
|
throw new Error(`Dataset '${name}' not found in project '${this.#projectName}' (after ${maxWaitMs}ms)`)
|
|
148
131
|
}
|
|
149
132
|
if (!succeeded && lastError) {
|
|
@@ -159,11 +142,10 @@ class Experiments {
|
|
|
159
142
|
return Dataset.fromExisting(
|
|
160
143
|
this.#client,
|
|
161
144
|
name,
|
|
162
|
-
description,
|
|
163
|
-
|
|
145
|
+
pulledDataset.description(),
|
|
146
|
+
pulledDataset.id(),
|
|
164
147
|
projectId,
|
|
165
148
|
records,
|
|
166
|
-
recordIds,
|
|
167
149
|
datasetVersion,
|
|
168
150
|
latestVersion
|
|
169
151
|
)
|
|
@@ -173,6 +155,22 @@ class Experiments {
|
|
|
173
155
|
experiment (options) {
|
|
174
156
|
return new Experiment(this.#client, options, this.#llmobs)
|
|
175
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Start an externally-driven experiment for eval frameworks that already own
|
|
161
|
+
* task execution. Call submitSpan() once per completed row, then
|
|
162
|
+
* submitEvaluationMetrics() with the generated span id.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} options
|
|
165
|
+
* @returns {Promise<ExternalExperiment>}
|
|
166
|
+
*/
|
|
167
|
+
startExperiment (options) {
|
|
168
|
+
const client = options?.projectName === undefined || options.projectName === this.#projectName
|
|
169
|
+
? this.#client
|
|
170
|
+
: this.#clientForProject(options.projectName)
|
|
171
|
+
return new Experiment(client, { ...options, external: true }).start()
|
|
172
|
+
.then(experiment => new ExternalExperiment(experiment))
|
|
173
|
+
}
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
// Factory used by the LLMObs SDK: returns a real Experiments instance when
|
|
@@ -186,12 +184,13 @@ function createExperiments (config, llmobs) {
|
|
|
186
184
|
return new NoopExperiments('DD_API_KEY and DD_APP_KEY are required for experiments')
|
|
187
185
|
}
|
|
188
186
|
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 ' +
|
|
187
|
+
const reason = 'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
|
|
192
188
|
'tracer.init()) to name the LLM Obs project, or DD_SERVICE (or service in tracer.init()) as a fallback, ' +
|
|
193
189
|
'then retry'
|
|
194
|
-
)
|
|
190
|
+
const experiments = new Experiments(config, llmobs)
|
|
191
|
+
return new NoopExperiments(reason, {
|
|
192
|
+
startExperiment: (options) => experiments.startExperiment(options),
|
|
193
|
+
})
|
|
195
194
|
}
|
|
196
195
|
return new Experiments(config, llmobs)
|
|
197
196
|
}
|