dd-trace 6.11.0 → 6.13.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.
Files changed (139) hide show
  1. package/LICENSE-3rdparty.csv +5 -0
  2. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  3. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  4. package/index.d.ts +143 -25
  5. package/package.json +26 -25
  6. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  7. package/packages/datadog-instrumentations/src/cucumber.js +4 -4
  8. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  9. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  12. package/packages/datadog-instrumentations/src/jest.js +140 -18
  13. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  14. package/packages/datadog-instrumentations/src/mariadb.js +7 -0
  15. package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
  16. package/packages/datadog-instrumentations/src/pino.js +15 -4
  17. package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
  18. package/packages/datadog-instrumentations/src/playwright.js +67 -53
  19. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +13 -12
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
  21. package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
  22. package/packages/datadog-instrumentations/src/winston.js +6 -3
  23. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  24. package/packages/datadog-plugin-http/src/client.js +2 -15
  25. package/packages/datadog-plugin-http2/src/client.js +2 -15
  26. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  27. package/packages/datadog-plugin-playwright/src/index.js +13 -1
  28. package/packages/datadog-plugin-undici/src/index.js +2 -15
  29. package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
  30. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  31. package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
  32. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  33. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  34. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  35. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  36. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  37. package/packages/dd-trace/src/appsec/reporter.js +2 -0
  38. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  39. package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
  40. package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
  41. package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
  42. package/packages/dd-trace/src/carrier.js +107 -48
  43. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
  44. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
  46. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
  47. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
  48. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
  49. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
  50. package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
  51. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
  52. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
  53. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  54. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
  55. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
  56. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
  57. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
  58. package/packages/dd-trace/src/config/index.js +2 -1
  59. package/packages/dd-trace/src/config/supported-configurations.json +27 -0
  60. package/packages/dd-trace/src/debugger/constants.js +1 -0
  61. package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
  62. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
  63. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
  64. package/packages/dd-trace/src/debugger/index.js +4 -2
  65. package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
  66. package/packages/dd-trace/src/dogstatsd.js +45 -18
  67. package/packages/dd-trace/src/encode/agentless-json.js +5 -4
  68. package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
  69. package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
  70. package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
  71. package/packages/dd-trace/src/evp_proxy/path.js +22 -0
  72. package/packages/dd-trace/src/exporters/agent/index.js +22 -2
  73. package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
  74. package/packages/dd-trace/src/exporters/common/request.js +5 -2
  75. package/packages/dd-trace/src/exporters/common/writer.js +24 -2
  76. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  77. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  78. package/packages/dd-trace/src/flush.js +102 -0
  79. package/packages/dd-trace/src/llmobs/experiments/client.js +10 -2
  80. package/packages/dd-trace/src/llmobs/experiments/dataset.js +301 -14
  81. package/packages/dd-trace/src/llmobs/experiments/experiment.js +440 -112
  82. package/packages/dd-trace/src/llmobs/experiments/index.js +68 -44
  83. package/packages/dd-trace/src/llmobs/experiments/noop.js +103 -12
  84. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  85. package/packages/dd-trace/src/llmobs/experiments/util.js +81 -4
  86. package/packages/dd-trace/src/llmobs/index.js +80 -17
  87. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  88. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  89. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  90. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  91. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  92. package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
  93. package/packages/dd-trace/src/noop/span.js +5 -0
  94. package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
  95. package/packages/dd-trace/src/openfeature/index.js +7 -4
  96. package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
  97. package/packages/dd-trace/src/openfeature/writers/exposures.js +70 -15
  98. package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
  99. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  100. package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
  101. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  102. package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
  103. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  104. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
  105. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
  106. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  107. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  108. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  109. package/packages/dd-trace/src/opentracing/propagation/text_map.js +429 -352
  110. package/packages/dd-trace/src/opentracing/span.js +28 -0
  111. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  112. package/packages/dd-trace/src/plugin_manager.js +2 -0
  113. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  114. package/packages/dd-trace/src/plugins/util/test.js +74 -21
  115. package/packages/dd-trace/src/plugins/util/web.js +2 -65
  116. package/packages/dd-trace/src/profiling/oom.js +4 -3
  117. package/packages/dd-trace/src/profiling/profiler.js +7 -0
  118. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  119. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  120. package/packages/dd-trace/src/proxy.js +36 -8
  121. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  122. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
  123. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
  124. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
  125. package/packages/dd-trace/src/serverless/vercel.js +122 -0
  126. package/packages/dd-trace/src/serverless.js +37 -20
  127. package/packages/dd-trace/src/span_stats.js +40 -3
  128. package/packages/dd-trace/src/telemetry/metrics.js +62 -8
  129. package/packages/dd-trace/src/telemetry/send-data.js +1 -0
  130. package/packages/dd-trace/src/tracer.js +22 -0
  131. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  132. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  133. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  134. package/vendor/dist/https-proxy-agent/LICENSE +22 -0
  135. package/vendor/dist/https-proxy-agent/index.js +4 -0
  136. package/vendor/dist/protobufjs/index.js +1 -1
  137. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  138. package/vendor/dist/proxy-from-env/LICENSE +20 -0
  139. package/vendor/dist/proxy-from-env/index.js +1 -0
