dd-trace 6.19.0 → 6.20.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 (77) hide show
  1. package/index.d.ts +163 -22
  2. package/package.json +2 -2
  3. package/packages/datadog-esbuild/index.js +9 -6
  4. package/packages/datadog-instrumentations/src/ai.js +26 -0
  5. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  9. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  10. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  11. package/packages/dd-trace/src/config/remote_config.js +12 -0
  12. package/packages/dd-trace/src/config/supported-configurations.json +21 -0
  13. package/packages/dd-trace/src/debugger/constants.js +13 -4
  14. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  15. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  16. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  17. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  18. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  19. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  20. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  21. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  22. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  23. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  24. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  25. package/packages/dd-trace/src/debugger/index.js +60 -28
  26. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  27. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  28. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  29. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  30. package/packages/dd-trace/src/knuth-hash.js +17 -0
  31. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  32. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  33. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  34. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  35. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  36. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  37. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  38. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  39. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +2 -2
  40. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  41. package/packages/dd-trace/src/llmobs/span_processor.js +42 -26
  42. package/packages/dd-trace/src/llmobs/tagger.js +35 -0
  43. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  44. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  45. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  46. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  47. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  48. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  49. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  50. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  51. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  52. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  53. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  54. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  55. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  56. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  57. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  58. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  59. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  60. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  61. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  62. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  63. package/packages/dd-trace/src/opentracing/span.js +14 -9
  64. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  65. package/packages/dd-trace/src/otel-sampling.js +171 -0
  66. package/packages/dd-trace/src/plugins/index.js +0 -1
  67. package/packages/dd-trace/src/priority_sampler.js +155 -33
  68. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  69. package/packages/dd-trace/src/sampler.js +2 -5
  70. package/packages/dd-trace/src/sampling_rule.js +4 -8
  71. package/packages/dd-trace/src/span_format.js +6 -0
  72. package/packages/dd-trace/src/span_processor.js +26 -2
  73. package/packages/dd-trace/src/standalone/index.js +5 -4
  74. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  75. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  76. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  77. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -0,0 +1,105 @@
