dd-trace 6.9.0 → 6.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -1,73 +1,98 @@
1
1
  'use strict'
2
2
 
3
- const { API_BASE_PATH } = require('./client')
3
+ const { randomUUID } = require('node:crypto')
4
4
 
5
- // Dataset record: { input, expectedOutput?, metadata?, id? }.
6
- // `id` may be user-provided before push or filled from the backend-created record.
5
+ // Dataset record: { input, expectedOutput?, metadata?, id }.
6
+ // IDs are generated locally unless the caller supplies one.
7
7
  class DatasetRecord {
8
8
  constructor (input, expectedOutput = null, metadata = {}, id = null) {
9
+ if (id != null && (typeof id !== 'string' || id.length === 0)) {
10
+ throw new Error('record id must be a non-empty string')
11
+ }
9
12
  this.input = input
10
13
  this.expectedOutput = expectedOutput ?? null
11
14
  this.metadata = metadata ?? {}
12
- this.id = id ?? null
15
+ this.id = id ?? randomUUID()
13
16
  }
14
17
  }
15
18
 
16
- function createdRecordsFromResponse (response) {
17
- if (Array.isArray(response?.records)) return response.records
18
- if (Array.isArray(response?.data)) return response.data
19
- return []
19
+ function versionFromMutationResult (result) {
20
+ return result?.version ?? null
20
21
  }
21
22
 
22
- function recordIdFromCreatedRecord (record) {
23
- return String(record?.id ?? record?.attributes?.id ?? '')
23
+ function serializedRecord (record) {
24
+ return {
25
+ id: record.id,
26
+ input: record.input,
27
+ expected_output: record.expectedOutput ?? null,
28
+ metadata: record.metadata ?? {},
29
+ }
30
+ }
31
+
32
+ function serializedRecordUpdate (update) {
33
+ const output = { id: update.id }
34
+ if (Object.hasOwn(update, 'input') && update.input !== undefined) output.input = update.input
35
+ if (Object.hasOwn(update, 'expectedOutput') && update.expectedOutput !== undefined) {
36
+ output.expected_output = update.expectedOutput
37
+ }
38
+ if (Object.hasOwn(update, 'metadata') && update.metadata !== undefined) output.metadata = update.metadata
39
+ return output
24
40
  }
25
41
 
26
- function versionFromCreatedRecords (records) {
27
- const versions = records
28
- .map(record => Number(record?.attributes?.valid_from_version ?? record?.attributes?.version))
29
- .filter(Number.isFinite)
30
- if (versions.length === 0) return null
31
- return Math.max(...versions)
42
+ function valuesAreEqual (left, right) {
43
+ return JSON.stringify(left) === JSON.stringify(right)
32
44
  }
33
45
 
34
- // A local buffer of dataset records, created remotely and pushed on first run
35
- // (or eagerly via push()). Pushes are incremental.
46
+ function updateFromInsertedRecord (recordId, record, payload) {
47
+ const update = { id: recordId }
48
+ if (!valuesAreEqual(record.input, payload.input)) update.input = record.input
49
+ if (!valuesAreEqual(record.expectedOutput, payload.expected_output)) {
50
+ update.expectedOutput = record.expectedOutput
51
+ }
52
+ if (!valuesAreEqual(record.metadata, payload.metadata)) update.metadata = record.metadata
53
+ return update
54
+ }
55
+
56
+ // A local dataset model that tracks pending changes and pushes them in one batch.
36
57
  class Dataset {
37
58
  #client
38
59
  #name
39
60
  #description
40
61
  #records
41
- #recordIds
62
+ #recordsById
63
+ #newRecordsById
64
+ #updatedRecordsById
65
+ #deletedRecordIds
42
66
  #id
43
67
  #projectId
44
- #pushedCount
45
68
  #version
46
69
  #latestVersion
70
+ #pushPromise
47
71
 
48
72
  constructor (client, name, description = '') {
49
73
  this.#client = client
50
74
  this.#name = name
51
75
  this.#description = description
52
76
  this.#records = []
53
- this.#recordIds = []
77
+ this.#recordsById = new Map()
78
+ this.#newRecordsById = new Map()
79
+ this.#updatedRecordsById = new Map()
80
+ this.#deletedRecordIds = new Set()
54
81
  this.#id = null
55
82
  this.#projectId = null
56
- this.#pushedCount = 0
57
83
  this.#version = null
58
84
  this.#latestVersion = null
85
+ this.#pushPromise = Promise.resolve()
59
86
  }
60
87
 
61
88
  // Build a Dataset that already exists remotely (used by pullDataset).
62
- static fromExisting (client, name, description, id, projectId, records, recordIds, version, latestVersion) {
89
+ static fromExisting (client, name, description, id, projectId, records, version, latestVersion) {
63
90
  const dataset = new Dataset(client, name, description)
64
91
  dataset.#id = id
65
92
  dataset.#projectId = projectId
66
- dataset.#records.push(...records)
67
- dataset.#recordIds.push(...recordIds)
68
- dataset.#pushedCount = records.length
69
93
  dataset.#version = version ?? null
70
94
  dataset.#latestVersion = latestVersion ?? version ?? null
95
+ for (const record of records) dataset.#addExistingRecord(record)
71
96
  return dataset
72
97
  }
73
98
 
@@ -76,7 +101,53 @@ class Dataset {
76
101
  const record = recordOrInput instanceof DatasetRecord
77
102
  ? recordOrInput
78
103
  : new DatasetRecord(recordOrInput, expectedOutput, metadata)
79
- this.#records.push(record)
104
+ this.#addRecord(record)
105
+ return this
106
+ }
107
+
108
+ /**
109
+ * Update an existing dataset record. New records are updated in place and are sent with their insert.
110
+ * @param {number} index Dataset record index.
111
+ * @param {{input?: unknown, expectedOutput?: unknown, metadata?: object}} fields Fields to update.
112
+ * @returns {Dataset} This dataset for chaining.
113
+ */
114
+ update (index, fields) {
115
+ if (fields == null || typeof fields !== 'object' || Array.isArray(fields)) {
116
+ throw new TypeError('record update must be an object')
117
+ }
118
+ const record = this.#recordAt(index)
119
+ const fieldNames = ['input', 'expectedOutput', 'metadata']
120
+ const providedFields = fieldNames.filter(field => Object.hasOwn(fields, field) && fields[field] !== undefined)
121
+ if (providedFields.length === 0) {
122
+ throw new Error('record update must include input, expectedOutput, or metadata')
123
+ }
124
+
125
+ for (const field of providedFields) {
126
+ record[field] = field === 'metadata' ? (fields[field] ?? {}) : fields[field]
127
+ }
128
+
129
+ if (this.#newRecordsById.has(record.id)) return this
130
+
131
+ const update = this.#updatedRecordsById.get(record.id) ?? { id: record.id }
132
+ for (const field of providedFields) update[field] = record[field]
133
+ this.#updatedRecordsById.set(record.id, update)
134
+ return this
135
+ }
136
+
137
+ /**
138
+ * Delete a dataset record. New records are removed locally without a backend operation.
139
+ * @param {number} index Dataset record index.
140
+ * @returns {Dataset} This dataset for chaining.
141
+ */
142
+ delete (index) {
143
+ const record = this.#recordAt(index)
144
+ this.#records.splice(index, 1)
145
+ this.#recordsById.delete(record.id)
146
+
147
+ if (this.#newRecordsById.delete(record.id)) return this
148
+
149
+ this.#updatedRecordsById.delete(record.id)
150
+ this.#deletedRecordIds.add(record.id)
80
151
  return this
81
152
  }
82
153
 
@@ -84,12 +155,16 @@ class Dataset {
84
155
  return this.#name
85
156
  }
86
157
 
158
+ description () {
159
+ return this.#description
160
+ }
161
+
87
162
  records () {
88
163
  return [...this.#records]
89
164
  }
90
165
 
91
166
  recordIds () {
92
- return [...this.#recordIds]
167
+ return this.#records.map(record => record.id)
93
168
  }
94
169
 
95
170
  id () {
@@ -114,92 +189,171 @@ class Dataset {
114
189
  return `${this.#client.appBase}/llm/datasets/${this.#id}`
115
190
  }
116
191
 
117
- // Eagerly create the dataset (if needed) and push any unpushed records.
118
- async push () {
119
- const projectId = await this.#client.ensureProjectId()
120
- return this.ensureCreatedAndPushed(projectId)
192
+ // Eagerly create the dataset (if needed) and push all pending changes.
193
+ push () {
194
+ return this.#enqueuePush(async () => {
195
+ const projectId = await this.#client.ensureProjectId()
196
+ return this.#ensureCreatedAndPushed(projectId)
197
+ })
121
198
  }
122
199
 
123
- // Create the remote dataset if needed, then push records added since the last
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) {
200
+ // Called by Experiment.run() after it has resolved the project id.
201
+ ensureCreatedAndPushed (projectId) {
202
+ return this.#enqueuePush(() => this.#ensureCreatedAndPushed(projectId))
203
+ }
204
+
205
+ #enqueuePush (push) {
206
+ const next = this.#pushPromise.then(push, push)
207
+ this.#pushPromise = next.then(() => {}, () => {})
208
+ return next
209
+ }
210
+
211
+ async #ensureCreatedAndPushed (projectId) {
127
212
  if (this.#id === null) {
128
213
  let response
129
214
  try {
130
- response = await this.#client.request('POST', `${API_BASE_PATH}/${projectId}/datasets`, {
131
- data: { type: 'datasets', attributes: { name: this.#name, description: this.#description } },
132
- })
215
+ response = await this.#client.createDataset(projectId, { name: this.#name, description: this.#description })
133
216
  } catch (err) {
134
217
  throw new Error(`Failed to create dataset '${this.#name}': ${err.message}`)
135
218
  }
136
- this.#id = response?.data?.id ?? null
219
+ this.#id = response?.id() ?? null
137
220
  if (this.#id === null) {
138
221
  throw new Error(`Failed to create dataset '${this.#name}': backend response is missing dataset id`)
139
222
  }
140
223
  this.#projectId = projectId
141
- this.#version = response?.data?.attributes?.current_version ?? this.#version
142
- this.#latestVersion = response?.data?.attributes?.current_version ?? this.#latestVersion
224
+ this.#version = response.version() ?? this.#version
225
+ this.#latestVersion = response.latestVersion() ?? this.#latestVersion
143
226
  }
144
227
 
145
- if (this.#pushedCount >= this.#records.length) return { pushedCount: 0, totalCount: 0 }
228
+ const pending = this.#pendingBatch()
229
+ if (pending.totalCount === 0) return { pushedCount: 0, totalCount: 0 }
230
+
231
+ let result
232
+ try {
233
+ result = await this.#client.batchUpdateDatasetRecords(projectId, this.#id, pending.attributes)
234
+ } catch (err) {
235
+ throw new Error(`Failed to push changes to dataset '${this.#name}': ${err.message}`)
236
+ }
146
237
 
147
- const pending = this.#records.slice(this.#pushedCount)
148
- const records = pending.map((rec) => {
149
- const out = { input: rec.input }
150
- if (rec.id != null) {
151
- out.id = rec.id
238
+ this.#updateVersionFromMutationResult(result)
239
+ this.#clearCommittedChanges(pending)
240
+ return { pushedCount: pending.totalCount, totalCount: pending.totalCount }
241
+ }
242
+
243
+ #pendingBatch () {
244
+ const insertRecords = []
245
+ const insertPayloads = new Map()
246
+ for (const [recordId, record] of this.#newRecordsById) {
247
+ const payload = serializedRecord(record)
248
+ insertRecords.push(payload)
249
+ insertPayloads.set(recordId, payload)
250
+ }
251
+
252
+ const updateRecords = []
253
+ const updatePayloads = new Map()
254
+ for (const [recordId, update] of this.#updatedRecordsById) {
255
+ const payload = serializedRecordUpdate(update)
256
+ updateRecords.push(payload)
257
+ updatePayloads.set(recordId, payload)
258
+ }
259
+
260
+ const deleteRecordIds = [...this.#deletedRecordIds]
261
+ return {
262
+ attributes: {
263
+ insert_records: insertRecords,
264
+ update_records: updateRecords,
265
+ delete_records: deleteRecordIds,
266
+ deduplicate: true,
267
+ create_new_version: true,
268
+ },
269
+ deleteRecordIds,
270
+ insertPayloads,
271
+ updatePayloads,
272
+ totalCount: new Set([
273
+ ...insertPayloads.keys(),
274
+ ...updatePayloads.keys(),
275
+ ...deleteRecordIds,
276
+ ]).size,
277
+ }
278
+ }
279
+
280
+ #clearCommittedChanges (pending) {
281
+ for (const [recordId, payload] of pending.insertPayloads) {
282
+ const current = this.#newRecordsById.get(recordId)
283
+ if (!current) {
284
+ this.#deletedRecordIds.add(recordId)
285
+ continue
152
286
  }
153
- if (rec.expectedOutput !== null && rec.expectedOutput !== undefined) {
154
- out.expected_output = rec.expectedOutput
287
+ if (valuesAreEqual(serializedRecord(current), payload)) {
288
+ this.#newRecordsById.delete(recordId)
289
+ continue
155
290
  }
156
- if (rec.metadata && Object.keys(rec.metadata).length > 0) {
157
- out.metadata = rec.metadata
291
+
292
+ this.#newRecordsById.delete(recordId)
293
+ const update = this.#updatedRecordsById.get(recordId) ??
294
+ updateFromInsertedRecord(recordId, current, payload)
295
+ this.#updatedRecordsById.set(recordId, update)
296
+ }
297
+
298
+ for (const [recordId, payload] of pending.updatePayloads) {
299
+ const current = this.#updatedRecordsById.get(recordId)
300
+ if (current && valuesAreEqual(serializedRecordUpdate(current), payload)) {
301
+ this.#updatedRecordsById.delete(recordId)
158
302
  }
159
- return out
160
- })
303
+ }
161
304
 
162
- let response
163
- try {
164
- response = await this.#client.request(
165
- 'POST',
166
- `${API_BASE_PATH}/${projectId}/datasets/${this.#id}/records`,
167
- { data: { type: 'datasets', attributes: { records } } }
168
- )
169
- } catch (err) {
170
- throw new Error(`Failed to push records to dataset '${this.#name}': ${err.message}`)
305
+ for (const recordId of pending.deleteRecordIds) {
306
+ if (!this.#recordsById.has(recordId)) this.#deletedRecordIds.delete(recordId)
171
307
  }
308
+ }
172
309
 
173
- // The append-records response has used both a top-level `records` array
174
- // and JSON:API `data` resources. Accept either so generated/custom record
175
- // ids are preserved for experiment row tagging.
176
- const created = createdRecordsFromResponse(response)
177
- const pushedVersion = versionFromCreatedRecords(created)
178
- if (pushedVersion === null) {
179
- // The dataset contents changed, but the backend did not report the new
180
- // version. Avoid pinning later experiments to the pre-append create version.
181
- this.#version = null
182
- } else {
310
+ #updateVersionFromMutationResult (result) {
311
+ const pushedVersion = versionFromMutationResult(result)
312
+ if (pushedVersion !== null) {
183
313
  this.#version = pushedVersion
184
- this.#latestVersion = Math.max(Number(this.#latestVersion ?? pushedVersion), pushedVersion)
314
+ this.#latestVersion = pushedVersion
315
+ return
185
316
  }
186
317
 
187
- let pushedCount = 0
188
- for (const [index, node] of created.entries()) {
189
- const recordId = recordIdFromCreatedRecord(node)
190
- if (recordId !== '') {
191
- pushedCount++
192
- pending[index].id = recordId
193
- }
194
- this.#recordIds.push(recordId)
318
+ const latestVersion = Number(this.#latestVersion)
319
+ if (Number.isFinite(latestVersion)) {
320
+ this.#version = latestVersion + 1
321
+ this.#latestVersion = this.#version
322
+ } else {
323
+ this.#version = null
195
324
  }
196
- for (let i = created.length; i < pending.length; i++) this.#recordIds.push('')
325
+ }
326
+
327
+ #addRecord (record) {
328
+ if (this.#recordsById.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
329
+ this.#records.push(record)
330
+ this.#recordsById.set(record.id, record)
331
+ this.#deletedRecordIds.delete(record.id)
332
+ this.#newRecordsById.set(record.id, record)
333
+ }
197
334
 
198
- // Advance by the snapshotted pending count, not the live records length,
199
- // so records added while this push was in flight aren't skipped by the next push.
200
- this.#pushedCount += pending.length
335
+ #addExistingRecord (record) {
336
+ if (record.id === null || record.id === undefined || record.id === '') {
337
+ throw new Error('Dataset records pulled from the backend must have an id')
338
+ }
339
+ if (!(record instanceof DatasetRecord)) {
340
+ record = new DatasetRecord(
341
+ record.input,
342
+ record.expectedOutput,
343
+ record.metadata,
344
+ record.id
345
+ )
346
+ }
347
+ if (this.#recordsById.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
348
+ this.#records.push(record)
349
+ this.#recordsById.set(record.id, record)
350
+ }
201
351
 
202
- return { pushedCount, totalCount: pending.length }
352
+ #recordAt (index) {
353
+ if (!Number.isInteger(index) || index < 0 || index >= this.#records.length) {
354
+ throw new RangeError(`Dataset record index ${index} is out of range`)
355
+ }
356
+ return this.#records[index]
203
357
  }
204
358
  }
205
359