@@ -1,17 +1,40 @@
1
1
  'use strict'
2
2
 
3
3
  const { randomUUID } = require('node:crypto')
4
-
5
- // Dataset record: { input, expectedOutput?, metadata?, id }.
4
+ const snapshotPayload = require('../../../../../vendor/dist/rfdc')({ proto: false, circles: false })
5
+
6
+ /** @typedef {{add?: string[], remove?: string[], replace?: string[]}} TagOperations */
7
+ /**
8
+ * @typedef {object} DatasetRecordNew
9
+ * @property {string} [id]
10
+ * @property {unknown} inputData
11
+ * @property {unknown} [expectedOutput]
12
+ * @property {Record<string, unknown>} [metadata]
13
+ * @property {string[]} [tags]
14
+ */
15
+ /**
16
+ * @typedef {object} PendingBatch
17
+ * @property {object} attributes
18
+ * @property {string[]} deleteRecordIds
19
+ * @property {Map<string, object>} insertPayloads
20
+ * @property {Map<string, object>} updatePayloads
21
+ * @property {number} totalCount
22
+ * @property {Map<string, TagOperations>} [inFlightTagOperations]
23
+ */
24
+
25
+ const { tagOperationsAreEmpty, validateTagsList } = require('./util')
26
+
27
+ // Dataset record: { input, expectedOutput?, metadata?, id, tags? }.
6
28
  // IDs are generated locally unless the caller supplies one.
