dd-trace 6.12.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 (97) hide show
  1. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  2. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  3. package/index.d.ts +127 -26
  4. package/package.json +10 -10
  5. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  6. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  7. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  10. package/packages/datadog-instrumentations/src/jest.js +140 -18
  11. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  12. package/packages/datadog-instrumentations/src/mariadb.js +7 -0
  13. package/packages/datadog-instrumentations/src/pino.js +15 -4
  14. package/packages/datadog-instrumentations/src/playwright.js +34 -20
  15. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -4
  16. package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
  17. package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
  18. package/packages/datadog-instrumentations/src/winston.js +6 -3
  19. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  20. package/packages/datadog-plugin-http/src/client.js +2 -15
  21. package/packages/datadog-plugin-http2/src/client.js +2 -15
  22. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  23. package/packages/datadog-plugin-playwright/src/index.js +9 -1
  24. package/packages/datadog-plugin-undici/src/index.js +2 -15
  25. package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
  26. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  27. package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
  28. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  29. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  30. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  31. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  32. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  33. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  34. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
  35. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
  36. package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
  37. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
  38. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
  39. package/packages/dd-trace/src/config/index.js +2 -1
  40. package/packages/dd-trace/src/config/supported-configurations.json +27 -0
  41. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
  42. package/packages/dd-trace/src/dogstatsd.js +45 -18
  43. package/packages/dd-trace/src/encode/agentless-json.js +5 -4
  44. package/packages/dd-trace/src/exporters/agent/index.js +22 -2
  45. package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
  46. package/packages/dd-trace/src/exporters/common/writer.js +24 -2
  47. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  48. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  49. package/packages/dd-trace/src/flush.js +102 -0
  50. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  51. package/packages/dd-trace/src/llmobs/experiments/dataset.js +51 -2
  52. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  53. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  54. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  55. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  56. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  57. package/packages/dd-trace/src/llmobs/index.js +80 -17
  58. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  59. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  60. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  61. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  62. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  63. package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
  64. package/packages/dd-trace/src/noop/span.js +5 -0
  65. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  66. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  67. package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
  68. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  69. package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
  70. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
  72. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
  73. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  74. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  75. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  76. package/packages/dd-trace/src/opentracing/propagation/text_map.js +410 -342
  77. package/packages/dd-trace/src/opentracing/span.js +28 -0
  78. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  79. package/packages/dd-trace/src/plugin_manager.js +2 -0
  80. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  81. package/packages/dd-trace/src/plugins/util/test.js +74 -21
  82. package/packages/dd-trace/src/plugins/util/web.js +2 -65
  83. package/packages/dd-trace/src/profiling/oom.js +4 -3
  84. package/packages/dd-trace/src/profiling/profiler.js +7 -0
  85. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  86. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  87. package/packages/dd-trace/src/proxy.js +36 -8
  88. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  89. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
  90. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
  91. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
  92. package/packages/dd-trace/src/serverless/vercel.js +122 -0
  93. package/packages/dd-trace/src/serverless.js +37 -20
  94. package/packages/dd-trace/src/span_stats.js +40 -3
  95. package/packages/dd-trace/src/tracer.js +22 -0
  96. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  97. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
@@ -8,6 +8,8 @@ const request = require('./exporters/common/request')
8
8
  const log = require('./log')
9
9
  const Histogram = require('./histogram')
10
10
  const { entityId } = require('./exporters/common/docker')
11
+ const { registerTelemetryFlusher } = require('./flush')
12
+ const { createServerlessDeliveryTracker } = require('./serverless')
11
13
 
12
14
  const legacyStorage = storage('legacy')
13
15
 
