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,95 +1,331 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { randomUUID } = require('node:crypto')
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** @typedef {{add?: string[], remove?: string[], replace?: string[]}} TagOperations */
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} PendingBatch
|
|
8
|
+
* @property {object} attributes
|
|
9
|
+
* @property {string[]} deleteRecordIds
|
|
10
|
+
* @property {Map<string, object>} insertPayloads
|
|
11
|
+
* @property {Map<string, object>} updatePayloads
|
|
12
|
+
* @property {number} totalCount
|
|
13
|
+
* @property {Map<string, TagOperations>} [inFlightTagOperations]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { tagOperationsAreEmpty, validateTagsList } = require('./util')
|
|
17
|
+
|
|
18
|
+
// Dataset record: { input, expectedOutput?, metadata?, id, tags? }.
|
|
19
|
+
// IDs are generated locally unless the caller supplies one.
|
|
7
20
|
class DatasetRecord {
|
|
8
|
-
constructor (input, expectedOutput = null, metadata = {}, id = null) {
|
|
21
|
+
constructor (input, expectedOutput = null, metadata = {}, id = null, tags = []) {
|
|
22
|
+
if (id != null && (typeof id !== 'string' || id.length === 0)) {
|
|
23
|
+
throw new Error('record id must be a non-empty string')
|
|
24
|
+
}
|
|
9
25
|
this.input = input
|
|
10
26
|
this.expectedOutput = expectedOutput ?? null
|
|
11
27
|
this.metadata = metadata ?? {}
|
|
12
|
-
this.
|
|
28
|
+
this.tags = validateTagsList(tags)
|
|
29
|
+
this.id = id ?? randomUUID()
|
|
13
30
|
}
|
|
14
31
|
}
|
|
15
32
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
function versionFromMutationResult (result) {
|
|
34
|
+
return result?.version ?? null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function serializedRecord (record) {
|
|
38
|
+
const output = {
|
|
39
|
+
id: record.id,
|
|
40
|
+
input: record.input,
|
|
41
|
+
expected_output: record.expectedOutput ?? null,
|
|
42
|
+
metadata: record.metadata ?? {},
|
|
43
|
+
}
|
|
44
|
+
if (record.tags.length > 0) output.tags = record.tags
|
|
45
|
+
return output
|
|
20
46
|
}
|
|
21
47
|
|
|
22
|
-
function
|
|
23
|
-
|
|
48
|
+
function serializedRecordUpdate (update) {
|
|
49
|
+
const output = { id: update.id }
|
|
50
|
+
if (Object.hasOwn(update, 'input') && update.input !== undefined) output.input = update.input
|
|
51
|
+
if (Object.hasOwn(update, 'expectedOutput') && update.expectedOutput !== undefined) {
|
|
52
|
+
output.expected_output = update.expectedOutput
|
|
53
|
+
}
|
|
54
|
+
if (Object.hasOwn(update, 'metadata') && update.metadata !== undefined) output.metadata = update.metadata
|
|
55
|
+
if (Object.hasOwn(update, 'tagOperations')) {
|
|
56
|
+
output.tag_operations = serializedTagOperations(update.tagOperations)
|
|
57
|
+
}
|
|
58
|
+
return output
|
|
24
59
|
}
|
|
25
60
|
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
61
|
+
function serializedTagOperations (operations) {
|
|
62
|
+
const output = {}
|
|
63
|
+
if (Object.hasOwn(operations, 'add')) output.add = operations.add
|
|
64
|
+
if (Object.hasOwn(operations, 'remove')) output.remove = operations.remove
|
|
65
|
+
if (Object.hasOwn(operations, 'replace')) output.set = operations.replace
|
|
66
|
+
return output
|
|
32
67
|
}
|
|
33
68
|
|
|
34
|
-
|
|
35
|
-
|
|
69
|
+
/**
|
|
70
|
+
* @param {TagOperations} operations
|
|
71
|
+
* @returns {TagOperations}
|
|
72
|
+
*/
|
|
73
|
+
function copyTagOperations (operations) {
|
|
74
|
+
return Object.fromEntries(Object.entries(operations).map(([key, tags]) => [key, [...tags]]))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mergeTagOperations (operations, operation, tags) {
|
|
78
|
+
if (operation === 'replace') return { replace: [...tags] }
|
|
79
|
+
if (Object.hasOwn(operations, 'replace')) {
|
|
80
|
+
const replaced = new Set(operations.replace)
|
|
81
|
+
for (const tag of tags) {
|
|
82
|
+
if (operation === 'add') replaced.add(tag)
|
|
83
|
+
else replaced.delete(tag)
|
|
84
|
+
}
|
|
85
|
+
return { replace: [...replaced].sort() }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const add = new Set(operations.add)
|
|
89
|
+
const remove = new Set(operations.remove)
|
|
90
|
+
for (const tag of tags) {
|
|
91
|
+
if (operation === 'add') {
|
|
92
|
+
if (remove.has(tag)) remove.delete(tag)
|
|
93
|
+
else add.add(tag)
|
|
94
|
+
} else if (add.has(tag)) {
|
|
95
|
+
add.delete(tag)
|
|
96
|
+
} else {
|
|
97
|
+
remove.add(tag)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const merged = {}
|
|
101
|
+
if (add.size > 0) merged.add = [...add].sort()
|
|
102
|
+
if (remove.size > 0) merged.remove = [...remove].sort()
|
|
103
|
+
return merged
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function valuesAreEqual (left, right) {
|
|
107
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function updateFromInsertedRecord (recordId, record, payload) {
|
|
111
|
+
const update = { id: recordId }
|
|
112
|
+
if (!valuesAreEqual(record.input, payload.input)) update.input = record.input
|
|
113
|
+
if (!valuesAreEqual(record.expectedOutput, payload.expected_output)) {
|
|
114
|
+
update.expectedOutput = record.expectedOutput
|
|
115
|
+
}
|
|
116
|
+
if (!valuesAreEqual(record.metadata, payload.metadata)) update.metadata = record.metadata
|
|
117
|
+
return update
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A local dataset model that tracks pending changes and pushes them in one batch.
|
|
36
121
|
class Dataset {
|
|
37
122
|
#client
|
|
38
123
|
#name
|
|
39
124
|
#description
|
|
40
125
|
#records
|
|
41
|
-
#
|
|
126
|
+
#recordsById
|
|
127
|
+
#newRecordsById
|
|
128
|
+
#updatedRecordsById
|
|
129
|
+
#deletedRecordIds
|
|
130
|
+
#pendingTagOperations
|
|
42
131
|
#id
|
|
43
132
|
#projectId
|
|
44
|
-
#pushedCount
|
|
45
133
|
#version
|
|
46
134
|
#latestVersion
|
|
135
|
+
#pushPromise
|
|
136
|
+
#filterTags
|
|
47
137
|
|
|
48
|
-
constructor (client, name, description = '') {
|
|
138
|
+
constructor (client, name, description = '', filterTags = []) {
|
|
49
139
|
this.#client = client
|
|
50
140
|
this.#name = name
|
|
51
141
|
this.#description = description
|
|
142
|
+
this.#filterTags = validateTagsList(filterTags)
|
|
52
143
|
this.#records = []
|
|
53
|
-
this.#
|
|
144
|
+
this.#recordsById = new Map()
|
|
145
|
+
this.#newRecordsById = new Map()
|
|
146
|
+
this.#updatedRecordsById = new Map()
|
|
147
|
+
this.#deletedRecordIds = new Set()
|
|
148
|
+
this.#pendingTagOperations = new Map()
|
|
54
149
|
this.#id = null
|
|
55
150
|
this.#projectId = null
|
|
56
|
-
this.#pushedCount = 0
|
|
57
151
|
this.#version = null
|
|
58
152
|
this.#latestVersion = null
|
|
153
|
+
this.#pushPromise = Promise.resolve()
|
|
59
154
|
}
|
|
60
155
|
|
|
61
156
|
// Build a Dataset that already exists remotely (used by pullDataset).
|
|
62
|
-
static fromExisting (client, name, description, id, projectId, records,
|
|
63
|
-
const dataset = new Dataset(client, name, description)
|
|
157
|
+
static fromExisting (client, name, description, id, projectId, records, version, latestVersion, filterTags) {
|
|
158
|
+
const dataset = new Dataset(client, name, description, filterTags)
|
|
64
159
|
dataset.#id = id
|
|
65
160
|
dataset.#projectId = projectId
|
|
66
|
-
dataset.#records.push(...records)
|
|
67
|
-
dataset.#recordIds.push(...recordIds)
|
|
68
|
-
dataset.#pushedCount = records.length
|
|
69
161
|
dataset.#version = version ?? null
|
|
70
162
|
dataset.#latestVersion = latestVersion ?? version ?? null
|
|
163
|
+
for (const record of records) dataset.#addExistingRecord(record)
|
|
71
164
|
return dataset
|
|
72
165
|
}
|
|
73
166
|
|
|
74
167
|
// Append a record. Accepts a DatasetRecord or (input, expectedOutput?, metadata?).
|
|
75
|
-
addRecord (recordOrInput, expectedOutput, metadata) {
|
|
168
|
+
addRecord (recordOrInput, expectedOutput, metadata, tags) {
|
|
76
169
|
const record = recordOrInput instanceof DatasetRecord
|
|
77
170
|
? recordOrInput
|
|
78
|
-
: new DatasetRecord(recordOrInput, expectedOutput, metadata)
|
|
79
|
-
this.#
|
|
171
|
+
: new DatasetRecord(recordOrInput, expectedOutput, metadata, null, tags)
|
|
172
|
+
this.#addRecord(record)
|
|
80
173
|
return this
|
|
81
174
|
}
|
|
82
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Add tags to a dataset record.
|
|
178
|
+
* @param {number} index Dataset record index.
|
|
179
|
+
* @param {string[]} tags Tags in key:value format.
|
|
180
|
+
* @returns {Dataset} This dataset for chaining.
|
|
181
|
+
*/
|
|
182
|
+
addTags (index, tags) {
|
|
183
|
+
const validated = validateTagsList(tags)
|
|
184
|
+
const record = this.#recordAt(index)
|
|
185
|
+
const next = new Set(record.tags)
|
|
186
|
+
const added = []
|
|
187
|
+
for (const tag of validated) {
|
|
188
|
+
if (next.has(tag)) continue
|
|
189
|
+
next.add(tag)
|
|
190
|
+
added.push(tag)
|
|
191
|
+
}
|
|
192
|
+
record.tags = [...next].sort()
|
|
193
|
+
this.#queueTagOperation(record.id, 'add', added)
|
|
194
|
+
return this
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Remove tags from a dataset record.
|
|
199
|
+
* @param {number} index Dataset record index.
|
|
200
|
+
* @param {string[]} tags Tags in key:value format.
|
|
201
|
+
* @returns {Dataset} This dataset for chaining.
|
|
202
|
+
*/
|
|
203
|
+
removeTags (index, tags) {
|
|
204
|
+
const validated = validateTagsList(tags)
|
|
205
|
+
const record = this.#recordAt(index)
|
|
206
|
+
const next = new Set(record.tags)
|
|
207
|
+
const removed = []
|
|
208
|
+
for (const tag of validated) {
|
|
209
|
+
if (!next.has(tag)) continue
|
|
210
|
+
next.delete(tag)
|
|
211
|
+
removed.push(tag)
|
|
212
|
+
}
|
|
213
|
+
record.tags = [...next].sort()
|
|
214
|
+
this.#queueTagOperation(record.id, 'remove', removed)
|
|
215
|
+
return this
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Replace all tags on a dataset record.
|
|
220
|
+
* @param {number} index Dataset record index.
|
|
221
|
+
* @param {string[]} tags Tags in key:value format.
|
|
222
|
+
* @returns {Dataset} This dataset for chaining.
|
|
223
|
+
*/
|
|
224
|
+
replaceTags (index, tags) {
|
|
225
|
+
const validated = validateTagsList(tags)
|
|
226
|
+
const record = this.#recordAt(index)
|
|
227
|
+
record.tags = validated
|
|
228
|
+
this.#queueTagOperation(record.id, 'replace', validated)
|
|
229
|
+
return this
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Update an existing dataset record. New records are updated in place and are sent with their insert.
|
|
234
|
+
* @param {number} index Dataset record index.
|
|
235
|
+
* @param {{input?: unknown, expectedOutput?: unknown, metadata?: object}} fields Fields to update.
|
|
236
|
+
* @returns {Dataset} This dataset for chaining.
|
|
237
|
+
*/
|
|
238
|
+
update (index, fields) {
|
|
239
|
+
if (fields == null || typeof fields !== 'object' || Array.isArray(fields)) {
|
|
240
|
+
throw new TypeError('record update must be an object')
|
|
241
|
+
}
|
|
242
|
+
const record = this.#recordAt(index)
|
|
243
|
+
const fieldNames = ['input', 'expectedOutput', 'metadata']
|
|
244
|
+
const providedFields = fieldNames.filter(field => Object.hasOwn(fields, field) && fields[field] !== undefined)
|
|
245
|
+
if (providedFields.length === 0) {
|
|
246
|
+
throw new Error('record update must include input, expectedOutput, or metadata')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const field of providedFields) {
|
|
250
|
+
record[field] = field === 'metadata' ? (fields[field] ?? {}) : fields[field]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (this.#newRecordsById.has(record.id)) return this
|
|
254
|
+
|
|
255
|
+
const update = this.#updatedRecordsById.get(record.id) ?? { id: record.id }
|
|
256
|
+
for (const field of providedFields) update[field] = record[field]
|
|
257
|
+
if (this.#pendingTagOperations.has(record.id)) {
|
|
258
|
+
update.tagOperations = this.#pendingTagOperations.get(record.id)
|
|
259
|
+
}
|
|
260
|
+
this.#updatedRecordsById.set(record.id, update)
|
|
261
|
+
return this
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Delete a dataset record. New records are removed locally without a backend operation.
|
|
266
|
+
* @param {number} index Dataset record index.
|
|
267
|
+
* @returns {Dataset} This dataset for chaining.
|
|
268
|
+
*/
|
|
269
|
+
delete (index) {
|
|
270
|
+
const record = this.#recordAt(index)
|
|
271
|
+
this.#records.splice(index, 1)
|
|
272
|
+
this.#recordsById.delete(record.id)
|
|
273
|
+
|
|
274
|
+
if (this.#newRecordsById.delete(record.id)) {
|
|
275
|
+
this.#pendingTagOperations.delete(record.id)
|
|
276
|
+
return this
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.#updatedRecordsById.delete(record.id)
|
|
280
|
+
this.#pendingTagOperations.delete(record.id)
|
|
281
|
+
this.#deletedRecordIds.add(record.id)
|
|
282
|
+
return this
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Queue a tag operation for the next dataset push.
|
|
287
|
+
* @param {string} recordId Dataset record id.
|
|
288
|
+
* @param {'add' | 'remove' | 'replace'} operation Tag operation to queue.
|
|
289
|
+
* @param {string[]} tags Tags in key:value format.
|
|
290
|
+
* @returns {void}
|
|
291
|
+
*/
|
|
292
|
+
#queueTagOperation (recordId, operation, tags) {
|
|
293
|
+
if (operation !== 'replace' && tags.length === 0) return
|
|
294
|
+
const operations = mergeTagOperations(this.#pendingTagOperations.get(recordId) ?? {}, operation, tags)
|
|
295
|
+
if (tagOperationsAreEmpty(operations)) {
|
|
296
|
+
this.#pendingTagOperations.delete(recordId)
|
|
297
|
+
const update = this.#updatedRecordsById.get(recordId)
|
|
298
|
+
if (update) {
|
|
299
|
+
const hasRecordUpdate = Object.hasOwn(update, 'input') ||
|
|
300
|
+
Object.hasOwn(update, 'expectedOutput') ||
|
|
301
|
+
Object.hasOwn(update, 'metadata')
|
|
302
|
+
delete update.tagOperations
|
|
303
|
+
if (!hasRecordUpdate) this.#updatedRecordsById.delete(recordId)
|
|
304
|
+
}
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
this.#pendingTagOperations.set(recordId, operations)
|
|
308
|
+
if (!this.#newRecordsById.has(recordId)) {
|
|
309
|
+
const update = this.#updatedRecordsById.get(recordId) ?? { id: recordId }
|
|
310
|
+
update.tagOperations = operations
|
|
311
|
+
this.#updatedRecordsById.set(recordId, update)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
83
315
|
name () {
|
|
84
316
|
return this.#name
|
|
85
317
|
}
|
|
86
318
|
|
|
319
|
+
description () {
|
|
320
|
+
return this.#description
|
|
321
|
+
}
|
|
322
|
+
|
|
87
323
|
records () {
|
|
88
324
|
return [...this.#records]
|
|
89
325
|
}
|
|
90
326
|
|
|
91
327
|
recordIds () {
|
|
92
|
-
return
|
|
328
|
+
return this.#records.map(record => record.id)
|
|
93
329
|
}
|
|
94
330
|
|
|
95
331
|
id () {
|
|
@@ -108,98 +344,254 @@ class Dataset {
|
|
|
108
344
|
return this.#latestVersion
|
|
109
345
|
}
|
|
110
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Return the tags used to filter this dataset.
|
|
349
|
+
* @returns {string[]} Dataset record filter tags.
|
|
350
|
+
*/
|
|
351
|
+
filterTags () {
|
|
352
|
+
return [...this.#filterTags]
|
|
353
|
+
}
|
|
354
|
+
|
|
111
355
|
// Dashboard URL for this dataset, or null until pushed/pulled.
|
|
112
356
|
url () {
|
|
113
357
|
if (this.#id === null) return null
|
|
114
358
|
return `${this.#client.appBase}/llm/datasets/${this.#id}`
|
|
115
359
|
}
|
|
116
360
|
|
|
117
|
-
// Eagerly create the dataset (if needed) and push
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
361
|
+
// Eagerly create the dataset (if needed) and push all pending changes.
|
|
362
|
+
push () {
|
|
363
|
+
return this.#enqueuePush(async () => {
|
|
364
|
+
const projectId = await this.#client.ensureProjectId()
|
|
365
|
+
return this.#ensureCreatedAndPushed(projectId)
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Called by Experiment.run() after it has resolved the project id.
|
|
370
|
+
ensureCreatedAndPushed (projectId) {
|
|
371
|
+
return this.#enqueuePush(() => this.#ensureCreatedAndPushed(projectId))
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#enqueuePush (push) {
|
|
375
|
+
const next = this.#pushPromise.then(push, push)
|
|
376
|
+
this.#pushPromise = next.then(() => {}, () => {})
|
|
377
|
+
return next
|
|
121
378
|
}
|
|
122
379
|
|
|
123
|
-
|
|
124
|
-
// push. Idempotent and incremental. Resolves to { pushedCount, totalCount } for
|
|
125
|
-
// the records attempted in this call, so callers can confirm the push landed.
|
|
126
|
-
async ensureCreatedAndPushed (projectId) {
|
|
380
|
+
async #ensureCreatedAndPushed (projectId) {
|
|
127
381
|
if (this.#id === null) {
|
|
128
382
|
let response
|
|
129
383
|
try {
|
|
130
|
-
response = await this.#client.
|
|
131
|
-
data: { type: 'datasets', attributes: { name: this.#name, description: this.#description } },
|
|
132
|
-
})
|
|
384
|
+
response = await this.#client.createDataset(projectId, { name: this.#name, description: this.#description })
|
|
133
385
|
} catch (err) {
|
|
134
386
|
throw new Error(`Failed to create dataset '${this.#name}': ${err.message}`)
|
|
135
387
|
}
|
|
136
|
-
this.#id = response?.
|
|
388
|
+
this.#id = response?.id() ?? null
|
|
137
389
|
if (this.#id === null) {
|
|
138
390
|
throw new Error(`Failed to create dataset '${this.#name}': backend response is missing dataset id`)
|
|
139
391
|
}
|
|
140
392
|
this.#projectId = projectId
|
|
141
|
-
this.#version = response
|
|
142
|
-
this.#latestVersion = response
|
|
393
|
+
this.#version = response.version() ?? this.#version
|
|
394
|
+
this.#latestVersion = response.latestVersion() ?? this.#latestVersion
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const pending = this.#pendingBatch()
|
|
398
|
+
if (pending.totalCount === 0) return { pushedCount: 0, totalCount: 0 }
|
|
399
|
+
|
|
400
|
+
this.#detachCommittedTagOperations(pending)
|
|
401
|
+
|
|
402
|
+
let result
|
|
403
|
+
try {
|
|
404
|
+
result = await this.#client.batchUpdateDatasetRecords(projectId, this.#id, pending.attributes)
|
|
405
|
+
} catch (err) {
|
|
406
|
+
this.#restoreFailedTagOperations(pending)
|
|
407
|
+
throw new Error(`Failed to push changes to dataset '${this.#name}': ${err.message}`)
|
|
143
408
|
}
|
|
144
409
|
|
|
145
|
-
|
|
410
|
+
this.#updateVersionFromMutationResult(result)
|
|
411
|
+
this.#clearCommittedChanges(pending)
|
|
412
|
+
return { pushedCount: pending.totalCount, totalCount: pending.totalCount }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#pendingBatch () {
|
|
416
|
+
const insertRecords = []
|
|
417
|
+
const insertPayloads = new Map()
|
|
418
|
+
for (const [recordId, record] of this.#newRecordsById) {
|
|
419
|
+
const payload = serializedRecord(record)
|
|
420
|
+
insertRecords.push(payload)
|
|
421
|
+
insertPayloads.set(recordId, payload)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const updateRecords = []
|
|
425
|
+
const updatePayloads = new Map()
|
|
426
|
+
for (const [recordId, update] of this.#updatedRecordsById) {
|
|
427
|
+
const tagOperations = this.#pendingTagOperations.get(recordId)
|
|
428
|
+
if (tagOperations) update.tagOperations = tagOperations
|
|
429
|
+
else delete update.tagOperations
|
|
430
|
+
const payload = serializedRecordUpdate(update)
|
|
431
|
+
updateRecords.push(payload)
|
|
432
|
+
updatePayloads.set(recordId, payload)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const deleteRecordIds = [...this.#deletedRecordIds]
|
|
436
|
+
return {
|
|
437
|
+
attributes: {
|
|
438
|
+
insert_records: insertRecords,
|
|
439
|
+
update_records: updateRecords,
|
|
440
|
+
delete_records: deleteRecordIds,
|
|
441
|
+
deduplicate: true,
|
|
442
|
+
create_new_version: true,
|
|
443
|
+
},
|
|
444
|
+
deleteRecordIds,
|
|
445
|
+
insertPayloads,
|
|
446
|
+
updatePayloads,
|
|
447
|
+
totalCount: new Set([
|
|
448
|
+
...insertPayloads.keys(),
|
|
449
|
+
...updatePayloads.keys(),
|
|
450
|
+
...deleteRecordIds,
|
|
451
|
+
]).size,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Detach tag changes sent by this batch so edits made while the request is in flight
|
|
457
|
+
* are queued relative to the response that this batch will commit.
|
|
458
|
+
* @param {PendingBatch} pending
|
|
459
|
+
* @returns {void}
|
|
460
|
+
*/
|
|
461
|
+
#detachCommittedTagOperations (pending) {
|
|
462
|
+
pending.inFlightTagOperations = new Map()
|
|
463
|
+
for (const [recordId] of pending.insertPayloads) {
|
|
464
|
+
const operations = this.#pendingTagOperations.get(recordId)
|
|
465
|
+
if (!operations) continue
|
|
466
|
+
pending.inFlightTagOperations.set(recordId, copyTagOperations(operations))
|
|
467
|
+
this.#pendingTagOperations.delete(recordId)
|
|
468
|
+
}
|
|
469
|
+
for (const [recordId, payload] of pending.updatePayloads) {
|
|
470
|
+
const operations = this.#pendingTagOperations.get(recordId)
|
|
471
|
+
if (!operations || !Object.hasOwn(payload, 'tag_operations')) continue
|
|
472
|
+
|
|
473
|
+
pending.inFlightTagOperations.set(recordId, copyTagOperations(operations))
|
|
474
|
+
this.#pendingTagOperations.delete(recordId)
|
|
475
|
+
const update = this.#updatedRecordsById.get(recordId)
|
|
476
|
+
if (update) delete update.tagOperations
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Restore tag changes when a batch request fails, including edits made while it was in flight.
|
|
482
|
+
* @param {PendingBatch} pending
|
|
483
|
+
* @returns {void}
|
|
484
|
+
*/
|
|
485
|
+
#restoreFailedTagOperations (pending) {
|
|
486
|
+
if (!pending.inFlightTagOperations) return
|
|
487
|
+
for (const [recordId, operations] of pending.inFlightTagOperations) {
|
|
488
|
+
const record = this.#recordsById.get(recordId)
|
|
489
|
+
if (!record || this.#newRecordsById.has(recordId) || this.#deletedRecordIds.has(recordId)) continue
|
|
146
490
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
491
|
+
const update = this.#updatedRecordsById.get(recordId) ?? { id: recordId }
|
|
492
|
+
const queuedOperations = this.#pendingTagOperations.get(recordId)
|
|
493
|
+
const restoredOperations = queuedOperations
|
|
494
|
+
? { replace: [...record.tags] }
|
|
495
|
+
: copyTagOperations(operations)
|
|
496
|
+
this.#pendingTagOperations.set(recordId, restoredOperations)
|
|
497
|
+
update.tagOperations = restoredOperations
|
|
498
|
+
this.#updatedRecordsById.set(recordId, update)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Clear the changes represented by a completed batch while retaining concurrent local edits.
|
|
504
|
+
* @param {PendingBatch} pending
|
|
505
|
+
* @returns {void}
|
|
506
|
+
*/
|
|
507
|
+
#clearCommittedChanges (pending) {
|
|
508
|
+
for (const [recordId, payload] of pending.insertPayloads) {
|
|
509
|
+
const current = this.#newRecordsById.get(recordId)
|
|
510
|
+
if (!current) {
|
|
511
|
+
this.#deletedRecordIds.add(recordId)
|
|
512
|
+
continue
|
|
152
513
|
}
|
|
153
|
-
if (
|
|
154
|
-
|
|
514
|
+
if (valuesAreEqual(serializedRecord(current), payload)) {
|
|
515
|
+
this.#newRecordsById.delete(recordId)
|
|
516
|
+
this.#pendingTagOperations.delete(recordId)
|
|
517
|
+
continue
|
|
155
518
|
}
|
|
156
|
-
|
|
157
|
-
|
|
519
|
+
|
|
520
|
+
this.#newRecordsById.delete(recordId)
|
|
521
|
+
const update = this.#updatedRecordsById.get(recordId) ??
|
|
522
|
+
updateFromInsertedRecord(recordId, current, payload)
|
|
523
|
+
const queuedOperations = this.#pendingTagOperations.get(recordId)
|
|
524
|
+
if (queuedOperations) update.tagOperations = queuedOperations
|
|
525
|
+
this.#updatedRecordsById.set(recordId, update)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
for (const [recordId, payload] of pending.updatePayloads) {
|
|
529
|
+
const current = this.#updatedRecordsById.get(recordId)
|
|
530
|
+
const queuedOperations = this.#pendingTagOperations.get(recordId)
|
|
531
|
+
if (!current || queuedOperations) continue
|
|
532
|
+
|
|
533
|
+
const comparison = { ...current }
|
|
534
|
+
const committedOperations = pending.inFlightTagOperations?.get(recordId)
|
|
535
|
+
if (committedOperations) comparison.tagOperations = committedOperations
|
|
536
|
+
if (valuesAreEqual(serializedRecordUpdate(comparison), payload)) {
|
|
537
|
+
this.#updatedRecordsById.delete(recordId)
|
|
538
|
+
this.#pendingTagOperations.delete(recordId)
|
|
158
539
|
}
|
|
159
|
-
|
|
160
|
-
})
|
|
540
|
+
}
|
|
161
541
|
|
|
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}`)
|
|
542
|
+
for (const recordId of pending.deleteRecordIds) {
|
|
543
|
+
if (!this.#recordsById.has(recordId)) this.#deletedRecordIds.delete(recordId)
|
|
171
544
|
}
|
|
545
|
+
}
|
|
172
546
|
|
|
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 {
|
|
547
|
+
#updateVersionFromMutationResult (result) {
|
|
548
|
+
const pushedVersion = versionFromMutationResult(result)
|
|
549
|
+
if (pushedVersion !== null) {
|
|
183
550
|
this.#version = pushedVersion
|
|
184
|
-
this.#latestVersion =
|
|
551
|
+
this.#latestVersion = pushedVersion
|
|
552
|
+
return
|
|
185
553
|
}
|
|
186
554
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
this.#recordIds.push(recordId)
|
|
555
|
+
const latestVersion = Number(this.#latestVersion)
|
|
556
|
+
if (Number.isFinite(latestVersion)) {
|
|
557
|
+
this.#version = latestVersion + 1
|
|
558
|
+
this.#latestVersion = this.#version
|
|
559
|
+
} else {
|
|
560
|
+
this.#version = null
|
|
195
561
|
}
|
|
196
|
-
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#addRecord (record) {
|
|
565
|
+
if (this.#recordsById.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
566
|
+
this.#records.push(record)
|
|
567
|
+
this.#recordsById.set(record.id, record)
|
|
568
|
+
this.#deletedRecordIds.delete(record.id)
|
|
569
|
+
this.#newRecordsById.set(record.id, record)
|
|
570
|
+
}
|
|
197
571
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
572
|
+
#addExistingRecord (record) {
|
|
573
|
+
if (record.id === null || record.id === undefined || record.id === '') {
|
|
574
|
+
throw new Error('Dataset records pulled from the backend must have an id')
|
|
575
|
+
}
|
|
576
|
+
if (!(record instanceof DatasetRecord)) {
|
|
577
|
+
record = new DatasetRecord(
|
|
578
|
+
record.input,
|
|
579
|
+
record.expectedOutput,
|
|
580
|
+
record.metadata,
|
|
581
|
+
record.id,
|
|
582
|
+
record.tags
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
if (this.#recordsById.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
586
|
+
this.#records.push(record)
|
|
587
|
+
this.#recordsById.set(record.id, record)
|
|
588
|
+
}
|
|
201
589
|
|
|
202
|
-
|
|
590
|
+
#recordAt (index) {
|
|
591
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.#records.length) {
|
|
592
|
+
throw new RangeError(`Dataset record index ${index} is out of range`)
|
|
593
|
+
}
|
|
594
|
+
return this.#records[index]
|
|
203
595
|
}
|
|
204
596
|
}
|
|
205
597
|
|