dd-trace 5.121.0 → 5.122.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/index.d.ts +108 -3
- package/initialize.mjs +28 -17
- package/openfeature.js +2 -2
- package/package.json +11 -8
- 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 +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 +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 +130 -17
- 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.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 +42 -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/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 +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/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/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/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/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/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/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
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// Control-plane HTTP client for LLM Obs Experiments. Uses the global `fetch`,
|
|
4
4
|
// so this module adds no new dependency; credentials and site come from config.
|
|
5
5
|
|
|
6
|
+
const { Dataset, DatasetRecord } = require('./dataset')
|
|
7
|
+
const { ExperimentResult } = require('./result')
|
|
8
|
+
|
|
6
9
|
const API_BASE_PATH = '/api/v2/llm-obs/v1'
|
|
7
10
|
|
|
8
11
|
// Control-plane host for a Datadog site, e.g.
|
|
@@ -22,12 +25,67 @@ function appHost (site) {
|
|
|
22
25
|
return site.split('.').length === 2 ? `app.${site}` : site
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
function datasetRecordFromResource (resource) {
|
|
29
|
+
const attrs = resource?.attributes ?? resource ?? {}
|
|
30
|
+
const id = String(resource?.id ?? attrs.id ?? '')
|
|
31
|
+
if (id === '') throw new Error('Dataset record response is missing an id')
|
|
32
|
+
return new DatasetRecord(
|
|
33
|
+
attrs.input ?? null,
|
|
34
|
+
attrs.expected_output ?? null,
|
|
35
|
+
attrs.metadata ?? {},
|
|
36
|
+
id
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function datasetVersionFromResource (resource) {
|
|
41
|
+
const attrs = resource?.attributes ?? resource ?? {}
|
|
42
|
+
return attrs.valid_from_version ?? attrs.version ?? null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function datasetVersionFromResources (resources) {
|
|
46
|
+
const versions = resources
|
|
47
|
+
.map(datasetVersionFromResource)
|
|
48
|
+
.filter(version => version != null)
|
|
49
|
+
.map(Number)
|
|
50
|
+
.filter(Number.isFinite)
|
|
51
|
+
if (versions.length === 0) return null
|
|
52
|
+
return Math.max(...versions)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function datasetMutationResultFromResources (resources) {
|
|
56
|
+
return {
|
|
57
|
+
records: resources.map(datasetRecordFromResource),
|
|
58
|
+
version: datasetVersionFromResources(resources),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function datasetFromResource (client, projectId, resource) {
|
|
63
|
+
const attrs = resource?.attributes ?? resource ?? {}
|
|
64
|
+
const version = attrs.current_version ?? null
|
|
65
|
+
return Dataset.fromExisting(
|
|
66
|
+
client,
|
|
67
|
+
String(attrs.name ?? ''),
|
|
68
|
+
String(attrs.description ?? ''),
|
|
69
|
+
resource?.id ?? attrs.id ?? null,
|
|
70
|
+
projectId,
|
|
71
|
+
[],
|
|
72
|
+
version,
|
|
73
|
+
version
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function experimentFromResource (client, resource) {
|
|
78
|
+
const id = resource?.id
|
|
79
|
+
return new ExperimentResult(id, [], id == null ? null : `${client.appBase}/llm/experiments/${id}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
25
82
|
class ExperimentsClient {
|
|
26
83
|
#apiKey
|
|
27
84
|
#appKey
|
|
28
85
|
#site
|
|
29
86
|
#projectName
|
|
30
87
|
#timeout
|
|
88
|
+
apiBase
|
|
31
89
|
#cachedProjectId
|
|
32
90
|
|
|
33
91
|
constructor ({ apiKey, appKey, site, projectName, timeout = 30_000 } = {}) {
|
|
@@ -36,6 +94,7 @@ class ExperimentsClient {
|
|
|
36
94
|
this.#site = site
|
|
37
95
|
this.#projectName = projectName
|
|
38
96
|
this.#timeout = timeout
|
|
97
|
+
this.apiBase = `https://${apiHost(this.#site)}`
|
|
39
98
|
this.#cachedProjectId = null
|
|
40
99
|
}
|
|
41
100
|
|
|
@@ -61,7 +120,7 @@ class ExperimentsClient {
|
|
|
61
120
|
// Low-level request. Builds https://api.<site><path>, attaches both keys, and
|
|
62
121
|
// returns the parsed JSON body. Throws with status + body on a non-2xx.
|
|
63
122
|
async request (method, path, body) {
|
|
64
|
-
const url =
|
|
123
|
+
const url = `${this.apiBase}${path}`
|
|
65
124
|
const headers = {
|
|
66
125
|
'DD-API-KEY': this.#apiKey,
|
|
67
126
|
'DD-APPLICATION-KEY': this.#appKey,
|
|
@@ -92,6 +151,107 @@ class ExperimentsClient {
|
|
|
92
151
|
return text ? JSON.parse(text) : {}
|
|
93
152
|
}
|
|
94
153
|
|
|
154
|
+
jsonApiRequest (method, path, type, attributes) {
|
|
155
|
+
return this.request(method, path, {
|
|
156
|
+
data: { type, attributes },
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async createProject (name) {
|
|
161
|
+
const response = await this.jsonApiRequest('POST', `${API_BASE_PATH}/projects`, 'projects', { name })
|
|
162
|
+
return response?.data ?? null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async createDataset (projectId, attributes) {
|
|
166
|
+
const response = await this.jsonApiRequest('POST', `${API_BASE_PATH}/${projectId}/datasets`, 'datasets', attributes)
|
|
167
|
+
return datasetFromResource(this, projectId, response?.data ?? null)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
deleteDataset (projectId, datasetId) {
|
|
171
|
+
return this.jsonApiRequest('POST', `${API_BASE_PATH}/${projectId}/datasets/delete`, 'datasets', {
|
|
172
|
+
type: 'soft',
|
|
173
|
+
dataset_ids: [datasetId],
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async listDatasets (projectId, options = {}) {
|
|
178
|
+
const query = new URLSearchParams()
|
|
179
|
+
if (options.name !== undefined) query.set('filter[name]', options.name)
|
|
180
|
+
const response = await this.request('GET', `${API_BASE_PATH}/${projectId}/datasets?${query.toString()}`)
|
|
181
|
+
const resources = Array.isArray(response?.data) ? response.data : []
|
|
182
|
+
return resources.map(resource => datasetFromResource(this, projectId, resource))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async appendDatasetRecords (projectId, datasetId, records) {
|
|
186
|
+
const response = await this.jsonApiRequest(
|
|
187
|
+
'POST',
|
|
188
|
+
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records`,
|
|
189
|
+
'datasets',
|
|
190
|
+
{ records }
|
|
191
|
+
)
|
|
192
|
+
// The append-records response has used both a top-level `records` array
|
|
193
|
+
// and JSON:API `data` resources. Accept either so generated/custom record
|
|
194
|
+
// ids are preserved for experiment row tagging.
|
|
195
|
+
const resources = Array.isArray(response?.records)
|
|
196
|
+
? response.records
|
|
197
|
+
: (Array.isArray(response?.data) ? response.data : [])
|
|
198
|
+
return resources.map(datasetRecordFromResource)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async batchUpdateDatasetRecords (projectId, datasetId, attributes) {
|
|
202
|
+
const response = await this.request(
|
|
203
|
+
'POST',
|
|
204
|
+
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/batch_update`,
|
|
205
|
+
{
|
|
206
|
+
data: {
|
|
207
|
+
type: 'datasets',
|
|
208
|
+
id: datasetId,
|
|
209
|
+
attributes: {
|
|
210
|
+
insert_records: attributes.insert_records ?? [],
|
|
211
|
+
update_records: attributes.update_records ?? [],
|
|
212
|
+
delete_records: attributes.delete_records ?? [],
|
|
213
|
+
deduplicate: attributes.deduplicate !== false,
|
|
214
|
+
create_new_version: attributes.create_new_version !== false,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
const resources = Array.isArray(response?.records)
|
|
220
|
+
? response.records
|
|
221
|
+
: (Array.isArray(response?.data) ? response.data : [])
|
|
222
|
+
return datasetMutationResultFromResources(resources)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async listDatasetRecords (projectId, datasetId, options = {}) {
|
|
226
|
+
const query = new URLSearchParams()
|
|
227
|
+
if (options.cursor) query.set('page[cursor]', options.cursor)
|
|
228
|
+
if (options.version !== undefined && options.version !== null) query.set('filter[version]', String(options.version))
|
|
229
|
+
const response = await this.request(
|
|
230
|
+
'GET',
|
|
231
|
+
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records?${query.toString()}`
|
|
232
|
+
)
|
|
233
|
+
const records = Array.isArray(response?.data) ? response.data.map(datasetRecordFromResource) : []
|
|
234
|
+
return { records, after: response?.meta?.after ?? '' }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async createExperiment (attributes) {
|
|
238
|
+
const response = await this.jsonApiRequest('POST', `${API_BASE_PATH}/experiments`, 'experiments', attributes)
|
|
239
|
+
return experimentFromResource(this, response?.data ?? null)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
postExperimentEvents (experimentId, attributes) {
|
|
243
|
+
return this.jsonApiRequest(
|
|
244
|
+
'POST',
|
|
245
|
+
`${API_BASE_PATH}/experiments/${experimentId}/events`,
|
|
246
|
+
'experiments',
|
|
247
|
+
attributes
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
updateExperiment (experimentId, attributes) {
|
|
252
|
+
return this.jsonApiRequest('PATCH', `${API_BASE_PATH}/experiments/${experimentId}`, 'experiments', attributes)
|
|
253
|
+
}
|
|
254
|
+
|
|
95
255
|
// Resolve the project id for `name`, creating it if absent. The create
|
|
96
256
|
// endpoint is get-or-create on name, so repeated calls return the same id.
|
|
97
257
|
// Cached after the first resolution.
|
|
@@ -100,14 +260,12 @@ class ExperimentsClient {
|
|
|
100
260
|
|
|
101
261
|
let response
|
|
102
262
|
try {
|
|
103
|
-
response = await this.
|
|
104
|
-
data: { type: 'projects', attributes: { name } },
|
|
105
|
-
})
|
|
263
|
+
response = await this.createProject(name)
|
|
106
264
|
} catch (err) {
|
|
107
265
|
throw new Error(`Failed to create or get project '${name}': ${err.message}`)
|
|
108
266
|
}
|
|
109
267
|
|
|
110
|
-
this.#cachedProjectId = response?.
|
|
268
|
+
this.#cachedProjectId = response?.id ?? null
|
|
111
269
|
return this.#cachedProjectId
|
|
112
270
|
}
|
|
113
271
|
}
|
|
@@ -1,73 +1,98 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { randomUUID } = require('node:crypto')
|
|
4
4
|
|
|
5
|
-
// Dataset record: { input, expectedOutput?, metadata?, id
|
|
6
|
-
//
|
|
5
|
+
// Dataset record: { input, expectedOutput?, metadata?, id }.
|
|
6
|
+
// IDs are generated locally unless the caller supplies one.
|
|
7
7
|
class DatasetRecord {
|
|
8
8
|
constructor (input, expectedOutput = null, metadata = {}, id = null) {
|
|
9
|
+
if (id != null && (typeof id !== 'string' || id.length === 0)) {
|
|
10
|
+
throw new Error('record id must be a non-empty string')
|
|
11
|
+
}
|
|
9
12
|
this.input = input
|
|
10
13
|
this.expectedOutput = expectedOutput ?? null
|
|
11
14
|
this.metadata = metadata ?? {}
|
|
12
|
-
this.id = id ??
|
|
15
|
+
this.id = id ?? randomUUID()
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
if (Array.isArray(response?.data)) return response.data
|
|
19
|
-
return []
|
|
19
|
+
function versionFromMutationResult (result) {
|
|
20
|
+
return result?.version ?? null
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function
|
|
23
|
-
return
|
|
23
|
+
function serializedRecord (record) {
|
|
24
|
+
return {
|
|
25
|
+
id: record.id,
|
|
26
|
+
input: record.input,
|
|
27
|
+
expected_output: record.expectedOutput ?? null,
|
|
28
|
+
metadata: record.metadata ?? {},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function serializedRecordUpdate (update) {
|
|
33
|
+
const output = { id: update.id }
|
|
34
|
+
if (Object.hasOwn(update, 'input') && update.input !== undefined) output.input = update.input
|
|
35
|
+
if (Object.hasOwn(update, 'expectedOutput') && update.expectedOutput !== undefined) {
|
|
36
|
+
output.expected_output = update.expectedOutput
|
|
37
|
+
}
|
|
38
|
+
if (Object.hasOwn(update, 'metadata') && update.metadata !== undefined) output.metadata = update.metadata
|
|
39
|
+
return output
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
.map(record => Number(record?.attributes?.valid_from_version ?? record?.attributes?.version))
|
|
29
|
-
.filter(Number.isFinite)
|
|
30
|
-
if (versions.length === 0) return null
|
|
31
|
-
return Math.max(...versions)
|
|
42
|
+
function valuesAreEqual (left, right) {
|
|
43
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
function updateFromInsertedRecord (recordId, record, payload) {
|
|
47
|
+
const update = { id: recordId }
|
|
48
|
+
if (!valuesAreEqual(record.input, payload.input)) update.input = record.input
|
|
49
|
+
if (!valuesAreEqual(record.expectedOutput, payload.expected_output)) {
|
|
50
|
+
update.expectedOutput = record.expectedOutput
|
|
51
|
+
}
|
|
52
|
+
if (!valuesAreEqual(record.metadata, payload.metadata)) update.metadata = record.metadata
|
|
53
|
+
return update
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A local dataset model that tracks pending changes and pushes them in one batch.
|
|
36
57
|
class Dataset {
|
|
37
58
|
#client
|
|
38
59
|
#name
|
|
39
60
|
#description
|
|
40
61
|
#records
|
|
41
|
-
#
|
|
62
|
+
#recordsById
|
|
63
|
+
#newRecordsById
|
|
64
|
+
#updatedRecordsById
|
|
65
|
+
#deletedRecordIds
|
|
42
66
|
#id
|
|
43
67
|
#projectId
|
|
44
|
-
#pushedCount
|
|
45
68
|
#version
|
|
46
69
|
#latestVersion
|
|
70
|
+
#pushPromise
|
|
47
71
|
|
|
48
72
|
constructor (client, name, description = '') {
|
|
49
73
|
this.#client = client
|
|
50
74
|
this.#name = name
|
|
51
75
|
this.#description = description
|
|
52
76
|
this.#records = []
|
|
53
|
-
this.#
|
|
77
|
+
this.#recordsById = new Map()
|
|
78
|
+
this.#newRecordsById = new Map()
|
|
79
|
+
this.#updatedRecordsById = new Map()
|
|
80
|
+
this.#deletedRecordIds = new Set()
|
|
54
81
|
this.#id = null
|
|
55
82
|
this.#projectId = null
|
|
56
|
-
this.#pushedCount = 0
|
|
57
83
|
this.#version = null
|
|
58
84
|
this.#latestVersion = null
|
|
85
|
+
this.#pushPromise = Promise.resolve()
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
// Build a Dataset that already exists remotely (used by pullDataset).
|
|
62
|
-
static fromExisting (client, name, description, id, projectId, records,
|
|
89
|
+
static fromExisting (client, name, description, id, projectId, records, version, latestVersion) {
|
|
63
90
|
const dataset = new Dataset(client, name, description)
|
|
64
91
|
dataset.#id = id
|
|
65
92
|
dataset.#projectId = projectId
|
|
66
|
-
dataset.#records.push(...records)
|
|
67
|
-
dataset.#recordIds.push(...recordIds)
|
|
68
|
-
dataset.#pushedCount = records.length
|
|
69
93
|
dataset.#version = version ?? null
|
|
70
94
|
dataset.#latestVersion = latestVersion ?? version ?? null
|
|
95
|
+
for (const record of records) dataset.#addExistingRecord(record)
|
|
71
96
|
return dataset
|
|
72
97
|
}
|
|
73
98
|
|
|
@@ -76,7 +101,53 @@ class Dataset {
|
|
|
76
101
|
const record = recordOrInput instanceof DatasetRecord
|
|
77
102
|
? recordOrInput
|
|
78
103
|
: new DatasetRecord(recordOrInput, expectedOutput, metadata)
|
|
79
|
-
this.#
|
|
104
|
+
this.#addRecord(record)
|
|
105
|
+
return this
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update an existing dataset record. New records are updated in place and are sent with their insert.
|
|
110
|
+
* @param {number} index Dataset record index.
|
|
111
|
+
* @param {{input?: unknown, expectedOutput?: unknown, metadata?: object}} fields Fields to update.
|
|
112
|
+
* @returns {Dataset} This dataset for chaining.
|
|
113
|
+
*/
|
|
114
|
+
update (index, fields) {
|
|
115
|
+
if (fields == null || typeof fields !== 'object' || Array.isArray(fields)) {
|
|
116
|
+
throw new TypeError('record update must be an object')
|
|
117
|
+
}
|
|
118
|
+
const record = this.#recordAt(index)
|
|
119
|
+
const fieldNames = ['input', 'expectedOutput', 'metadata']
|
|
120
|
+
const providedFields = fieldNames.filter(field => Object.hasOwn(fields, field) && fields[field] !== undefined)
|
|
121
|
+
if (providedFields.length === 0) {
|
|
122
|
+
throw new Error('record update must include input, expectedOutput, or metadata')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const field of providedFields) {
|
|
126
|
+
record[field] = field === 'metadata' ? (fields[field] ?? {}) : fields[field]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.#newRecordsById.has(record.id)) return this
|
|
130
|
+
|
|
131
|
+
const update = this.#updatedRecordsById.get(record.id) ?? { id: record.id }
|
|
132
|
+
for (const field of providedFields) update[field] = record[field]
|
|
133
|
+
this.#updatedRecordsById.set(record.id, update)
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Delete a dataset record. New records are removed locally without a backend operation.
|
|
139
|
+
* @param {number} index Dataset record index.
|
|
140
|
+
* @returns {Dataset} This dataset for chaining.
|
|
141
|
+
*/
|
|
142
|
+
delete (index) {
|
|
143
|
+
const record = this.#recordAt(index)
|
|
144
|
+
this.#records.splice(index, 1)
|
|
145
|
+
this.#recordsById.delete(record.id)
|
|
146
|
+
|
|
147
|
+
if (this.#newRecordsById.delete(record.id)) return this
|
|
148
|
+
|
|
149
|
+
this.#updatedRecordsById.delete(record.id)
|
|
150
|
+
this.#deletedRecordIds.add(record.id)
|
|
80
151
|
return this
|
|
81
152
|
}
|
|
82
153
|
|
|
@@ -84,12 +155,16 @@ class Dataset {
|
|
|
84
155
|
return this.#name
|
|
85
156
|
}
|
|
86
157
|
|
|
158
|
+
description () {
|
|
159
|
+
return this.#description
|
|
160
|
+
}
|
|
161
|
+
|
|
87
162
|
records () {
|
|
88
163
|
return [...this.#records]
|
|
89
164
|
}
|
|
90
165
|
|
|
91
166
|
recordIds () {
|
|
92
|
-
return
|
|
167
|
+
return this.#records.map(record => record.id)
|
|
93
168
|
}
|
|
94
169
|
|
|
95
170
|
id () {
|
|
@@ -114,92 +189,171 @@ class Dataset {
|
|
|
114
189
|
return `${this.#client.appBase}/llm/datasets/${this.#id}`
|
|
115
190
|
}
|
|
116
191
|
|
|
117
|
-
// Eagerly create the dataset (if needed) and push
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
192
|
+
// Eagerly create the dataset (if needed) and push all pending changes.
|
|
193
|
+
push () {
|
|
194
|
+
return this.#enqueuePush(async () => {
|
|
195
|
+
const projectId = await this.#client.ensureProjectId()
|
|
196
|
+
return this.#ensureCreatedAndPushed(projectId)
|
|
197
|
+
})
|
|
121
198
|
}
|
|
122
199
|
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
200
|
+
// Called by Experiment.run() after it has resolved the project id.
|
|
201
|
+
ensureCreatedAndPushed (projectId) {
|
|
202
|
+
return this.#enqueuePush(() => this.#ensureCreatedAndPushed(projectId))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#enqueuePush (push) {
|
|
206
|
+
const next = this.#pushPromise.then(push, push)
|
|
207
|
+
this.#pushPromise = next.then(() => {}, () => {})
|
|
208
|
+
return next
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async #ensureCreatedAndPushed (projectId) {
|
|
127
212
|
if (this.#id === null) {
|
|
128
213
|
let response
|
|
129
214
|
try {
|
|
130
|
-
response = await this.#client.
|
|
131
|
-
data: { type: 'datasets', attributes: { name: this.#name, description: this.#description } },
|
|
132
|
-
})
|
|
215
|
+
response = await this.#client.createDataset(projectId, { name: this.#name, description: this.#description })
|
|
133
216
|
} catch (err) {
|
|
134
217
|
throw new Error(`Failed to create dataset '${this.#name}': ${err.message}`)
|
|
135
218
|
}
|
|
136
|
-
this.#id = response?.
|
|
219
|
+
this.#id = response?.id() ?? null
|
|
137
220
|
if (this.#id === null) {
|
|
138
221
|
throw new Error(`Failed to create dataset '${this.#name}': backend response is missing dataset id`)
|
|
139
222
|
}
|
|
140
223
|
this.#projectId = projectId
|
|
141
|
-
this.#version = response
|
|
142
|
-
this.#latestVersion = response
|
|
224
|
+
this.#version = response.version() ?? this.#version
|
|
225
|
+
this.#latestVersion = response.latestVersion() ?? this.#latestVersion
|
|
143
226
|
}
|
|
144
227
|
|
|
145
|
-
|
|
228
|
+
const pending = this.#pendingBatch()
|
|
229
|
+
if (pending.totalCount === 0) return { pushedCount: 0, totalCount: 0 }
|
|
230
|
+
|
|
231
|
+
let result
|
|
232
|
+
try {
|
|
233
|
+
result = await this.#client.batchUpdateDatasetRecords(projectId, this.#id, pending.attributes)
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw new Error(`Failed to push changes to dataset '${this.#name}': ${err.message}`)
|
|
236
|
+
}
|
|
146
237
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
238
|
+
this.#updateVersionFromMutationResult(result)
|
|
239
|
+
this.#clearCommittedChanges(pending)
|
|
240
|
+
return { pushedCount: pending.totalCount, totalCount: pending.totalCount }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#pendingBatch () {
|
|
244
|
+
const insertRecords = []
|
|
245
|
+
const insertPayloads = new Map()
|
|
246
|
+
for (const [recordId, record] of this.#newRecordsById) {
|
|
247
|
+
const payload = serializedRecord(record)
|
|
248
|
+
insertRecords.push(payload)
|
|
249
|
+
insertPayloads.set(recordId, payload)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const updateRecords = []
|
|
253
|
+
const updatePayloads = new Map()
|
|
254
|
+
for (const [recordId, update] of this.#updatedRecordsById) {
|
|
255
|
+
const payload = serializedRecordUpdate(update)
|
|
256
|
+
updateRecords.push(payload)
|
|
257
|
+
updatePayloads.set(recordId, payload)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const deleteRecordIds = [...this.#deletedRecordIds]
|
|
261
|
+
return {
|
|
262
|
+
attributes: {
|
|
263
|
+
insert_records: insertRecords,
|
|
264
|
+
update_records: updateRecords,
|
|
265
|
+
delete_records: deleteRecordIds,
|
|
266
|
+
deduplicate: true,
|
|
267
|
+
create_new_version: true,
|
|
268
|
+
},
|
|
269
|
+
deleteRecordIds,
|
|
270
|
+
insertPayloads,
|
|
271
|
+
updatePayloads,
|
|
272
|
+
totalCount: new Set([
|
|
273
|
+
...insertPayloads.keys(),
|
|
274
|
+
...updatePayloads.keys(),
|
|
275
|
+
...deleteRecordIds,
|
|
276
|
+
]).size,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#clearCommittedChanges (pending) {
|
|
281
|
+
for (const [recordId, payload] of pending.insertPayloads) {
|
|
282
|
+
const current = this.#newRecordsById.get(recordId)
|
|
283
|
+
if (!current) {
|
|
284
|
+
this.#deletedRecordIds.add(recordId)
|
|
285
|
+
continue
|
|
152
286
|
}
|
|
153
|
-
if (
|
|
154
|
-
|
|
287
|
+
if (valuesAreEqual(serializedRecord(current), payload)) {
|
|
288
|
+
this.#newRecordsById.delete(recordId)
|
|
289
|
+
continue
|
|
155
290
|
}
|
|
156
|
-
|
|
157
|
-
|
|
291
|
+
|
|
292
|
+
this.#newRecordsById.delete(recordId)
|
|
293
|
+
const update = this.#updatedRecordsById.get(recordId) ??
|
|
294
|
+
updateFromInsertedRecord(recordId, current, payload)
|
|
295
|
+
this.#updatedRecordsById.set(recordId, update)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const [recordId, payload] of pending.updatePayloads) {
|
|
299
|
+
const current = this.#updatedRecordsById.get(recordId)
|
|
300
|
+
if (current && valuesAreEqual(serializedRecordUpdate(current), payload)) {
|
|
301
|
+
this.#updatedRecordsById.delete(recordId)
|
|
158
302
|
}
|
|
159
|
-
|
|
160
|
-
})
|
|
303
|
+
}
|
|
161
304
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
response = await this.#client.request(
|
|
165
|
-
'POST',
|
|
166
|
-
`${API_BASE_PATH}/${projectId}/datasets/${this.#id}/records`,
|
|
167
|
-
{ data: { type: 'datasets', attributes: { records } } }
|
|
168
|
-
)
|
|
169
|
-
} catch (err) {
|
|
170
|
-
throw new Error(`Failed to push records to dataset '${this.#name}': ${err.message}`)
|
|
305
|
+
for (const recordId of pending.deleteRecordIds) {
|
|
306
|
+
if (!this.#recordsById.has(recordId)) this.#deletedRecordIds.delete(recordId)
|
|
171
307
|
}
|
|
308
|
+
}
|
|
172
309
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const created = createdRecordsFromResponse(response)
|
|
177
|
-
const pushedVersion = versionFromCreatedRecords(created)
|
|
178
|
-
if (pushedVersion === null) {
|
|
179
|
-
// The dataset contents changed, but the backend did not report the new
|
|
180
|
-
// version. Avoid pinning later experiments to the pre-append create version.
|
|
181
|
-
this.#version = null
|
|
182
|
-
} else {
|
|
310
|
+
#updateVersionFromMutationResult (result) {
|
|
311
|
+
const pushedVersion = versionFromMutationResult(result)
|
|
312
|
+
if (pushedVersion !== null) {
|
|
183
313
|
this.#version = pushedVersion
|
|
184
|
-
this.#latestVersion =
|
|
314
|
+
this.#latestVersion = pushedVersion
|
|
315
|
+
return
|
|
185
316
|
}
|
|
186
317
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
this.#recordIds.push(recordId)
|
|
318
|
+
const latestVersion = Number(this.#latestVersion)
|
|
319
|
+
if (Number.isFinite(latestVersion)) {
|
|
320
|
+
this.#version = latestVersion + 1
|
|
321
|
+
this.#latestVersion = this.#version
|
|
322
|
+
} else {
|
|
323
|
+
this.#version = null
|
|
195
324
|
}
|
|
196
|
-
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#addRecord (record) {
|
|
328
|
+
if (this.#recordsById.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
329
|
+
this.#records.push(record)
|
|
330
|
+
this.#recordsById.set(record.id, record)
|
|
331
|
+
this.#deletedRecordIds.delete(record.id)
|
|
332
|
+
this.#newRecordsById.set(record.id, record)
|
|
333
|
+
}
|
|
197
334
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
335
|
+
#addExistingRecord (record) {
|
|
336
|
+
if (record.id === null || record.id === undefined || record.id === '') {
|
|
337
|
+
throw new Error('Dataset records pulled from the backend must have an id')
|
|
338
|
+
}
|
|
339
|
+
if (!(record instanceof DatasetRecord)) {
|
|
340
|
+
record = new DatasetRecord(
|
|
341
|
+
record.input,
|
|
342
|
+
record.expectedOutput,
|
|
343
|
+
record.metadata,
|
|
344
|
+
record.id
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
if (this.#recordsById.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
348
|
+
this.#records.push(record)
|
|
349
|
+
this.#recordsById.set(record.id, record)
|
|
350
|
+
}
|
|
201
351
|
|
|
202
|
-
|
|
352
|
+
#recordAt (index) {
|
|
353
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.#records.length) {
|
|
354
|
+
throw new RangeError(`Dataset record index ${index} is out of range`)
|
|
355
|
+
}
|
|
356
|
+
return this.#records[index]
|
|
203
357
|
}
|
|
204
358
|
}
|
|
205
359
|
|