7
29
  class DatasetRecord {
8
- constructor (input, expectedOutput = null, metadata = {}, id = null) {
30
+ constructor (input, expectedOutput = null, metadata = {}, id = null, tags = []) {
9
31
  if (id != null && (typeof id !== 'string' || id.length === 0)) {
10
32
  throw new Error('record id must be a non-empty string')
11
33
  }
12
34
  this.input = input
13
35
  this.expectedOutput = expectedOutput ?? null
14
36
  this.metadata = metadata ?? {}
37
+ this.tags = validateTagsList(tags)
15
38
  this.id = id ?? randomUUID()
16
39
  }
17
40
  }
@@ -21,12 +44,14 @@ function versionFromMutationResult (result) {
21
44
  }
22
45
 
23
46
  function serializedRecord (record) {
24
- return {
47
+ const output = {
25
48
  id: record.id,
26
49
  input: record.input,
27
50
  expected_output: record.expectedOutput ?? null,
28
51
  metadata: record.metadata ?? {},
29
52
  }
53
+ if (record.tags.length > 0) output.tags = record.tags
54
+ return output
30
55
  }
31
56
 
32
57
  function serializedRecordUpdate (update) {
@@ -36,9 +61,57 @@ function serializedRecordUpdate (update) {
36
61
  output.expected_output = update.expectedOutput
37
62
  }
38
63
  if (Object.hasOwn(update, 'metadata') && update.metadata !== undefined) output.metadata = update.metadata
64
+ if (Object.hasOwn(update, 'tagOperations')) {
65
+ output.tag_operations = serializedTagOperations(update.tagOperations)
66
+ }
67
+ return output
68
+ }
69
+
70
+ function serializedTagOperations (operations) {
71
+ const output = {}
72
+ if (Object.hasOwn(operations, 'add')) output.add = operations.add
73
+ if (Object.hasOwn(operations, 'remove')) output.remove = operations.remove
74
+ if (Object.hasOwn(operations, 'replace')) output.set = operations.replace
39
75
  return output
40
76
  }
41
77
 
78
+ /**
79
+ * @param {TagOperations} operations
80
+ * @returns {TagOperations}
81
+ */
82
+ function copyTagOperations (operations) {
83
+ return Object.fromEntries(Object.entries(operations).map(([key, tags]) => [key, [...tags]]))
84
+ }
85
+
86
+ function mergeTagOperations (operations, operation, tags) {
87
+ if (operation === 'replace') return { replace: [...tags] }
88
+ if (Object.hasOwn(operations, 'replace')) {
89
+ const replaced = new Set(operations.replace)
90
+ for (const tag of tags) {
91
+ if (operation === 'add') replaced.add(tag)
92
+ else replaced.delete(tag)
93
+ }
94
+ return { replace: [...replaced].sort() }
95
+ }
96
+
97
+ const add = new Set(operations.add)
98
+ const remove = new Set(operations.remove)
99
+ for (const tag of tags) {
100
+ if (operation === 'add') {
101
+ if (remove.has(tag)) remove.delete(tag)
102
+ else add.add(tag)
103
+ } else if (add.has(tag)) {
104
+ add.delete(tag)
105
+ } else {
106
+ remove.add(tag)
107
+ }
108
+ }
109
+ const merged = {}
110
+ if (add.size > 0) merged.add = [...add].sort()
111
+ if (remove.size > 0) merged.remove = [...remove].sort()
112
+ return merged
113
+ }
114
+
42
115
  function valuesAreEqual (left, right) {
43
116
  return JSON.stringify(left) === JSON.stringify(right)
44
117
  }
@@ -50,6 +123,9 @@ function updateFromInsertedRecord (recordId, record, payload) {
50
123
  update.expectedOutput = record.expectedOutput
51
124
  }
52
125
  if (!valuesAreEqual(record.metadata, payload.metadata)) update.metadata = record.metadata
126
+ if (!valuesAreEqual(record.tags, payload.tags ?? [])) {
127
+ update.tagOperations = { replace: [...record.tags] }
128
+ }
53
129
  return update
54
130
  }
55
131
 
@@ -63,21 +139,25 @@ class Dataset {
63
139
  #newRecordsById
64
140
  #updatedRecordsById
65
141
  #deletedRecordIds
142
+ #pendingTagOperations
66
143
  #id
67
144
  #projectId
68
145
  #version
69
146
  #latestVersion
70
147
  #pushPromise
148
+ #filterTags
71
149
 
72
- constructor (client, name, description = '') {
150
+ constructor (client, name, description = '', filterTags = []) {
73
151
  this.#client = client
74
152
  this.#name = name
75
153
  this.#description = description
154
+ this.#filterTags = validateTagsList(filterTags)
76
155
  this.#records = []
77
156
  this.#recordsById = new Map()
78
157
  this.#newRecordsById = new Map()
79
158
  this.#updatedRecordsById = new Map()
80
159
  this.#deletedRecordIds = new Set()
160
+ this.#pendingTagOperations = new Map()
81
161
  this.#id = null
82
162
  this.#projectId = null
83
163
  this.#version = null
@@ -86,8 +166,8 @@ class Dataset {
86
166
  }
87
167
 
88
168
  // 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)
169
+ static fromExisting (client, name, description, id, projectId, records, version, latestVersion, filterTags) {
170
+ const dataset = new Dataset(client, name, description, filterTags)
91
171
  dataset.#id = id
92
172
  dataset.#projectId = projectId
93
173
  dataset.#version = version ?? null
@@ -97,14 +177,100 @@ class Dataset {
97
177
  }
98
178
 
99
179
  // Append a record. Accepts a DatasetRecord or (input, expectedOutput?, metadata?).
100
- addRecord (recordOrInput, expectedOutput, metadata) {
180
+ addRecord (recordOrInput, expectedOutput, metadata, tags) {
101
181
  const record = recordOrInput instanceof DatasetRecord
102
182
  ? recordOrInput
103
- : new DatasetRecord(recordOrInput, expectedOutput, metadata)
183
+ : new DatasetRecord(recordOrInput, expectedOutput, metadata, null, tags)
104
184
  this.#addRecord(record)
105
185
  return this
106
186
  }
107
187
 
188
+ /**
189
+ * Add multiple records to a dataset.
190
+ * @param {DatasetRecordNew[]} records
191
+ * @returns {Dataset} This dataset for chaining.
192
+ */
193
+ addRecords (records) {
194
+ const newRecords = []
195
+ const recordIds = new Set(this.#recordsById.keys())
196
+
197
+ // Construct and validate the entire batch before mutating the dataset.
198
+ for (const record of records) {
199
+ if (record.id !== undefined && (typeof record.id !== 'string' || record.id.length === 0)) {
200
+ throw new Error('record id must be a non-empty string')
201
+ }
202
+ const newRecord = new DatasetRecord(
203
+ record.inputData,
204
+ record.expectedOutput,
205
+ record.metadata,
206
+ record.id,
207
+ record.tags
208
+ )
209
+ if (recordIds.has(newRecord.id)) throw new Error(`Duplicate record id '${newRecord.id}'`)
210
+ recordIds.add(newRecord.id)
211
+ newRecords.push(newRecord)
212
+ }
213
+
214
+ for (const record of newRecords) this.#addRecord(record)
215
+ return this
216
+ }
217
+
218
+ /**
219
+ * Add tags to 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
+ addTags (index, tags) {
225
+ const validated = validateTagsList(tags)
226
+ const record = this.#recordAt(index)
227
+ const next = new Set(record.tags)
228
+ const added = []
229
+ for (const tag of validated) {
230
+ if (next.has(tag)) continue
231
+ next.add(tag)
232
+ added.push(tag)
233
+ }
234
+ record.tags = [...next].sort()
235
+ this.#queueTagOperation(record.id, 'add', added)
236
+ return this
237
+ }
238
+
239
+ /**
240
+ * Remove tags from a dataset record.
241
+ * @param {number} index Dataset record index.
242
+ * @param {string[]} tags Tags in key:value format.
243
+ * @returns {Dataset} This dataset for chaining.
244
+ */
245
+ removeTags (index, tags) {
246
+ const validated = validateTagsList(tags)
247
+ const record = this.#recordAt(index)
248
+ const next = new Set(record.tags)
249
+ const removed = []
250
+ for (const tag of validated) {
251
+ if (!next.has(tag)) continue
252
+ next.delete(tag)
253
+ removed.push(tag)
254
+ }
255
+ record.tags = [...next].sort()
256
+ this.#queueTagOperation(record.id, 'remove', removed)
257
+ return this
258
+ }
259
+
260
+ /**
261
+ * Replace all tags on a dataset record.
262
+ * @param {number} index Dataset record index.
263
+ * @param {string[]} tags Tags in key:value format.
264
+ * @returns {Dataset} This dataset for chaining.
265
+ */
266
+ replaceTags (index, tags) {
267
+ const validated = validateTagsList(tags)
268
+ const record = this.#recordAt(index)
269
+ record.tags = validated
270
+ this.#queueTagOperation(record.id, 'replace', validated)
271
+ return this
272
+ }
273
+
108
274
  /**
109
275
  * Update an existing dataset record. New records are updated in place and are sent with their insert.
110
276
  * @param {number} index Dataset record index.
@@ -130,6 +296,9 @@ class Dataset {
130
296
 
131
297
  const update = this.#updatedRecordsById.get(record.id) ?? { id: record.id }
132
298
  for (const field of providedFields) update[field] = record[field]
299
+ if (this.#pendingTagOperations.has(record.id)) {
300
+ update.tagOperations = this.#pendingTagOperations.get(record.id)
301
+ }
133
302
  this.#updatedRecordsById.set(record.id, update)
134
303
  return this
135
304
  }
@@ -144,13 +313,47 @@ class Dataset {
144
313
  this.#records.splice(index, 1)
145
314
  this.#recordsById.delete(record.id)
146
315
 
147
- if (this.#newRecordsById.delete(record.id)) return this
316
+ if (this.#newRecordsById.delete(record.id)) {
317
+ this.#pendingTagOperations.delete(record.id)
318
+ return this
319
+ }
148
320
 
149
321
  this.#updatedRecordsById.delete(record.id)
322
+ this.#pendingTagOperations.delete(record.id)
150
323
  this.#deletedRecordIds.add(record.id)
151
324
  return this
152
325
  }
153
326
 
327
+ /**
328
+ * Queue a tag operation for the next dataset push.
329
+ * @param {string} recordId Dataset record id.
330
+ * @param {'add' | 'remove' | 'replace'} operation Tag operation to queue.
331
+ * @param {string[]} tags Tags in key:value format.
332
+ * @returns {void}
333
+ */
334
+ #queueTagOperation (recordId, operation, tags) {
335
+ if (operation !== 'replace' && tags.length === 0) return
336
+ const operations = mergeTagOperations(this.#pendingTagOperations.get(recordId) ?? {}, operation, tags)
337
+ if (tagOperationsAreEmpty(operations)) {
338
+ this.#pendingTagOperations.delete(recordId)
339
+ const update = this.#updatedRecordsById.get(recordId)
340
+ if (update) {
341
+ const hasRecordUpdate = Object.hasOwn(update, 'input') ||
342
+ Object.hasOwn(update, 'expectedOutput') ||
343
+ Object.hasOwn(update, 'metadata')
344
+ delete update.tagOperations
345
+ if (!hasRecordUpdate) this.#updatedRecordsById.delete(recordId)
346
+ }
347
+ return
348
+ }
349
+ this.#pendingTagOperations.set(recordId, operations)
350
+ if (!this.#newRecordsById.has(recordId)) {
351
+ const update = this.#updatedRecordsById.get(recordId) ?? { id: recordId }
352
+ update.tagOperations = operations
353
+ this.#updatedRecordsById.set(recordId, update)
354
+ }
355
+ }
356
+
154
357
  name () {
155
358
  return this.#name
156
359
  }
@@ -175,6 +378,10 @@ class Dataset {
175
378
  return this.#projectId
176
379
  }
177
380
 
381
+ projectName () {
382
+ return this.#client.projectName
383
+ }
384
+
178
385
  version () {
179
386
  return this.#version
180
387
  }
@@ -183,6 +390,14 @@ class Dataset {
183
390
  return this.#latestVersion
184
391
  }
185
392
 
393
+ /**
394
+ * Return the tags used to filter this dataset.
395
+ * @returns {string[]} Dataset record filter tags.
396
+ */
397
+ filterTags () {
398
+ return [...this.#filterTags]
399
+ }
400
+
186
401
  // Dashboard URL for this dataset, or null until pushed/pulled.
187
402
  url () {
188
403
  if (this.#id === null) return null
@@ -228,10 +443,13 @@ class Dataset {
228
443
  const pending = this.#pendingBatch()
229
444
  if (pending.totalCount === 0) return { pushedCount: 0, totalCount: 0 }
230
445
 
446
+ this.#detachCommittedTagOperations(pending)
447
+
231
448
  let result
232
449
  try {
233
450
  result = await this.#client.batchUpdateDatasetRecords(projectId, this.#id, pending.attributes)
234
451
  } catch (err) {
452
+ this.#restoreFailedTagOperations(pending)
235
453
  throw new Error(`Failed to push changes to dataset '${this.#name}': ${err.message}`)
236
454
  }
237
455
 
@@ -244,7 +462,7 @@ class Dataset {
244
462
  const insertRecords = []
245
463
  const insertPayloads = new Map()
246
464
  for (const [recordId, record] of this.#newRecordsById) {
247
- const payload = serializedRecord(record)
465
+ const payload = snapshotPayload(serializedRecord(record))
248
466
  insertRecords.push(payload)
249
467
  insertPayloads.set(recordId, payload)
250
468
  }
@@ -252,7 +470,10 @@ class Dataset {
252
470
  const updateRecords = []
253
471
  const updatePayloads = new Map()
254
472
  for (const [recordId, update] of this.#updatedRecordsById) {
255
- const payload = serializedRecordUpdate(update)
473
+ const tagOperations = this.#pendingTagOperations.get(recordId)
474
+ if (tagOperations) update.tagOperations = tagOperations
475
+ else delete update.tagOperations
476
+ const payload = snapshotPayload(serializedRecordUpdate(update))
256
477
  updateRecords.push(payload)
257
478
  updatePayloads.set(recordId, payload)
258
479
  }
@@ -277,6 +498,58 @@ class Dataset {
277
498
  }
278
499
  }
279
500
 
501
+ /**
502
+ * Detach tag changes sent by this batch so edits made while the request is in flight
503
+ * are queued relative to the response that this batch will commit.
504
+ * @param {PendingBatch} pending
505
+ * @returns {void}
506
+ */
507
+ #detachCommittedTagOperations (pending) {
508
+ pending.inFlightTagOperations = new Map()
509
+ for (const [recordId] of pending.insertPayloads) {
510
+ const operations = this.#pendingTagOperations.get(recordId)
511
+ if (!operations) continue
512
+ pending.inFlightTagOperations.set(recordId, copyTagOperations(operations))
513
+ this.#pendingTagOperations.delete(recordId)
514
+ }
515
+ for (const [recordId, payload] of pending.updatePayloads) {
516
+ const operations = this.#pendingTagOperations.get(recordId)
517
+ if (!operations || !Object.hasOwn(payload, 'tag_operations')) continue
518
+
519
+ pending.inFlightTagOperations.set(recordId, copyTagOperations(operations))
520
+ this.#pendingTagOperations.delete(recordId)
521
+ const update = this.#updatedRecordsById.get(recordId)
522
+ if (update) delete update.tagOperations
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Restore tag changes when a batch request fails, including edits made while it was in flight.
528
+ * @param {PendingBatch} pending
529
+ * @returns {void}
530
+ */
531
+ #restoreFailedTagOperations (pending) {
532
+ if (!pending.inFlightTagOperations) return
533
+ for (const [recordId, operations] of pending.inFlightTagOperations) {
534
+ const record = this.#recordsById.get(recordId)
535
+ if (!record || this.#newRecordsById.has(recordId) || this.#deletedRecordIds.has(recordId)) continue
536
+
537
+ const update = this.#updatedRecordsById.get(recordId) ?? { id: recordId }
538
+ const queuedOperations = this.#pendingTagOperations.get(recordId)
539
+ const restoredOperations = queuedOperations
540
+ ? { replace: [...record.tags] }
541
+ : copyTagOperations(operations)
542
+ this.#pendingTagOperations.set(recordId, restoredOperations)
543
+ update.tagOperations = restoredOperations
544
+ this.#updatedRecordsById.set(recordId, update)
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Clear the changes represented by a completed batch while retaining concurrent local edits.
550
+ * @param {PendingBatch} pending
551
+ * @returns {void}
552
+ */
280
553
  #clearCommittedChanges (pending) {
281
554
  for (const [recordId, payload] of pending.insertPayloads) {
282
555
  const current = this.#newRecordsById.get(recordId)
@@ -286,19 +559,32 @@ class Dataset {
286
559
  }
287
560
  if (valuesAreEqual(serializedRecord(current), payload)) {
288
561
  this.#newRecordsById.delete(recordId)
562
+ this.#pendingTagOperations.delete(recordId)
289
563
  continue
290
564
  }
291
565
 
292
566
  this.#newRecordsById.delete(recordId)
293
567
  const update = this.#updatedRecordsById.get(recordId) ??
294
568
  updateFromInsertedRecord(recordId, current, payload)
569
+ const queuedOperations = this.#pendingTagOperations.get(recordId)
570
+ if (queuedOperations) update.tagOperations = queuedOperations
571
+ if (update.tagOperations) {
572
+ this.#pendingTagOperations.set(recordId, copyTagOperations(update.tagOperations))
573
+ }
295
574
  this.#updatedRecordsById.set(recordId, update)
296
575
  }
297
576
 
298
577
  for (const [recordId, payload] of pending.updatePayloads) {
299
578
  const current = this.#updatedRecordsById.get(recordId)
300
- if (current && valuesAreEqual(serializedRecordUpdate(current), payload)) {
579
+ const queuedOperations = this.#pendingTagOperations.get(recordId)
580
+ if (!current || queuedOperations) continue
581
+
582
+ const comparison = { ...current }
583
+ const committedOperations = pending.inFlightTagOperations?.get(recordId)
584
+ if (committedOperations) comparison.tagOperations = committedOperations
585
+ if (valuesAreEqual(serializedRecordUpdate(comparison), payload)) {
301
586
  this.#updatedRecordsById.delete(recordId)
587
+ this.#pendingTagOperations.delete(recordId)
302
588
  }
303
589
  }
304
590
 
@@ -341,7 +627,8 @@ class Dataset {
341
627
  record.input,
342
628
  record.expectedOutput,
343
629
  record.metadata,
344
- record.id
630
+ record.id,
631
+ record.tags
345
632
  )
346
633
  }
347
634
  if (this.#recordsById.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)