@@ -25,6 +27,8 @@ const TYPE_HISTOGRAM = 'h'
25
27
  class DogStatsDClient {
26
28
  #lookup
27
29
  #tagsPrefix
30
+ #serverlessDeliveryTracker
31
+
28
32
  constructor (options) {
29
33
  this.#lookup = options.lookup
30
34
  if (options.metricsProxyUrl) {
@@ -41,6 +45,7 @@ class DogStatsDClient {
41
45
  this._tags = options.tags
42
46
  this.#tagsPrefix = this._tags?.length ? `|#${this._tags.join(',')}` : ''
43
47
  this._queue = []
48
+ this.#serverlessDeliveryTracker = createServerlessDeliveryTracker()
44
49
  this._buffer = ''
45
50
  this._offset = 0
46
51
  this._udp4 = this._socket('udp4')
@@ -67,23 +72,30 @@ class DogStatsDClient {
67
72
  this._add(stat, value, TYPE_HISTOGRAM, tags)
68
73
  }
69
74
 
70
- flush () {
75
+ flush (done) {
71
76
  const queue = this._enqueue()
72
77
 
73
- if (queue.length === 0) return
78
+ if (queue.length === 0) {
79
+ if (this.#serverlessDeliveryTracker) return this.#serverlessDeliveryTracker.waitForIdle(done)
80
+ return done?.()
81
+ }
74
82
 
75
83
  log.debug('Flushing %s metrics via %s', queue.length, this._httpOptions ? 'HTTP' : 'UDP')
76
84
 
77
85
  this._queue = []
78
86
 
79
- if (this._httpOptions) {
80
- this._sendHttp(queue)
81
- } else {
82
- this._sendUdp(queue)
87
+ const send = complete => {
88
+ if (this._httpOptions) this._sendHttp(queue, complete)
89
+ else this._sendUdp(queue, complete)
90
+ }
91
+ if (this.#serverlessDeliveryTracker) {
92
+ this.#serverlessDeliveryTracker.track(send)
93
+ return this.#serverlessDeliveryTracker.waitForIdle(done)
83
94
  }
95
+ send(done)
84
96
  }
85
97
 
86
- _sendHttp (queue) {
98
+ _sendHttp (queue, done) {
87
99
  const buffer = Buffer.concat(queue)
88
100
  request(buffer, this._httpOptions, (err) => {
89
101
  if (err) {
@@ -95,32 +107,46 @@ class DogStatsDClient {
95
107
  // options. Either way, we can give UDP a try.
96
108
  this._httpOptions = undefined
97
109
  }
98
- this._sendUdp(queue)
110
+ this._sendUdp(queue, done)
111
+ } else {
112
+ done?.()
99
113
  }
100
114
  })
101
115
  }
102
116
 
103
- _sendUdp (queue) {
117
+ _sendUdp (queue, done) {
104
118
  // dgram resolves the local address via the instrumented dns.lookup when it
105
119
  // binds on first send; the noop store keeps that self-traffic off the trace.
106
120
  legacyStorage.run({ noop: true }, () => {
107
121
  if (this._family === 0) {
108
122
  this.#lookup(this._host, (error, address, family) => {
109
- if (error) return log.error('DogStatsDClient: Host not found', error)
110
- this._sendUdpFromQueue(queue, address, family)
123
+ if (error) {
124
+ log.error('DogStatsDClient: Host not found', error)
125
+ return done?.()
126
+ }
127
+ this._sendUdpFromQueue(queue, address, family, done)
111
128
  })
112
129
  } else {
113
- this._sendUdpFromQueue(queue, this._host, this._family)
130
+ this._sendUdpFromQueue(queue, this._host, this._family, done)
114
131
  }
115
132
  })
116
133
  }
117
134
 
118
- _sendUdpFromQueue (queue, address, family) {
135
+ _sendUdpFromQueue (queue, address, family, done) {
119
136
  const socket = family === 6 ? this._udp6 : this._udp4
137
+ let pending = queue.length
138
+ const complete = () => {
139
+ if (--pending === 0) done?.()
140
+ }
120
141
 
121
142
  for (const buffer of queue) {
122
143
  log.debug('Sending to DogStatsD: %s', buffer)
123
- socket.send(buffer, 0, buffer.length, this._port, address)
144
+ try {
145
+ socket.send(buffer, 0, buffer.length, this._port, address, complete)
146
+ } catch (error) {
147
+ log.error('DogStatsDClient: UDP error sending metrics', error)
148
+ complete()
149
+ }
124
150
  }
125
151
  }
126
152
 
@@ -212,12 +238,12 @@ class MetricsAggregationClient {
212
238
  this.reset()
213
239
  }
214
240
 
215
- flush () {
241
+ flush (done) {
216
242
  this._captureCounters()
217
243
  this._captureGauges()
218
244
  this._captureHistograms()
219
245
 
220
- this._client.flush()
246
+ this._client.flush(done)
221
247
  }
222
248
 
223
249
  reset () {
@@ -370,6 +396,7 @@ class CustomMetrics {
370
396
  setInterval(flush, 10 * 1000).unref?.()
371
397
 
372
398
  globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(flush)
399
+ registerTelemetryFlusher(done => this.flush(done))
373
400
  }
374
401
 
375
402
  increment (stat, value = 1, tags) {
@@ -392,8 +419,8 @@ class CustomMetrics {
392
419
  this.#client.histogram(stat, value, CustomMetrics.tagTranslator(tags))
393
420
  }
394
421
 
395
- flush () {
396
- return this.#client.flush()
422
+ flush (done) {
423
+ return this.#client.flush(done)
397
424
  }
398
425
 
399
426
  /**
@@ -167,17 +167,18 @@ class AgentlessJSONEncoder {
167
167
  const metadataPrefix = metadataJson.slice(0, -1)
168
168
  const hasMetadata = metadataPrefix.length > 1 // more than just '{'
169
169
 
170
- const traceParts = []
170
+ let tracesJson = ''
171
171
  for (const spanStrings of this._traces) {
172
172
  const spansJson = '[' + spanStrings.join(',') + ']'
173
+ if (tracesJson) tracesJson += ','
173
174
  if (hasMetadata) {
174
- traceParts.push(metadataPrefix + ',"spans":' + spansJson + '}')
175
+ tracesJson += metadataPrefix + ',"spans":' + spansJson + '}'
175
176
  } else {
176
- traceParts.push('{"spans":' + spansJson + '}')
177
+ tracesJson += '{"spans":' + spansJson + '}'
177
178
  }
178
179
  }
179
180
 
180
- const payload = '{"traces":[' + traceParts.join(',') + ']}'
181
+ const payload = '{"traces":[' + tracesJson + ']}'
181
182
  this._reset()
182
183
  return Buffer.from(payload, 'utf8')
183
184
  } catch (err) {
@@ -2,12 +2,15 @@
2
2
 
3
3
  const { URL } = require('url')
4
4
  const log = require('../../log')
5
+ const { createServerlessDeliveryTracker } = require('../../serverless')
5
6
  const Writer = require('./writer')
6
7
 
7
8
  class AgentExporter {
8
9
  #timer
10
+ #serverlessDeliveryTracker
9
11
 
10
12
  constructor (config, prioritySampler) {
13
+ this.#serverlessDeliveryTracker = createServerlessDeliveryTracker()
11
14
  this._config = config
12
15
  const { lookup, protocolVersion, stats = {}, apmTracingEnabled } = config
13
16
  this._url = config.url
@@ -23,6 +26,7 @@ class AgentExporter {
23
26
  lookup,
24
27
  protocolVersion,
25
28
  headers,
29
+ deliveryTracker: this.#serverlessDeliveryTracker,
26
30
  })
27
31
 
28
32
  globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(this.flush.bind(this))
@@ -54,10 +58,26 @@ class AgentExporter {
54
58
  }
55
59
  }
56
60
 
57
- flush (done = () => {}) {
61
+ flush (done) {
58
62
  clearTimeout(this.#timer)
59
63
  this.#timer = undefined
60
- this._writer.flush(done)
64
+
65
+ if (!this.#serverlessDeliveryTracker) {
66
+ try {
67
+ return this._writer.flush(done)
68
+ } catch (error) {
69
+ log.error('Failed to flush traces: %s', error.message)
70
+ done?.()
71
+ return
72
+ }
73
+ }
74
+
75
+ try {
76
+ this._writer.flush()
77
+ } catch (error) {
78
+ log.error('Failed to flush traces: %s', error.message)
79
+ }
80
+ this.#serverlessDeliveryTracker.waitForIdle(done)
61
81
  }
62
82
  }
63
83
 
@@ -39,18 +39,18 @@ class AgentWriter extends BaseWriter {
39
39
  }
40
40
 
41
41
  /**
42
- * Flushes payloads, including requests already in flight during Test Optimization finalization.
43
- *
42
+ * Performs the writer flush without registering a serverless delivery.
43
+ * Test Optimization owns its own request lifecycle tracking.
44
44
  * @param {(error?: Error) => void} [done]
45
45
  * @param {{ deadline?: number }} [options]
46
46
  * @returns {void}
47
47
  */
48
- flush (done, options) {
48
+ flushDirect (done, options) {
49
49
  if (this.#requestTracker) {
50
50
  this.#requestTracker.flush(done, options)
51
51
  return
52
52
  }
53
- super.flush(done, options)
53
+ super.flushDirect(done, options)
54
54
  }
55
55
 
56
56
  _sendPayload (data, count, done, flushOptions) {
@@ -10,14 +10,36 @@ const { safeJSONStringify } = require('./util')
10
10
  const firstFlushChannel = channel('dd-trace:exporter:first-flush')
11
11
 
12
12
  class Writer {
13
- constructor ({ url, beforeFirstFlush }) {
13
+ #deliveryTracker
14
+
15
+ constructor ({ url, beforeFirstFlush, deliveryTracker }) {
14
16
  this._url = url
15
17
  this._beforeFirstFlush = beforeFirstFlush
18
+ this.#deliveryTracker = deliveryTracker
16
19
  }
17
20
 
18
21
  #isFirstFlush = true
19
22
 
20
- flush (done = () => {}, options) {
23
+ /**
24
+ * Flushes queued telemetry, retaining delivery on supported serverless platforms.
25
+ * @param {(error?: Error) => void} [done]
26
+ * @param {{ deadline?: number }} [options]
27
+ * @returns {void}
28
+ */
29
+ flush (done, options) {
30
+ if (this.#deliveryTracker) {
31
+ return this.#deliveryTracker.track(callback => this.flushDirect(callback, options), done)
32
+ }
33
+ this.flushDirect(done, options)
34
+ }
35
+
36
+ /**
37
+ * Flushes queued telemetry without registering serverless delivery retention.
38
+ * @param {(error?: Error) => void} [done]
39
+ * @param {{ deadline?: number }} [options]
40
+ * @returns {void}
41
+ */
42
+ flushDirect (done = () => {}, options) {
21
43
  const count = this._encoder.count()
22
44
 
23
45
  if (!request.writable && options?.deadline === undefined) {
@@ -1,16 +1,35 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../../log')
4
+ const { createServerlessDeliveryTracker } = require('../../serverless')
3
5
  const { Writer } = require('./writer')
4
6
 
5
7
  class SpanStatsExporter {
8
+ #serverlessDeliveryTracker
9
+
6
10
  constructor (config) {
11
+ this.#serverlessDeliveryTracker = createServerlessDeliveryTracker()
7
12
  this._url = config.url
8
- this._writer = new Writer({ url: this._url })
13
+ this._writer = new Writer({
14
+ url: this._url,
15
+ deliveryTracker: this.#serverlessDeliveryTracker,
16
+ })
17
+ }
18
+
19
+ export (payload, done) {
20
+ try {
21
+ this._writer.append(payload)
22
+ this._writer.flush(this.#serverlessDeliveryTracker ? undefined : done)
23
+ } catch (error) {
24
+ if (!done) throw error
25
+ log.error('Failed to flush span stats: %s', error.message)
26
+ }
27
+ this.#serverlessDeliveryTracker?.waitForIdle(done)
9
28
  }
10
29
 
11
- export (payload) {
12
- this._writer.append(payload)
13
- this._writer.flush()
30
+ flush (done) {
31
+ this._writer.flush(this.#serverlessDeliveryTracker ? undefined : done)
32
+ this.#serverlessDeliveryTracker?.waitForIdle(done)
14
33
  }
15
34
  }
16
35
 
@@ -33,11 +33,12 @@ class ExternalLogger {
33
33
  }
34
34
 
35
35
  static tagString (tags) {
36
- const tagArray = []
36
+ let tagString = ''
37
37
  for (const key in tags) {
38
- tagArray.push(key + ':' + tags[key])
38
+ if (tagString) tagString += ','
39
+ tagString += key + ':' + tags[key]
39
40
  }
40
- return tagArray.join(',')
41
+ return tagString
41
42
  }
42
43
 
43
44
  // Parses and enqueues a log
@@ -0,0 +1,102 @@
1
+ 'use strict'
2
+
3
+ const log = require('./log')
4
+ const { supportsServerlessTelemetryRetention } = require('./serverless')
5
+
6
+ /**
7
+ * @typedef {(done: () => void) => void} TelemetryFlusher
8
+ */
9
+
10
+ /** @type {Set<TelemetryFlusher>} */
11
+ const telemetryFlushers = new Set()
12
+ const postTraceTelemetryFlushers = new Set()
13
+
14
+ /**
15
+ * @typedef {{
16
+ * trace?: TelemetryFlusher,
17
+ * spanStats?: TelemetryFlusher
18
+ * }} TraceFlushers
19
+ */
20
+
21
+ /**
22
+ * Registers a configured telemetry pipeline so serverless lifecycle retention
23
+ * waits for its final export alongside trace delivery.
24
+ * @param {TelemetryFlusher} flusher
25
+ * @param {{ afterTrace?: boolean }} [options]
26
+ * @returns {() => void} Removes this pipeline when its provider is replaced.
27
+ */
28
+ function registerTelemetryFlusher (flusher, options) {
29
+ if (!supportsServerlessTelemetryRetention()) return () => {}
30
+
31
+ const flushers = options?.afterTrace ? postTraceTelemetryFlushers : telemetryFlushers
32
+ flushers.add(flusher)
33
+ // Avoid retaining a replaced provider or flushing it alongside the new one.
34
+ return () => flushers.delete(flusher)
35
+ }
36
+
37
+ /**
38
+ * Coordinates the configured telemetry flushers for a serverless lifecycle.
39
+ *
40
+ * Trace-owned flushers are supplied by DatadogTracer so this module does not
41
+ * depend on its private implementation details.
42
+ * @param {() => void} [done]
43
+ * @param {{ timeout?: number }} [options]
44
+ * @param {TraceFlushers} [traceFlushers]
45
+ */
46
+ function flushServerlessTelemetry (done, options, traceFlushers = {}) {
47
+ const { trace: traceFlusher, spanStats: spanStatsFlusher } = traceFlushers
48
+ // TODO: Include DSM after DataStreamsProcessor exposes a completion-aware flush API.
49
+ let pending = telemetryFlushers.size + postTraceTelemetryFlushers.size +
50
+ (typeof traceFlusher === 'function' ? 1 : 0) +
51
+ (typeof spanStatsFlusher === 'function' ? 1 : 0)
52
+ let completed = false
53
+ let timeout
54
+
55
+ const finish = () => {
56
+ if (completed) return
57
+ completed = true
58
+ clearTimeout(timeout)
59
+ done?.()
60
+ }
61
+ const complete = () => {
62
+ if (--pending === 0) finish()
63
+ }
64
+
65
+ if (pending === 0) return finish()
66
+ if (options?.timeout) {
67
+ timeout = setTimeout(() => {
68
+ log.warn('Timed out waiting for telemetry flush after %dms', options.timeout)
69
+ finish()
70
+ }, options.timeout)
71
+ }
72
+
73
+ const flush = (flusher, afterFlushed) => {
74
+ let flushed = false
75
+ const onFlushed = error => {
76
+ if (flushed) return
77
+ flushed = true
78
+ if (error) log.error('Error flushing telemetry pipeline:', error)
79
+ afterFlushed?.()
80
+ complete()
81
+ }
82
+ try {
83
+ flusher(onFlushed)
84
+ } catch (error) {
85
+ onFlushed(error)
86
+ }
87
+ }
88
+
89
+ if (typeof traceFlusher === 'function') {
90
+ flush(traceFlusher, () => {
91
+ for (const flusher of postTraceTelemetryFlushers) flush(flusher)
92
+ })
93
+ } else {
94
+ for (const flusher of postTraceTelemetryFlushers) flush(flusher)
95
+ }
96
+ if (typeof spanStatsFlusher === 'function') {
97
+ flush(spanStatsFlusher)
98
+ }
99
+ for (const flusher of telemetryFlushers) flush(flusher)
100
+ }
101
+
102
+ module.exports = { flushServerlessTelemetry, registerTelemetryFlusher }
@@ -108,6 +108,10 @@ class ExperimentsClient {
108
108
  return this.#site
109
109
  }
110
110
 
111
+ get projectName () {
112
+ return this.#projectName
113
+ }
114
+
111
115
  // Dashboard URL base for the configured site, e.g. https://app.datadoghq.com
112
116
  get appBase () {
113
117
  return `https://${appHost(this.#site)}`
@@ -1,8 +1,17 @@
1
1
  'use strict'
2
2
 
3
3
  const { randomUUID } = require('node:crypto')
4
+ const snapshotPayload = require('../../../../../vendor/dist/rfdc')({ proto: false, circles: false })
4
5
 
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
+ */
6
15
  /**
7
16
  * @typedef {object} PendingBatch
8
17
  * @property {object} attributes
@@ -114,6 +123,9 @@ function updateFromInsertedRecord (recordId, record, payload) {
114
123
  update.expectedOutput = record.expectedOutput
115
124
  }
116
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
+ }
117
129
  return update
118
130
  }
119
131
 
@@ -173,6 +185,36 @@ class Dataset {
173
185
  return this
174
186
  }
175
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
+
176
218
  /**
177
219
  * Add tags to a dataset record.
178
220
  * @param {number} index Dataset record index.
@@ -336,6 +378,10 @@ class Dataset {
336
378
  return this.#projectId
337
379
  }
338
380
 
381
+ projectName () {
382
+ return this.#client.projectName
383
+ }
384
+
339
385
  version () {
340
386
  return this.#version
341
387
  }
@@ -416,7 +462,7 @@ class Dataset {
416
462
  const insertRecords = []
417
463
  const insertPayloads = new Map()
418
464
  for (const [recordId, record] of this.#newRecordsById) {
419
- const payload = serializedRecord(record)
465
+ const payload = snapshotPayload(serializedRecord(record))
420
466
  insertRecords.push(payload)
421
467
  insertPayloads.set(recordId, payload)
422
468
  }
@@ -427,7 +473,7 @@ class Dataset {
427
473
  const tagOperations = this.#pendingTagOperations.get(recordId)
428
474
  if (tagOperations) update.tagOperations = tagOperations
429
475
  else delete update.tagOperations
430
- const payload = serializedRecordUpdate(update)
476
+ const payload = snapshotPayload(serializedRecordUpdate(update))
431
477
  updateRecords.push(payload)
432
478
  updatePayloads.set(recordId, payload)
433
479
  }
@@ -522,6 +568,9 @@ class Dataset {
522
568
  updateFromInsertedRecord(recordId, current, payload)
523
569
  const queuedOperations = this.#pendingTagOperations.get(recordId)
524
570
  if (queuedOperations) update.tagOperations = queuedOperations
571
+ if (update.tagOperations) {
572
+ this.#pendingTagOperations.set(recordId, copyTagOperations(update.tagOperations))
573
+ }
525
574
  this.#updatedRecordsById.set(recordId, update)
526
575
  }
527
576