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.
- package/index.d.ts +163 -22
- package/package.json +2 -2
- package/packages/datadog-esbuild/index.js +9 -6
- package/packages/datadog-instrumentations/src/ai.js +26 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
- package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
- package/packages/dd-trace/src/config/remote_config.js +12 -0
- package/packages/dd-trace/src/config/supported-configurations.json +21 -0
- package/packages/dd-trace/src/debugger/constants.js +13 -4
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
- package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
- package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
- package/packages/dd-trace/src/debugger/index.js +60 -28
- package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
- package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
- package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
- package/packages/dd-trace/src/debugger/redaction.js +139 -0
- package/packages/dd-trace/src/knuth-hash.js +17 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
- package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
- package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
- package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
- package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +2 -2
- package/packages/dd-trace/src/llmobs/sdk.js +19 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +42 -26
- package/packages/dd-trace/src/llmobs/tagger.js +35 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
- package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
- package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
- package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
- package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
- package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
- package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
- package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
- package/packages/dd-trace/src/opentracing/span.js +14 -9
- package/packages/dd-trace/src/opentracing/tracer.js +10 -3
- package/packages/dd-trace/src/otel-sampling.js +171 -0
- package/packages/dd-trace/src/plugins/index.js +0 -1
- package/packages/dd-trace/src/priority_sampler.js +155 -33
- package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
- package/packages/dd-trace/src/sampler.js +2 -5
- package/packages/dd-trace/src/sampling_rule.js +4 -8
- package/packages/dd-trace/src/span_format.js +6 -0
- package/packages/dd-trace/src/span_processor.js +26 -2
- package/packages/dd-trace/src/standalone/index.js +5 -4
- package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
- package/packages/dd-trace/src/telemetry/metrics.js +4 -3
- package/packages/datadog-instrumentations/src/postgres.js +0 -7
- 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
|