dd-trace 6.11.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/index.d.ts +20 -3
- package/package.json +18 -17
- package/packages/datadog-instrumentations/src/cucumber.js +4 -4
- package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
- package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
- package/packages/datadog-instrumentations/src/playwright.js +33 -33
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +6 -8
- package/packages/datadog-plugin-playwright/src/index.js +4 -0
- 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/early-flake-detection/get-known-tests.js +5 -5
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
- package/packages/dd-trace/src/debugger/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
- package/packages/dd-trace/src/debugger/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/common/request.js +5 -2
- package/packages/dd-trace/src/llmobs/experiments/client.js +6 -2
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +249 -11
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +23 -10
- package/packages/dd-trace/src/llmobs/experiments/index.js +18 -6
- package/packages/dd-trace/src/llmobs/experiments/noop.js +64 -4
- package/packages/dd-trace/src/llmobs/experiments/util.js +60 -4
- package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
- package/packages/dd-trace/src/openfeature/index.js +7 -4
- package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
- package/packages/dd-trace/src/openfeature/writers/exposures.js +62 -14
- package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +22 -13
- package/packages/dd-trace/src/telemetry/metrics.js +62 -8
- package/packages/dd-trace/src/telemetry/send-data.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
|
@@ -2,16 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomUUID } = require('node:crypto')
|
|
4
4
|
|
|
5
|
-
|
|
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? }.
|
|
6
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 = []) {
|
|
9
22
|
if (id != null && (typeof id !== 'string' || id.length === 0)) {
|
|
10
23
|
throw new Error('record id must be a non-empty string')
|
|
11
24
|
}
|
|
12
25
|
this.input = input
|
|
13
26
|
this.expectedOutput = expectedOutput ?? null
|
|
14
27
|
this.metadata = metadata ?? {}
|
|
28
|
+
this.tags = validateTagsList(tags)
|
|
15
29
|
this.id = id ?? randomUUID()
|
|
16
30
|
}
|
|
17
31
|
}
|
|
@@ -21,12 +35,14 @@ function versionFromMutationResult (result) {
|
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
function serializedRecord (record) {
|
|
24
|
-
|
|
38
|
+
const output = {
|
|
25
39
|
id: record.id,
|
|
26
40
|
input: record.input,
|
|
27
41
|
expected_output: record.expectedOutput ?? null,
|
|
28
42
|
metadata: record.metadata ?? {},
|
|
29
43
|
}
|
|
44
|
+
if (record.tags.length > 0) output.tags = record.tags
|
|
45
|
+
return output
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
function serializedRecordUpdate (update) {
|
|
@@ -36,9 +52,57 @@ function serializedRecordUpdate (update) {
|
|
|
36
52
|
output.expected_output = update.expectedOutput
|
|
37
53
|
}
|
|
38
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
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
|
39
66
|
return output
|
|
40
67
|
}
|
|
41
68
|
|
|
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
|
+
|
|
42
106
|
function valuesAreEqual (left, right) {
|
|
43
107
|
return JSON.stringify(left) === JSON.stringify(right)
|
|
44
108
|
}
|
|
@@ -63,21 +127,25 @@ class Dataset {
|
|
|
63
127
|
#newRecordsById
|
|
64
128
|
#updatedRecordsById
|
|
65
129
|
#deletedRecordIds
|
|
130
|
+
#pendingTagOperations
|
|
66
131
|
#id
|
|
67
132
|
#projectId
|
|
68
133
|
#version
|
|
69
134
|
#latestVersion
|
|
70
135
|
#pushPromise
|
|
136
|
+
#filterTags
|
|
71
137
|
|
|
72
|
-
constructor (client, name, description = '') {
|
|
138
|
+
constructor (client, name, description = '', filterTags = []) {
|
|
73
139
|
this.#client = client
|
|
74
140
|
this.#name = name
|
|
75
141
|
this.#description = description
|
|
142
|
+
this.#filterTags = validateTagsList(filterTags)
|
|
76
143
|
this.#records = []
|
|
77
144
|
this.#recordsById = new Map()
|
|
78
145
|
this.#newRecordsById = new Map()
|
|
79
146
|
this.#updatedRecordsById = new Map()
|
|
80
147
|
this.#deletedRecordIds = new Set()
|
|
148
|
+
this.#pendingTagOperations = new Map()
|
|
81
149
|
this.#id = null
|
|
82
150
|
this.#projectId = null
|
|
83
151
|
this.#version = null
|
|
@@ -86,8 +154,8 @@ class Dataset {
|
|
|
86
154
|
}
|
|
87
155
|
|
|
88
156
|
// Build a Dataset that already exists remotely (used by pullDataset).
|
|
89
|
-
static fromExisting (client, name, description, id, projectId, records, version, latestVersion) {
|
|
90
|
-
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)
|
|
91
159
|
dataset.#id = id
|
|
92
160
|
dataset.#projectId = projectId
|
|
93
161
|
dataset.#version = version ?? null
|
|
@@ -97,14 +165,70 @@ class Dataset {
|
|
|
97
165
|
}
|
|
98
166
|
|
|
99
167
|
// Append a record. Accepts a DatasetRecord or (input, expectedOutput?, metadata?).
|
|
100
|
-
addRecord (recordOrInput, expectedOutput, metadata) {
|
|
168
|
+
addRecord (recordOrInput, expectedOutput, metadata, tags) {
|
|
101
169
|
const record = recordOrInput instanceof DatasetRecord
|
|
102
170
|
? recordOrInput
|
|
103
|
-
: new DatasetRecord(recordOrInput, expectedOutput, metadata)
|
|
171
|
+
: new DatasetRecord(recordOrInput, expectedOutput, metadata, null, tags)
|
|
104
172
|
this.#addRecord(record)
|
|
105
173
|
return this
|
|
106
174
|
}
|
|
107
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
|
+
|
|
108
232
|
/**
|
|
109
233
|
* Update an existing dataset record. New records are updated in place and are sent with their insert.
|
|
110
234
|
* @param {number} index Dataset record index.
|
|
@@ -130,6 +254,9 @@ class Dataset {
|
|
|
130
254
|
|
|
131
255
|
const update = this.#updatedRecordsById.get(record.id) ?? { id: record.id }
|
|
132
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
|
+
}
|
|
133
260
|
this.#updatedRecordsById.set(record.id, update)
|
|
134
261
|
return this
|
|
135
262
|
}
|
|
@@ -144,13 +271,47 @@ class Dataset {
|
|
|
144
271
|
this.#records.splice(index, 1)
|
|
145
272
|
this.#recordsById.delete(record.id)
|
|
146
273
|
|
|
147
|
-
if (this.#newRecordsById.delete(record.id))
|
|
274
|
+
if (this.#newRecordsById.delete(record.id)) {
|
|
275
|
+
this.#pendingTagOperations.delete(record.id)
|
|
276
|
+
return this
|
|
277
|
+
}
|
|
148
278
|
|
|
149
279
|
this.#updatedRecordsById.delete(record.id)
|
|
280
|
+
this.#pendingTagOperations.delete(record.id)
|
|
150
281
|
this.#deletedRecordIds.add(record.id)
|
|
151
282
|
return this
|
|
152
283
|
}
|
|
153
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
|
+
|
|
154
315
|
name () {
|
|
155
316
|
return this.#name
|
|
156
317
|
}
|
|
@@ -183,6 +344,14 @@ class Dataset {
|
|
|
183
344
|
return this.#latestVersion
|
|
184
345
|
}
|
|
185
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
|
+
|
|
186
355
|
// Dashboard URL for this dataset, or null until pushed/pulled.
|
|
187
356
|
url () {
|
|
188
357
|
if (this.#id === null) return null
|
|
@@ -228,10 +397,13 @@ class Dataset {
|
|
|
228
397
|
const pending = this.#pendingBatch()
|
|
229
398
|
if (pending.totalCount === 0) return { pushedCount: 0, totalCount: 0 }
|
|
230
399
|
|
|
400
|
+
this.#detachCommittedTagOperations(pending)
|
|
401
|
+
|
|
231
402
|
let result
|
|
232
403
|
try {
|
|
233
404
|
result = await this.#client.batchUpdateDatasetRecords(projectId, this.#id, pending.attributes)
|
|
234
405
|
} catch (err) {
|
|
406
|
+
this.#restoreFailedTagOperations(pending)
|
|
235
407
|
throw new Error(`Failed to push changes to dataset '${this.#name}': ${err.message}`)
|
|
236
408
|
}
|
|
237
409
|
|
|
@@ -252,6 +424,9 @@ class Dataset {
|
|
|
252
424
|
const updateRecords = []
|
|
253
425
|
const updatePayloads = new Map()
|
|
254
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
|
|
255
430
|
const payload = serializedRecordUpdate(update)
|
|
256
431
|
updateRecords.push(payload)
|
|
257
432
|
updatePayloads.set(recordId, payload)
|
|
@@ -277,6 +452,58 @@ class Dataset {
|
|
|
277
452
|
}
|
|
278
453
|
}
|
|
279
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
|
|
490
|
+
|
|
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
|
+
*/
|
|
280
507
|
#clearCommittedChanges (pending) {
|
|
281
508
|
for (const [recordId, payload] of pending.insertPayloads) {
|
|
282
509
|
const current = this.#newRecordsById.get(recordId)
|
|
@@ -286,19 +513,29 @@ class Dataset {
|
|
|
286
513
|
}
|
|
287
514
|
if (valuesAreEqual(serializedRecord(current), payload)) {
|
|
288
515
|
this.#newRecordsById.delete(recordId)
|
|
516
|
+
this.#pendingTagOperations.delete(recordId)
|
|
289
517
|
continue
|
|
290
518
|
}
|
|
291
519
|
|
|
292
520
|
this.#newRecordsById.delete(recordId)
|
|
293
521
|
const update = this.#updatedRecordsById.get(recordId) ??
|
|
294
522
|
updateFromInsertedRecord(recordId, current, payload)
|
|
523
|
+
const queuedOperations = this.#pendingTagOperations.get(recordId)
|
|
524
|
+
if (queuedOperations) update.tagOperations = queuedOperations
|
|
295
525
|
this.#updatedRecordsById.set(recordId, update)
|
|
296
526
|
}
|
|
297
527
|
|
|
298
528
|
for (const [recordId, payload] of pending.updatePayloads) {
|
|
299
529
|
const current = this.#updatedRecordsById.get(recordId)
|
|
300
|
-
|
|
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)) {
|
|
301
537
|
this.#updatedRecordsById.delete(recordId)
|
|
538
|
+
this.#pendingTagOperations.delete(recordId)
|
|
302
539
|
}
|
|
303
540
|
}
|
|
304
541
|
|
|
@@ -341,7 +578,8 @@ class Dataset {
|
|
|
341
578
|
record.input,
|
|
342
579
|
record.expectedOutput,
|
|
343
580
|
record.metadata,
|
|
344
|
-
record.id
|
|
581
|
+
record.id,
|
|
582
|
+
record.tags
|
|
345
583
|
)
|
|
346
584
|
}
|
|
347
585
|
if (this.#recordsById.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
normalizeEvaluators,
|
|
14
14
|
mergeTags,
|
|
15
15
|
normalizeJsonMetricValue,
|
|
16
|
+
recordTagsToObject,
|
|
16
17
|
sleep,
|
|
17
18
|
stringify,
|
|
18
19
|
timestampMs,
|
|
@@ -20,7 +21,7 @@ const {
|
|
|
20
21
|
} = require('./util')
|
|
21
22
|
|
|
22
23
|
// One span per experiment row (LLM Obs experiment span wire format).
|
|
23
|
-
function toSpan (row, metadata, ids, spanName, userTags) {
|
|
24
|
+
function toSpan (row, metadata, ids, spanName, userTags, recordTags) {
|
|
24
25
|
const meta = {
|
|
25
26
|
input: row.input ?? null,
|
|
26
27
|
output: row.output ?? null,
|
|
@@ -33,6 +34,20 @@ function toSpan (row, metadata, ids, spanName, userTags) {
|
|
|
33
34
|
meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: row.errorStack ?? '' }
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
const tags = buildTags({
|
|
38
|
+
...userTags,
|
|
39
|
+
...recordTagsToObject(recordTags),
|
|
40
|
+
}, {
|
|
41
|
+
experiment_id: ids.experimentId,
|
|
42
|
+
run_id: ids.runId,
|
|
43
|
+
run_iteration: ids.runIteration,
|
|
44
|
+
project_id: ids.projectId,
|
|
45
|
+
dataset_id: ids.datasetId,
|
|
46
|
+
dataset_record_id: ids.datasetRecordId,
|
|
47
|
+
dataset_name: ids.datasetName,
|
|
48
|
+
experiment_name: ids.experimentName,
|
|
49
|
+
})
|
|
50
|
+
|
|
36
51
|
return {
|
|
37
52
|
span_id: row.spanId,
|
|
38
53
|
trace_id: row.traceId,
|
|
@@ -43,13 +58,7 @@ function toSpan (row, metadata, ids, spanName, userTags) {
|
|
|
43
58
|
duration: row.durationNs,
|
|
44
59
|
status: row.isError ? 'error' : 'ok',
|
|
45
60
|
meta,
|
|
46
|
-
tags
|
|
47
|
-
experiment_id: ids.experimentId,
|
|
48
|
-
run_id: ids.runId,
|
|
49
|
-
run_iteration: ids.runIteration,
|
|
50
|
-
dataset_id: ids.datasetId,
|
|
51
|
-
dataset_record_id: ids.datasetRecordId,
|
|
52
|
-
}),
|
|
61
|
+
tags,
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
|
|
@@ -152,6 +161,8 @@ class Experiment {
|
|
|
152
161
|
this.#evaluators = normalizeEvaluators(options.evaluators, 'row')
|
|
153
162
|
this.#summaryEvaluators = normalizeEvaluators(options.summaryEvaluators, 'summary')
|
|
154
163
|
this.#config = { ...options.config }
|
|
164
|
+
const filterTags = this.#dataset.filterTags?.() ?? []
|
|
165
|
+
if (filterTags.length > 0) this.#config.filtered_record_tags = filterTags
|
|
155
166
|
this.#tags = { ...options.tags }
|
|
156
167
|
this.#metadata = { ...options.metadata }
|
|
157
168
|
this.#projectId = null
|
|
@@ -454,9 +465,11 @@ class Experiment {
|
|
|
454
465
|
projectId,
|
|
455
466
|
datasetId,
|
|
456
467
|
datasetRecordId,
|
|
468
|
+
datasetName: this.#dataset.name(),
|
|
469
|
+
experimentName: this.#name,
|
|
457
470
|
runId,
|
|
458
471
|
runIteration,
|
|
459
|
-
}, this.#task.name || this.#name, this.#tags))
|
|
472
|
+
}, this.#task.name || this.#name, this.#tags, record.tags))
|
|
460
473
|
}
|
|
461
474
|
}
|
|
462
475
|
|
|
@@ -521,7 +534,7 @@ class Experiment {
|
|
|
521
534
|
dataset_name: this.#dataset.name(),
|
|
522
535
|
experiment_name: this.#name,
|
|
523
536
|
}
|
|
524
|
-
const tags = mergeTags(this.#tags, autoTags)
|
|
537
|
+
const tags = mergeTags(this.#tags, { ...recordTagsToObject(record.tags), ...autoTags })
|
|
525
538
|
|
|
526
539
|
const execute = () => this.#runWithRetries(
|
|
527
540
|
() => this.#task(record.input, this.#config, record.metadata),
|
|
@@ -4,6 +4,7 @@ const log = require('../../log')
|
|
|
4
4
|
const { ExperimentsClient } = require('./client')
|
|
5
5
|
const { Dataset, DatasetRecord } = require('./dataset')
|
|
6
6
|
const { Experiment, ExternalExperiment } = require('./experiment')
|
|
7
|
+
const { validateTagsList } = require('./util')
|
|
7
8
|
const NoopExperiments = require('./noop')
|
|
8
9
|
|
|
9
10
|
// Poll `attempt` with exponential backoff until it returns true or the time
|
|
@@ -66,7 +67,9 @@ class Experiments {
|
|
|
66
67
|
if (recordIds.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
67
68
|
recordIds.add(record.id)
|
|
68
69
|
}
|
|
69
|
-
dataset.addRecord(
|
|
70
|
+
dataset.addRecord(
|
|
71
|
+
new DatasetRecord(record.inputData, record.expectedOutput, record.metadata, record.id, record.tags)
|
|
72
|
+
)
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
return dataset
|
|
@@ -74,14 +77,16 @@ class Experiments {
|
|
|
74
77
|
|
|
75
78
|
// Pull an existing dataset by name (with its records). Polls with exponential
|
|
76
79
|
// backoff to absorb read-after-write lag; pass `expectedRecordCount` to also
|
|
77
|
-
// wait until that many records are readable.
|
|
80
|
+
// wait until that many records are readable. Pass `tags` to filter records by
|
|
81
|
+
// dataset record tags.
|
|
78
82
|
async pullDataset (name, options = {}) {
|
|
79
|
-
const { expectedRecordCount, maxWaitMs = 30_000, version } = options
|
|
83
|
+
const { expectedRecordCount, maxWaitMs = 30_000, tags, version } = options
|
|
84
|
+
const filterTags = validateTagsList(tags)
|
|
80
85
|
const projectId = await this.#client.ensureProjectId()
|
|
81
86
|
|
|
82
87
|
let pulledDataset = null
|
|
83
88
|
let records = []
|
|
84
|
-
|
|
89
|
+
const datasetVersion = version ?? null
|
|
85
90
|
let latestVersion = null
|
|
86
91
|
let lastError = ''
|
|
87
92
|
|
|
@@ -93,7 +98,6 @@ class Experiments {
|
|
|
93
98
|
if (dataset.name() === name) {
|
|
94
99
|
pulledDataset = dataset
|
|
95
100
|
latestVersion = dataset.latestVersion()
|
|
96
|
-
datasetVersion = version ?? latestVersion
|
|
97
101
|
break
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -107,6 +111,7 @@ class Experiments {
|
|
|
107
111
|
// eslint-disable-next-line no-await-in-loop
|
|
108
112
|
const page = await this.#client.listDatasetRecords(projectId, pulledDataset.id(), {
|
|
109
113
|
cursor,
|
|
114
|
+
tags: filterTags,
|
|
110
115
|
version: datasetVersion,
|
|
111
116
|
})
|
|
112
117
|
for (const record of page.records) recs.push(record)
|
|
@@ -139,6 +144,12 @@ class Experiments {
|
|
|
139
144
|
)
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
for (const record of records) {
|
|
148
|
+
if (record.id === null || record.id === undefined || record.id === '') {
|
|
149
|
+
throw new Error(`Failed to pull dataset '${name}': backend returned a record without an id`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
142
153
|
return Dataset.fromExisting(
|
|
143
154
|
this.#client,
|
|
144
155
|
name,
|
|
@@ -147,7 +158,8 @@ class Experiments {
|
|
|
147
158
|
projectId,
|
|
148
159
|
records,
|
|
149
160
|
datasetVersion,
|
|
150
|
-
latestVersion
|
|
161
|
+
latestVersion,
|
|
162
|
+
filterTags
|
|
151
163
|
)
|
|
152
164
|
}
|
|
153
165
|
|
|
@@ -11,20 +11,29 @@ class NoopDataset {
|
|
|
11
11
|
#name
|
|
12
12
|
#description
|
|
13
13
|
#records
|
|
14
|
+
#filterTags
|
|
14
15
|
|
|
15
16
|
constructor (name = '', options = {}) {
|
|
16
17
|
this.#name = name
|
|
17
18
|
this.#description = typeof options === 'string' ? options : (options.description ?? '')
|
|
19
|
+
this.#filterTags = typeof options === 'string' ? [] : [...(options.filterTags ?? [])]
|
|
18
20
|
this.#records = (typeof options === 'string' ? [] : (options.records ?? [])).map(record => ({
|
|
19
21
|
id: record.id ?? null,
|
|
20
22
|
input: record.inputData,
|
|
21
23
|
expectedOutput: record.expectedOutput ?? null,
|
|
22
24
|
metadata: record.metadata ?? {},
|
|
25
|
+
tags: [...(record.tags ?? [])],
|
|
23
26
|
}))
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
addRecord (input, expectedOutput, metadata) {
|
|
27
|
-
this.#records.push({
|
|
29
|
+
addRecord (input, expectedOutput, metadata, tags) {
|
|
30
|
+
this.#records.push({
|
|
31
|
+
id: null,
|
|
32
|
+
input,
|
|
33
|
+
expectedOutput: expectedOutput ?? null,
|
|
34
|
+
metadata: metadata ?? {},
|
|
35
|
+
tags: [...(tags ?? [])],
|
|
36
|
+
})
|
|
28
37
|
return this
|
|
29
38
|
}
|
|
30
39
|
|
|
@@ -46,6 +55,49 @@ class NoopDataset {
|
|
|
46
55
|
return Promise.resolve({ pushedCount: 0, totalCount: 0 })
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Add tags to a dataset record.
|
|
60
|
+
* @param {number} index Dataset record index.
|
|
61
|
+
* @param {string[]} tags Tags in key:value format.
|
|
62
|
+
* @returns {NoopDataset} This dataset for chaining.
|
|
63
|
+
*/
|
|
64
|
+
addTags (index, tags) {
|
|
65
|
+
const record = this.#records[index]
|
|
66
|
+
if (!record) return this
|
|
67
|
+
const normalizedTags = tags ?? []
|
|
68
|
+
record.tags = [...new Set([...(record.tags ?? []), ...normalizedTags])].sort()
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remove tags from a dataset record.
|
|
74
|
+
* @param {number} index Dataset record index.
|
|
75
|
+
* @param {string[]} tags Tags in key:value format.
|
|
76
|
+
* @returns {NoopDataset} This dataset for chaining.
|
|
77
|
+
*/
|
|
78
|
+
removeTags (index, tags) {
|
|
79
|
+
const record = this.#records[index]
|
|
80
|
+
if (!record) return this
|
|
81
|
+
const normalizedTags = tags ?? []
|
|
82
|
+
const removed = new Set(normalizedTags)
|
|
83
|
+
record.tags = (record.tags ?? []).filter(tag => !removed.has(tag)).sort()
|
|
84
|
+
return this
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Replace all tags on a dataset record.
|
|
89
|
+
* @param {number} index Dataset record index.
|
|
90
|
+
* @param {string[]} tags Tags in key:value format.
|
|
91
|
+
* @returns {NoopDataset} This dataset for chaining.
|
|
92
|
+
*/
|
|
93
|
+
replaceTags (index, tags) {
|
|
94
|
+
const record = this.#records[index]
|
|
95
|
+
if (!record) return this
|
|
96
|
+
const normalizedTags = tags ?? []
|
|
97
|
+
record.tags = [...normalizedTags]
|
|
98
|
+
return this
|
|
99
|
+
}
|
|
100
|
+
|
|
49
101
|
name () {
|
|
50
102
|
return this.#name
|
|
51
103
|
}
|
|
@@ -70,6 +122,14 @@ class NoopDataset {
|
|
|
70
122
|
return null
|
|
71
123
|
}
|
|
72
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Return the tags used to filter this dataset.
|
|
127
|
+
* @returns {string[]} Dataset record filter tags.
|
|
128
|
+
*/
|
|
129
|
+
filterTags () {
|
|
130
|
+
return [...this.#filterTags]
|
|
131
|
+
}
|
|
132
|
+
|
|
73
133
|
records () {
|
|
74
134
|
return [...this.#records]
|
|
75
135
|
}
|
|
@@ -156,9 +216,9 @@ class NoopExperiments {
|
|
|
156
216
|
return new NoopDataset(name, options)
|
|
157
217
|
}
|
|
158
218
|
|
|
159
|
-
pullDataset (name) {
|
|
219
|
+
pullDataset (name, options = {}) {
|
|
160
220
|
this.#warn()
|
|
161
|
-
return Promise.resolve(new NoopDataset(name))
|
|
221
|
+
return Promise.resolve(new NoopDataset(name, { filterTags: options.tags }))
|
|
162
222
|
}
|
|
163
223
|
|
|
164
224
|
experiment (options = {}) {
|