1
+ 'use strict'
2
+
3
+ const { MAX_EVALUATION_TIMESTAMP_MS } = require('../constants/constants')
4
+ const { snapshotEvaluationContext } = require('./flag-evaluation-context')
5
+ const {
6
+ recordContextTruncated, recordDropped, recordHookError, recordTargetingKeyOmitted,
7
+ } = require('./flag-evaluation-telemetry')
8
+ const FlagEvaluationsWriter = require('./flag-evaluations')
9
+ const { setExposureDeliveryStrategy } = require('./util')
10
+
11
+ /** Captures terminal SDK results; the writer owns deferred aggregation and delivery. */
12
+ class FlagEvalEVPHook {
13
+ /** @type {FlagEvaluationsWriter} */
14
+ #writer
15
+ #closed = false
16
+ #stopDeliveryStrategy
17
+
18
+ /**
19
+ * The provider only constructs this hook when evaluation counts are enabled.
20
+ *
21
+ * @param {import('../../config/config-base')} config
22
+ */
23
+ constructor (config) {
24
+ const writer = new FlagEvaluationsWriter(config)
25
+ this.#writer = writer
26
+ this.#stopDeliveryStrategy = setExposureDeliveryStrategy(config, (enabled, route) => {
27
+ if (this.#closed) return
28
+ writer.setEnabled(enabled, route)
29
+ })
30
+ }
31
+
32
+ /**
33
+ * Provider hooks run in finally even when the SDK short-circuits before resolution.
34
+ * Consent belongs to the captured result, never the provider's current configuration.
35
+ *
36
+ * @param {import('@openfeature/core').HookContext} hookContext
37
+ * @param {import('@openfeature/core').EvaluationDetails<import('@openfeature/core').FlagValue>} evaluationDetails
38
+ */
39
+ finally (hookContext, evaluationDetails) {
40
+ try {
41
+ const unavailableReason = this.#closed ? 'closed' : this.#writer.getUnavailableReason()
42
+ if (unavailableReason !== undefined) {
43
+ recordDropped(unavailableReason)
44
+ return
45
+ }
46
+ if (!this.#writer.hasCapacity()) {
47
+ recordDropped('pre_queue_overflow')
48
+ return
49
+ }
50
+
51
+ const metadata = evaluationDetails.flagMetadata
52
+ const consent = metadata?.__dd_observe_full_evaluation_data === true
53
+ const capturedTime = metadata?.__dd_eval_timestamp_ms
54
+ const timestamp = typeof capturedTime === 'number' && Number.isSafeInteger(capturedTime) &&
55
+ Math.abs(capturedTime) <= MAX_EVALUATION_TIMESTAMP_MS
56
+ ? capturedTime
57
+ : Date.now()
58
+ const context = hookContext.context
59
+ let targetingKey
60
+ try {
61
+ targetingKey = context?.targetingKey
62
+ } catch {
63
+ // An unreadable identity must not discard an otherwise valid evaluation count.
64
+ recordTargetingKeyOmitted()
65
+ }
66
+ let attrs
67
+ if (consent) {
68
+ let snapshot
69
+ try {
70
+ snapshot = snapshotEvaluationContext(context)
71
+ } catch {
72
+ // Preserve the evaluation count without the failed context. Never log caller-controlled errors.
73
+ recordContextTruncated('snapshot_error')
74
+ }
75
+ if (snapshot !== undefined) {
76
+ attrs = snapshot.attrs
77
+ for (const reason of snapshot.reasons) recordContextTruncated(reason)
78
+ }
79
+ }
80
+
81
+ this.#writer.enqueue({
82
+ flagKey: hookContext.flagKey,
83
+ variant: evaluationDetails.variant,
84
+ allocationKey: typeof metadata?.__dd_allocation_key === 'string' ? metadata.__dd_allocation_key : undefined,
85
+ runtimeDefault: evaluationDetails.variant === undefined,
86
+ errorCode: evaluationDetails.errorCode,
87
+ targetingKey,
88
+ attrs,
89
+ observeFullEvaluationData: consent,
90
+ timestamp,
91
+ })
92
+ } catch {
93
+ recordHookError()
94
+ }
95
+ }
96
+
97
+ destroy () {
98
+ if (this.#closed) return
99
+ this.#closed = true
100
+ this.#stopDeliveryStrategy?.()
101
+ this.#writer.destroy()
102
+ }
103
+ }
104
+
105
+ module.exports = FlagEvalEVPHook
@@ -0,0 +1,190 @@
1
+ 'use strict'
2
+
3
+ const { createHash } = require('node:crypto')
4
+
5
+ const {
6
+ FLAG_EVALUATION_DEGRADED_CAP,
7
+ FLAG_EVALUATION_GLOBAL_CAP,
8
+ FLAG_EVALUATION_PER_FLAG_CAP,
9
+ } = require('../constants/constants')
10
+ const { validatedContextEntries, snapshotFromEntries } = require('./flag-evaluation-context')
11
+ const { normalizeTargetingKey, optionalKey, protectedErrorCode } =
12
+ require('./flag-evaluation-pii')
13
+ const { recordDegraded, recordDropped } = require('./flag-evaluation-telemetry')
14
+
15
+ // V8 hashes longer strings by length alone, making equal-length identities expensive to distinguish.
16
+ const MAX_DIRECT_IDENTITY_LENGTH = 16_383
17
+
18
+ /** @typedef {import('./flag-evaluation-context').ContextSnapshot} ContextSnapshot */
19
+ /**
20
+ * @typedef {object} FlagEvaluationEvent
21
+ * @property {string} flagKey
22
+ * @property {string} [variant]
23
+ * @property {string} [allocationKey]
24
+ * @property {boolean} runtimeDefault
25
+ * @property {unknown} [errorCode]
26
+ * @property {unknown} [targetingKey]
27
+ * @property {ContextSnapshot} [attrs]
28
+ * @property {unknown} [observeFullEvaluationData]
29
+ * @property {number} timestamp
30
+ */
31
+ /**
32
+ * @typedef {object} AggregationDimensions
33
+ * @property {string} flagKey
34
+ * @property {string} [variant]
35
+ * @property {string} [allocation]
36
+ * @property {string} [error]
37
+ */
38
+ /**
39
+ * @typedef {AggregationDimensions & {
40
+ * runtimeDefault: boolean,
41
+ * rawTargetingKey?: string,
42
+ * attrs?: ContextSnapshot,
43
+ * consent: boolean,
44
+ * count: number,
45
+ * first: number,
46
+ * last: number
47
+ * }} AggregationEntry
48
+ */
49
+ /**
50
+ * @typedef {object} AggregationResult
51
+ * @property {Map<string, AggregationEntry>} full
52
+ * @property {Map<string, AggregationEntry>} degraded
53
+ */
54
+
55
+ /**
56
+ * Merge one observation into an aggregation entry.
57
+ *
58
+ * @param {AggregationEntry} entry
59
+ * @param {number} timestamp
60
+ * @param {unknown} consent
61
+ */
62
+ function observeEntry (entry, timestamp, consent) {
63
+ entry.count++
64
+ entry.first = Math.min(entry.first, timestamp)
65
+ entry.last = Math.max(entry.last, timestamp)
66
+ entry.consent &&= consent === true
67
+ }
68
+
69
+ /** @param {Map<string, AggregationEntry>} entries */
70
+ function evaluationCount (entries) {
71
+ let count = 0
72
+ for (const entry of entries.values()) count += entry.count
73
+ return count
74
+ }
75
+
76
+ class FlagEvaluationAggregator {
77
+ /** @type {Map<string, AggregationEntry>} */
78
+ #full = new Map()
79
+ /** @type {Map<string, AggregationEntry>} */
80
+ #degraded = new Map()
81
+ /** @type {Map<string, number>} */
82
+ #perFlag = new Map()
83
+
84
+ /** @param {FlagEvaluationEvent} event */
85
+ add (event) {
86
+ const flagKey = normalizeTargetingKey(event.flagKey)
87
+ if (flagKey === undefined || !Number.isSafeInteger(event.timestamp)) {
88
+ recordDropped('serialization_error')
89
+ return
90
+ }
91
+ const consent = event.observeFullEvaluationData === true
92
+ const targetingKey = normalizeTargetingKey(event.targetingKey)
93
+ const contextEntries = consent ? validatedContextEntries(event.attrs) : undefined
94
+ const error = protectedErrorCode(event.errorCode)
95
+ const variant = optionalKey(event.variant)
96
+ const allocation = optionalKey(event.allocationKey)
97
+ // Group by the raw key; the serializer independently hashes protected rows once per output bucket.
98
+ const identity = JSON.stringify([
99
+ flagKey, variant, allocation, event.runtimeDefault === true, error,
100
+ targetingKey, contextEntries, consent,
101
+ ])
102
+ // Keep digest keys separate from short JSON identities, which always start with '['.
103
+ const fullKey = identity.length > MAX_DIRECT_IDENTITY_LENGTH
104
+ ? '#' + createHash('sha256').update(identity).digest('base64')
105
+ : identity
106
+ const existing = this.#full.get(fullKey)
107
+ if (existing) {
108
+ observeEntry(existing, event.timestamp, consent)
109
+ return
110
+ }
111
+
112
+ const perFlag = this.#perFlag.get(flagKey) ?? 0
113
+ if (perFlag >= FLAG_EVALUATION_PER_FLAG_CAP) {
114
+ this.#addDegraded(event, { flagKey, variant, allocation, error })
115
+ return
116
+ }
117
+ if (this.#full.size >= FLAG_EVALUATION_GLOBAL_CAP) {
118
+ this.#addDegraded(event, { flagKey, variant, allocation, error })
119
+ return
120
+ }
121
+
122
+ this.#perFlag.set(flagKey, perFlag + 1)
123
+ this.#full.set(fullKey, {
124
+ flagKey,
125
+ variant,
126
+ allocation,
127
+ runtimeDefault: event.runtimeDefault === true,
128
+ error,
129
+ rawTargetingKey: targetingKey,
130
+ // Repeated observations need validation and identity, but no new retained snapshot.
131
+ attrs: snapshotFromEntries(contextEntries),
132
+ consent,
133
+ count: 1,
134
+ first: event.timestamp,
135
+ last: event.timestamp,
136
+ })
137
+ }
138
+
139
+ /** @returns {AggregationResult} */
140
+ take () {
141
+ const result = { full: this.#full, degraded: this.#degraded }
142
+ this.#full = new Map()
143
+ this.#degraded = new Map()
144
+ this.#perFlag = new Map()
145
+ return result
146
+ }
147
+
148
+ clear () {
149
+ const count = evaluationCount(this.#full) + evaluationCount(this.#degraded)
150
+ this.#full = new Map()
151
+ this.#degraded = new Map()
152
+ this.#perFlag = new Map()
153
+ return count
154
+ }
155
+
156
+ get size () {
157
+ return this.#full.size + this.#degraded.size
158
+ }
159
+
160
+ /**
161
+ * @param {FlagEvaluationEvent} event
162
+ * @param {AggregationDimensions} dimensions
163
+ */
164
+ #addDegraded (event, dimensions) {
165
+ recordDegraded('cardinality_cap')
166
+ const key = JSON.stringify([
167
+ dimensions.flagKey, dimensions.variant, dimensions.allocation,
168
+ event.runtimeDefault === true, dimensions.error,
169
+ ])
170
+ const existing = this.#degraded.get(key)
171
+ if (existing) {
172
+ observeEntry(existing, event.timestamp, event.observeFullEvaluationData)
173
+ return
174
+ }
175
+ if (this.#degraded.size >= FLAG_EVALUATION_DEGRADED_CAP) {
176
+ recordDropped('degraded_cap')
177
+ return
178
+ }
179
+ this.#degraded.set(key, {
180
+ ...dimensions,
181
+ runtimeDefault: event.runtimeDefault === true,
182
+ consent: event.observeFullEvaluationData === true,
183
+ count: 1,
184
+ first: event.timestamp,
185
+ last: event.timestamp,
186
+ })
187
+ }
188
+ }
189
+
190
+ module.exports = { FlagEvaluationAggregator }
@@ -0,0 +1,233 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ EVP_PAYLOAD_SIZE_LIMIT,
5
+ FLAG_EVALUATION_ENDPOINT,
6
+ FLAG_EVALUATION_FLUSH_INTERVAL,
7
+ FLAG_EVALUATION_QUEUE_CAP,
8
+ } = require('../constants/constants')
9
+ const {
10
+ EVP_EVENT_PLATFORM_SUBDOMAIN,
11
+ EVP_PROXY_PATH_V2,
12
+ EVP_SUBDOMAIN_HEADER_NAME,
13
+ } = require('../../evp_proxy/constants')
14
+ const { joinEVPProxyPath } = require('../../evp_proxy/path')
15
+ const { FlagEvaluationAggregator } = require('./flag-evaluation-aggregation')
16
+ const { iterateFlagEvaluationPayloads } = require('./flag-evaluation-payload')
17
+ const { normalizeFlagEvaluationEvent, normalizeTargetingKey } = require('./flag-evaluation-pii')
18
+ const { recordDropped, recordTargetingKeyOmitted } = require('./flag-evaluation-telemetry')
19
+ const BaseFFEWriter = require('./base')
20
+
21
+ /** @typedef {import('./flag-evaluation-aggregation').FlagEvaluationEvent} FlagEvaluationEvent */
22
+
23
+ /**
24
+ * @typedef {object} FlagEvaluationRoute
25
+ * @property {URL} url
26
+ * @property {string} basePath
27
+ * @property {object} [headers]
28
+ * @property {import('node:https').Agent} [agent]
29
+ * @property {() => void} [onFallback]
30
+ * @property {() => void} [onUnavailable]
31
+ * @property {FlagEvaluationRoute} [fallback]
32
+ */
33
+
34
+ class FlagEvaluationConsumer extends BaseFFEWriter {
35
+ #enabled = false
36
+ #closed = false
37
+ #queue = []
38
+ #immediate
39
+ #aggregator = new FlagEvaluationAggregator()
40
+ #context
41
+ #pendingBytes = 0
42
+ #pending
43
+ #pendingCount = 0
44
+ #pumping = false
45
+ #flushRequested = false
46
+ #onProcessed
47
+ #onDelivered
48
+ #onIdle
49
+
50
+ /**
51
+ * @param {import('../../config/config-base')} config
52
+ * @param {FlagEvaluationRoute} [route]
53
+ * @param {object} [callbacks] - Worker ownership callbacks
54
+ * @param {(count: number) => void} [callbacks.onProcessed] - Release input credit after processing
55
+ * @param {(count: number) => void} [callbacks.onDelivered] - Release observations after successful delivery
56
+ * @param {() => void} [callbacks.onIdle] - Called once shutdown has drained delivery
57
+ */
58
+ constructor (config, route, { onProcessed, onDelivered, onIdle } = {}) {
59
+ route ??= {
60
+ url: /** @type {URL} */ (config.url),
61
+ basePath: EVP_PROXY_PATH_V2,
62
+ }
63
+ const headers = route.headers ?? { [EVP_SUBDOMAIN_HEADER_NAME]: EVP_EVENT_PLATFORM_SUBDOMAIN }
64
+ super({
65
+ config,
66
+ interval: FLAG_EVALUATION_FLUSH_INTERVAL,
67
+ agentUrl: route.url,
68
+ endpoint: joinEVPProxyPath(route.basePath, FLAG_EVALUATION_ENDPOINT),
69
+ headers,
70
+ // Consent to send evaluation data never permits logging an echoed response body or error message.
71
+ formatError: (error, statusCode) => statusCode === undefined
72
+ ? `Transport error ${error.code ?? 'UNKNOWN'}`
73
+ : `HTTP status ${statusCode}`,
74
+ })
75
+ this.#setRoute({ ...route, headers })
76
+ this.#onProcessed = onProcessed
77
+ this.#onDelivered = onDelivered
78
+ this.#onIdle = onIdle
79
+
80
+ this.#context = { service: typeof config.service === 'string' ? config.service : '' }
81
+ if (typeof config.env === 'string') this.#context.env = config.env
82
+ if (typeof config.version === 'string') this.#context.version = config.version
83
+ }
84
+
85
+ hasCapacity () {
86
+ return this.#enabled && !this.#closed && this.#queue.length < FLAG_EVALUATION_QUEUE_CAP
87
+ }
88
+
89
+ /**
90
+ * @param {boolean} enabled
91
+ * @param {FlagEvaluationRoute} [route]
92
+ */
93
+ setEnabled (enabled, route) {
94
+ if (route) this.#setRoute(route)
95
+ if (!enabled && this.#enabled) this.#discardBuffered('unavailable')
96
+ this.#enabled = enabled && !this.#closed
97
+ }
98
+
99
+ /** @param {FlagEvaluationEvent} event */
100
+ enqueue (event) {
101
+ if (this.#closed) {
102
+ recordDropped('closed')
103
+ return false
104
+ }
105
+ if (!this.#enabled) {
106
+ recordDropped('unavailable')
107
+ return false
108
+ }
109
+ if (this.#queue.length >= FLAG_EVALUATION_QUEUE_CAP) {
110
+ recordDropped('queue_overflow')
111
+ return false
112
+ }
113
+
114
+ const targetingKey = normalizeTargetingKey(event.targetingKey)
115
+ if (targetingKey === undefined && event.targetingKey !== undefined && event.targetingKey !== null) {
116
+ recordTargetingKeyOmitted()
117
+ }
118
+ this.#queue.push(normalizeFlagEvaluationEvent(event, targetingKey))
119
+ if (this.#immediate === undefined) {
120
+ this.#immediate = setImmediate(() => this.#drain())
121
+ this.#immediate.unref?.()
122
+ }
123
+ return true
124
+ }
125
+
126
+ flush () {
127
+ this.#cancelImmediate()
128
+ this.#drain()
129
+ if (!this.#enabled && !this.#closed) return
130
+ this.#flushRequested = true
131
+ this.#pump()
132
+ }
133
+
134
+ destroy () {
135
+ if (this.#closed) return
136
+ this.#cancelImmediate()
137
+ this.#drain()
138
+ this.#closed = true
139
+ this.#enabled = false
140
+ super.destroy()
141
+ }
142
+
143
+ #pump () {
144
+ if (this.#pumping) return
145
+ this.#pumping = true
146
+ try {
147
+ // Reserve a maximum-sized payload before advancing the iterator. HTTP callbacks release this budget.
148
+ while (this.#pendingBytes <= EVP_PAYLOAD_SIZE_LIMIT) {
149
+ if (!this.#pending) {
150
+ if ((!this.#flushRequested && !this.#closed) || this.#aggregator.size === 0) break
151
+ this.#flushRequested = false
152
+ const { full, degraded } = this.#aggregator.take()
153
+ for (const entry of full.values()) this.#pendingCount += entry.count
154
+ for (const entry of degraded.values()) this.#pendingCount += entry.count
155
+ this.#pending = iterateFlagEvaluationPayloads(full, degraded, this.#context, Date.now(), count => {
156
+ this.#pendingCount -= count
157
+ })
158
+ }
159
+ const next = this.#pending.next()
160
+ if (next.done) {
161
+ this.#pending = undefined
162
+ continue
163
+ }
164
+ const payload = next.value
165
+ const bytes = Buffer.byteLength(payload.encoded)
166
+ this.#pendingBytes += bytes
167
+ this._sendPayload(payload.encoded, payload.rows, delivered => {
168
+ this.#pendingBytes -= bytes
169
+ if (delivered) this.#onDelivered?.(payload.evaluations)
170
+ else recordDropped('delivery_failure', payload.evaluations)
171
+ this.#pump()
172
+ })
173
+ }
174
+ } finally {
175
+ this.#pumping = false
176
+ }
177
+ if (this.#closed && !this.#pending && this.#aggregator.size === 0 && this.#pendingBytes === 0) {
178
+ this.#onIdle?.()
179
+ this.#onIdle = undefined
180
+ }
181
+ }
182
+
183
+ /** @param {FlagEvaluationRoute} route */
184
+ #setRoute (route) {
185
+ const headers = route.headers ?? { [EVP_SUBDOMAIN_HEADER_NAME]: EVP_EVENT_PLATFORM_SUBDOMAIN }
186
+ const fallback = route.fallback && {
187
+ url: route.fallback.url,
188
+ endpoint: joinEVPProxyPath(route.fallback.basePath, FLAG_EVALUATION_ENDPOINT),
189
+ headers: route.fallback.headers ?? {},
190
+ agent: route.fallback.agent,
191
+ onFallback: route.fallback.onFallback,
192
+ onUnavailable: route.fallback.onUnavailable,
193
+ }
194
+ this._setRoutes({
195
+ url: route.url,
196
+ endpoint: joinEVPProxyPath(route.basePath, FLAG_EVALUATION_ENDPOINT),
197
+ headers,
198
+ agent: route.agent,
199
+ onFallback: route.onFallback,
200
+ onUnavailable: route.onUnavailable,
201
+ }, fallback)
202
+ }
203
+
204
+ #cancelImmediate () {
205
+ if (this.#immediate !== undefined) clearImmediate(this.#immediate)
206
+ this.#immediate = undefined
207
+ }
208
+
209
+ #drain () {
210
+ this.#immediate = undefined
211
+ const queue = this.#queue
212
+ this.#queue = []
213
+ for (const event of queue) {
214
+ this.#aggregator.add(event)
215
+ this.#onProcessed?.(1)
216
+ }
217
+ }
218
+
219
+ /** @param {string} reason */
220
+ #discardBuffered (reason) {
221
+ this.#cancelImmediate()
222
+ const count = this.#queue.length + this.#aggregator.clear() + this.#pendingCount
223
+ this.#pending?.return()
224
+ this.#pending = undefined
225
+ this.#pendingCount = 0
226
+ this.#flushRequested = false
227
+ this.#onProcessed?.(this.#queue.length)
228
+ this.#queue = []
229
+ recordDropped(reason, count)
230
+ }
231
+ }
232
+
233
+ module.exports = FlagEvaluationConsumer