dd-trace 6.18.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/LICENSE-3rdparty.csv +1 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +166 -24
- package/package.json +7 -7
- package/packages/datadog-esbuild/index.js +9 -6
- package/packages/datadog-instrumentations/src/ai.js +26 -0
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +40 -4
- 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-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +3 -10
- package/packages/datadog-instrumentations/src/playwright.js +26 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +19 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
- package/packages/dd-trace/src/config/remote_config.js +12 -0
- package/packages/dd-trace/src/config/supported-configurations.json +26 -5
- package/packages/dd-trace/src/constants.js +2 -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 +33 -8
- 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/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
- package/packages/dd-trace/src/llmobs/sdk.js +19 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
- package/packages/dd-trace/src/llmobs/tagger.js +37 -36
- 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/index.js +5 -0
- 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/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/priority_sampler.js +155 -33
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
- package/packages/dd-trace/src/ritm.js +12 -9
- 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 +32 -3
- 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/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/packages/datadog-instrumentations/src/postgres.js +0 -7
- package/packages/datadog-instrumentations/src/supabase.js +0 -15
|
@@ -7,6 +7,8 @@ const logger = require('../log')
|
|
|
7
7
|
const { getValueFromEnvSources } = require('../config/helper')
|
|
8
8
|
const Span = require('../opentracing/span')
|
|
9
9
|
const {
|
|
10
|
+
EXPERIMENT_INPUT,
|
|
11
|
+
EXPERIMENT_OUTPUT,
|
|
10
12
|
SPAN_KIND,
|
|
11
13
|
OUTPUT_VALUE,
|
|
12
14
|
INPUT_VALUE,
|
|
@@ -300,13 +302,18 @@ class LLMObs extends NoopLLMObs {
|
|
|
300
302
|
|
|
301
303
|
const { inputData, outputData, metadata, metrics, tags, prompt, costTags, toolDefinitions } = options
|
|
302
304
|
|
|
303
|
-
|
|
305
|
+
const hasInputOrOutput = spanKind === 'experiment'
|
|
306
|
+
? inputData !== undefined || outputData !== undefined
|
|
307
|
+
: inputData || outputData
|
|
308
|
+
if (hasInputOrOutput) {
|
|
304
309
|
if (spanKind === 'llm') {
|
|
305
310
|
this._tagger.tagLLMIO(span, inputData, outputData)
|
|
306
311
|
} else if (spanKind === 'embedding') {
|
|
307
312
|
this._tagger.tagEmbeddingIO(span, inputData, outputData)
|
|
308
313
|
} else if (spanKind === 'retrieval') {
|
|
309
314
|
this._tagger.tagRetrievalIO(span, inputData, outputData)
|
|
315
|
+
} else if (spanKind === 'experiment') {
|
|
316
|
+
this._tagger.tagExperimentIO(span, inputData, outputData)
|
|
310
317
|
} else {
|
|
311
318
|
this._tagger.tagTextIO(span, inputData, outputData)
|
|
312
319
|
}
|
|
@@ -638,11 +645,20 @@ class LLMObs extends NoopLLMObs {
|
|
|
638
645
|
|
|
639
646
|
#autoAnnotate (span, kind, input, output) {
|
|
640
647
|
const annotations = {}
|
|
641
|
-
|
|
648
|
+
const spanTags = LLMObsTagger.tagMap.get(span)
|
|
649
|
+
const isExperiment = kind === 'experiment'
|
|
650
|
+
const inputKey = isExperiment ? EXPERIMENT_INPUT : INPUT_VALUE
|
|
651
|
+
const outputKey = isExperiment ? EXPERIMENT_OUTPUT : OUTPUT_VALUE
|
|
652
|
+
const hasInput = isExperiment ? input !== undefined : input
|
|
653
|
+
const hasOutput = isExperiment ? output !== undefined : output
|
|
654
|
+
const hasInputTag = spanTags !== undefined && Object.hasOwn(spanTags, inputKey)
|
|
655
|
+
const hasOutputTag = spanTags !== undefined && Object.hasOwn(spanTags, outputKey)
|
|
656
|
+
|
|
657
|
+
if (hasInput && !['llm', 'embedding'].includes(kind) && !hasInputTag) {
|
|
642
658
|
annotations.inputData = input
|
|
643
659
|
}
|
|
644
660
|
|
|
645
|
-
if (
|
|
661
|
+
if (hasOutput && !['llm', 'retrieval'].includes(kind) && !hasOutputTag) {
|
|
646
662
|
annotations.outputData = output
|
|
647
663
|
}
|
|
648
664
|
|
|
@@ -16,6 +16,8 @@ const {
|
|
|
16
16
|
METADATA,
|
|
17
17
|
COST_TAGS,
|
|
18
18
|
TOOL_DEFINITIONS,
|
|
19
|
+
EXPERIMENT_INPUT,
|
|
20
|
+
EXPERIMENT_OUTPUT,
|
|
19
21
|
INPUT_MESSAGES,
|
|
20
22
|
INPUT_VALUE,
|
|
21
23
|
INTEGRATION,
|
|
@@ -38,41 +40,13 @@ const {
|
|
|
38
40
|
SAMPLE_RATE,
|
|
39
41
|
SAMPLING_DECISION,
|
|
40
42
|
TRACE_ID,
|
|
41
|
-
|
|
42
|
-
OUTPUT_TOKENS_METRIC_KEY,
|
|
43
|
-
TOTAL_TOKENS_METRIC_KEY,
|
|
44
|
-
CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
45
|
-
CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
46
|
-
REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
47
|
-
GEN_AI_OPERATION_NAME,
|
|
48
|
-
GEN_AI_REQUEST_MODEL,
|
|
49
|
-
GEN_AI_PROVIDER_NAME,
|
|
50
|
-
GEN_AI_APPLICATION_NAME,
|
|
51
|
-
GEN_AI_CONVERSATION_ID,
|
|
52
|
-
GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY,
|
|
53
|
-
GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY,
|
|
54
|
-
GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY,
|
|
55
|
-
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
56
|
-
GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
57
|
-
GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
58
|
-
ARTIFICIAL_GEN_AI_TAGS,
|
|
43
|
+
DEFAULT_MODEL,
|
|
59
44
|
} = require('./constants/tags')
|
|
60
45
|
const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
|
|
46
|
+
const { setGenAiApmTags } = require('./gen-ai-tags')
|
|
61
47
|
const telemetry = require('./telemetry')
|
|
62
48
|
const LLMObsTagger = require('./tagger')
|
|
63
49
|
|
|
64
|
-
const DEFAULT_MODEL = 'custom'
|
|
65
|
-
const MODEL_BACKED_SPAN_KINDS = new Set(['llm', 'embedding'])
|
|
66
|
-
|
|
67
|
-
const GEN_AI_TOKEN_METRIC_KEYS = [
|
|
68
|
-
[INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY],
|
|
69
|
-
[OUTPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY],
|
|
70
|
-
[TOTAL_TOKENS_METRIC_KEY, GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY],
|
|
71
|
-
[CACHE_READ_INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY],
|
|
72
|
-
[CACHE_WRITE_INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY],
|
|
73
|
-
[REASONING_OUTPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY],
|
|
74
|
-
]
|
|
75
|
-
|
|
76
50
|
class LLMObservabilitySpan {
|
|
77
51
|
/**
|
|
78
52
|
* @param {string} kind span kind
|
|
@@ -210,8 +184,13 @@ class LLMObsSpanProcessor {
|
|
|
210
184
|
}
|
|
211
185
|
|
|
212
186
|
const llmObsSpan = new LLMObservabilitySpan(spanKind)
|
|
187
|
+
const isExperiment = spanKind === 'experiment'
|
|
188
|
+
const hasExperimentInput = isExperiment && Object.hasOwn(mlObsTags, EXPERIMENT_INPUT)
|
|
189
|
+
const hasExperimentOutput = isExperiment && Object.hasOwn(mlObsTags, EXPERIMENT_OUTPUT)
|
|
213
190
|
|
|
214
|
-
if (
|
|
191
|
+
if (hasExperimentInput) {
|
|
192
|
+
llmObsSpan.input = [{ role: '', content: mlObsTags[EXPERIMENT_INPUT] }]
|
|
193
|
+
} else if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
|
|
215
194
|
llmObsSpan.input = mlObsTags[INPUT_MESSAGES]
|
|
216
195
|
inputType = 'messages'
|
|
217
196
|
} else if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
|
|
@@ -222,7 +201,9 @@ class LLMObsSpanProcessor {
|
|
|
222
201
|
inputType = 'value'
|
|
223
202
|
}
|
|
224
203
|
|
|
225
|
-
if (
|
|
204
|
+
if (hasExperimentOutput) {
|
|
205
|
+
llmObsSpan.output = [{ role: '', content: mlObsTags[EXPERIMENT_OUTPUT] }]
|
|
206
|
+
} else if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
|
|
226
207
|
llmObsSpan.output = mlObsTags[OUTPUT_MESSAGES]
|
|
227
208
|
outputType = 'messages'
|
|
228
209
|
} else if (spanKind === 'retrieval' && mlObsTags[OUTPUT_DOCUMENTS]) {
|
|
@@ -254,34 +235,41 @@ class LLMObsSpanProcessor {
|
|
|
254
235
|
const processedSpan = this.#runProcessor(llmObsSpan)
|
|
255
236
|
if (processedSpan === undefined) return null
|
|
256
237
|
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
})
|
|
238
|
+
if (isExperiment) {
|
|
239
|
+
const [processedInput] = processedSpan.input
|
|
240
|
+
const [processedOutput] = processedSpan.output
|
|
241
|
+
if (hasExperimentInput && processedInput !== undefined) meta.input = processedInput.content
|
|
242
|
+
if (hasExperimentOutput && processedOutput !== undefined) meta.output = processedOutput.content
|
|
243
|
+
} else {
|
|
244
|
+
if (processedSpan.input) {
|
|
245
|
+
if (inputType === 'messages') {
|
|
246
|
+
input.messages = processedSpan.input
|
|
247
|
+
} else if (inputType === 'value') {
|
|
248
|
+
input.value = processedSpan.input[0].content
|
|
249
|
+
} else if (inputType === 'documents') {
|
|
250
|
+
input.documents = processedSpan.input.map((processedDocument, processedDocumentIdx) => ({
|
|
251
|
+
...mlObsTags[INPUT_DOCUMENTS][processedDocumentIdx],
|
|
252
|
+
text: processedDocument.content,
|
|
253
|
+
}))
|
|
254
|
+
}
|
|
267
255
|
}
|
|
268
|
-
}
|
|
269
256
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
257
|
+
if (processedSpan.output) {
|
|
258
|
+
if (outputType === 'messages') {
|
|
259
|
+
output.messages = processedSpan.output
|
|
260
|
+
} else if (outputType === 'value') {
|
|
261
|
+
output.value = processedSpan.output[0].content
|
|
262
|
+
} else if (outputType === 'documents') {
|
|
263
|
+
output.documents = processedSpan.output.map((processedDocument, processedDocumentIdx) => ({
|
|
264
|
+
...mlObsTags[OUTPUT_DOCUMENTS][processedDocumentIdx],
|
|
265
|
+
text: processedDocument.content,
|
|
266
|
+
}))
|
|
267
|
+
}
|
|
280
268
|
}
|
|
281
|
-
}
|
|
282
269
|
|
|
283
|
-
|
|
284
|
-
|
|
270
|
+
meta.input = input
|
|
271
|
+
meta.output = output
|
|
272
|
+
}
|
|
285
273
|
|
|
286
274
|
const prompt = mlObsTags[INPUT_PROMPT]
|
|
287
275
|
if (prompt && spanKind === 'llm') {
|
|
@@ -320,46 +308,19 @@ class LLMObsSpanProcessor {
|
|
|
320
308
|
}
|
|
321
309
|
|
|
322
310
|
/**
|
|
323
|
-
* Writes the scalar `gen_ai.*` attributes onto the APM span, so model, provider, application,
|
|
324
|
-
* conversation and token usage are searchable in APM. Message bodies stay off the APM span.
|
|
325
|
-
*
|
|
326
311
|
* @param {import('../opentracing/span')} span
|
|
327
312
|
*/
|
|
328
313
|
#setGenAiApmTags (span) {
|
|
329
314
|
const mlObsTags = LLMObsTagger.tagMap.get(span)
|
|
330
|
-
const spanContext = span.context()
|
|
331
|
-
const spanKind = mlObsTags[SPAN_KIND]
|
|
332
|
-
|
|
333
|
-
if (spanKind) spanContext.setTag(GEN_AI_OPERATION_NAME, spanKind)
|
|
334
|
-
|
|
335
|
-
const modelName = mlObsTags[MODEL_NAME]
|
|
336
|
-
const modelProvider = mlObsTags[MODEL_PROVIDER]
|
|
337
|
-
const modelBacked = MODEL_BACKED_SPAN_KINDS.has(spanKind)
|
|
338
|
-
if (modelBacked) {
|
|
339
|
-
spanContext.setTag(GEN_AI_REQUEST_MODEL, modelName || DEFAULT_MODEL)
|
|
340
|
-
spanContext.setTag(GEN_AI_PROVIDER_NAME, (modelProvider || DEFAULT_MODEL).toLowerCase())
|
|
341
|
-
} else {
|
|
342
|
-
if (modelName) spanContext.setTag(GEN_AI_REQUEST_MODEL, modelName)
|
|
343
|
-
if (modelProvider) spanContext.setTag(GEN_AI_PROVIDER_NAME, modelProvider.toLowerCase())
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const mlApp = mlObsTags[ML_APP]
|
|
347
|
-
if (mlApp) spanContext.setTag(GEN_AI_APPLICATION_NAME, mlApp)
|
|
348
|
-
|
|
349
|
-
const sessionId = mlObsTags[SESSION_ID]
|
|
350
|
-
if (sessionId) spanContext.setTag(GEN_AI_CONVERSATION_ID, sessionId)
|
|
351
|
-
|
|
352
|
-
const metrics = mlObsTags[METRICS]
|
|
353
|
-
// Other kinds carry unrelated metrics that would be misleading under a `gen_ai.usage.*` key.
|
|
354
|
-
if (modelBacked && metrics) {
|
|
355
|
-
for (const [metricKey, genAiKey] of GEN_AI_TOKEN_METRIC_KEYS) {
|
|
356
|
-
const value = metrics[metricKey]
|
|
357
|
-
if (value != null) spanContext.setTag(genAiKey, value)
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
315
|
|
|
361
|
-
|
|
362
|
-
|
|
316
|
+
setGenAiApmTags(span, {
|
|
317
|
+
spanKind: mlObsTags[SPAN_KIND],
|
|
318
|
+
modelName: mlObsTags[MODEL_NAME],
|
|
319
|
+
modelProvider: mlObsTags[MODEL_PROVIDER],
|
|
320
|
+
mlApp: mlObsTags[ML_APP],
|
|
321
|
+
sessionId: mlObsTags[SESSION_ID],
|
|
322
|
+
metrics: mlObsTags[METRICS],
|
|
323
|
+
})
|
|
363
324
|
}
|
|
364
325
|
|
|
365
326
|
// For now, this only applies to metadata, as we let users annotate this field with any object
|
|
@@ -10,6 +10,8 @@ const {
|
|
|
10
10
|
SESSION_ID_TRACE_DEFAULT_KEY,
|
|
11
11
|
ML_APP,
|
|
12
12
|
SPAN_KIND,
|
|
13
|
+
EXPERIMENT_INPUT,
|
|
14
|
+
EXPERIMENT_OUTPUT,
|
|
13
15
|
INPUT_VALUE,
|
|
14
16
|
OUTPUT_DOCUMENTS,
|
|
15
17
|
INPUT_DOCUMENTS,
|
|
@@ -29,14 +31,7 @@ const {
|
|
|
29
31
|
PROPAGATED_PARENT_AGENT_ID_KEY,
|
|
30
32
|
PROPAGATED_PARENT_AGENT_NAME_KEY,
|
|
31
33
|
ROOT_PARENT_ID,
|
|
32
|
-
|
|
33
|
-
CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
34
|
-
CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY,
|
|
35
|
-
CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY,
|
|
36
|
-
INPUT_TOKENS_METRIC_KEY,
|
|
37
|
-
OUTPUT_TOKENS_METRIC_KEY,
|
|
38
|
-
TOTAL_TOKENS_METRIC_KEY,
|
|
39
|
-
REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
34
|
+
METRIC_KEY_ALIASES,
|
|
40
35
|
INTEGRATION,
|
|
41
36
|
DECORATOR,
|
|
42
37
|
PROPAGATED_ML_APP_KEY,
|
|
@@ -280,6 +275,18 @@ class LLMObsTagger {
|
|
|
280
275
|
this.#tagDocuments(span, outputData, OUTPUT_DOCUMENTS)
|
|
281
276
|
}
|
|
282
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Tags arbitrary JSON-compatible experiment input and output without converting structured values to text.
|
|
280
|
+
*
|
|
281
|
+
* @param {import('../opentracing/span')} span
|
|
282
|
+
* @param {unknown} inputData
|
|
283
|
+
* @param {unknown} outputData
|
|
284
|
+
*/
|
|
285
|
+
tagExperimentIO (span, inputData, outputData) {
|
|
286
|
+
this.#tagExperimentValue(span, inputData, EXPERIMENT_INPUT, 'input')
|
|
287
|
+
this.#tagExperimentValue(span, outputData, EXPERIMENT_OUTPUT, 'output')
|
|
288
|
+
}
|
|
289
|
+
|
|
283
290
|
tagTextIO (span, inputData, outputData) {
|
|
284
291
|
this.#tagText(span, inputData, INPUT_VALUE)
|
|
285
292
|
this.#tagText(span, outputData, OUTPUT_VALUE)
|
|
@@ -307,35 +314,8 @@ class LLMObsTagger {
|
|
|
307
314
|
tagMetrics (span, metrics) {
|
|
308
315
|
const filterdMetrics = {}
|
|
309
316
|
for (const [key, value] of Object.entries(metrics)) {
|
|
310
|
-
let processedKey = key
|
|
311
|
-
|
|
312
317
|
// processing these specifically for our metrics ingestion
|
|
313
|
-
|
|
314
|
-
case 'inputTokens':
|
|
315
|
-
processedKey = INPUT_TOKENS_METRIC_KEY
|
|
316
|
-
break
|
|
317
|
-
case 'outputTokens':
|
|
318
|
-
processedKey = OUTPUT_TOKENS_METRIC_KEY
|
|
319
|
-
break
|
|
320
|
-
case 'totalTokens':
|
|
321
|
-
processedKey = TOTAL_TOKENS_METRIC_KEY
|
|
322
|
-
break
|
|
323
|
-
case 'cacheReadTokens':
|
|
324
|
-
processedKey = CACHE_READ_INPUT_TOKENS_METRIC_KEY
|
|
325
|
-
break
|
|
326
|
-
case 'cacheWriteTokens':
|
|
327
|
-
processedKey = CACHE_WRITE_INPUT_TOKENS_METRIC_KEY
|
|
328
|
-
break
|
|
329
|
-
case 'cacheWrite5mTokens':
|
|
330
|
-
processedKey = CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY
|
|
331
|
-
break
|
|
332
|
-
case 'cacheWrite1hTokens':
|
|
333
|
-
processedKey = CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY
|
|
334
|
-
break
|
|
335
|
-
case 'reasoningOutputTokens':
|
|
336
|
-
processedKey = REASONING_OUTPUT_TOKENS_METRIC_KEY
|
|
337
|
-
break
|
|
338
|
-
}
|
|
318
|
+
const processedKey = METRIC_KEY_ALIASES[key] ?? key
|
|
339
319
|
|
|
340
320
|
if (typeof value === 'number') {
|
|
341
321
|
filterdMetrics[processedKey] = value
|
|
@@ -596,6 +576,27 @@ class LLMObsTagger {
|
|
|
596
576
|
}
|
|
597
577
|
}
|
|
598
578
|
|
|
579
|
+
/**
|
|
580
|
+
* Validates and stores one free-form experiment I/O value.
|
|
581
|
+
*
|
|
582
|
+
* @param {import('../opentracing/span')} span
|
|
583
|
+
* @param {unknown} data
|
|
584
|
+
* @param {string} key
|
|
585
|
+
* @param {string} type
|
|
586
|
+
*/
|
|
587
|
+
#tagExperimentValue (span, data, key, type) {
|
|
588
|
+
if (data === undefined) return
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
if (JSON.stringify(data) !== undefined) {
|
|
592
|
+
this._setTag(span, key, data)
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
} catch {}
|
|
596
|
+
|
|
597
|
+
this.#handleFailure(`Failed to parse ${type} value, must be JSON serializable.`, 'invalid_io_text')
|
|
598
|
+
}
|
|
599
|
+
|
|
599
600
|
#tagDocuments (span, data, key) {
|
|
600
601
|
if (!data) {
|
|
601
602
|
return
|
|
@@ -293,12 +293,7 @@ class BaseLLMObsWriter {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
_encode (payload) {
|
|
296
|
-
return JSON.stringify(payload
|
|
297
|
-
if (typeof value === 'string') {
|
|
298
|
-
return encodeUnicode(value) // serialize unicode characters
|
|
299
|
-
}
|
|
300
|
-
return value
|
|
301
|
-
}).replaceAll(String.raw`\\u`, String.raw`\u`) // remove double escaping
|
|
296
|
+
return encodeUnicode(JSON.stringify(payload))
|
|
302
297
|
}
|
|
303
298
|
}
|
|
304
299
|
|
|
@@ -19,6 +19,16 @@ module.exports = {
|
|
|
19
19
|
*/
|
|
20
20
|
EVP_EVENT_SIZE_LIMIT: (1 << 20) - 1024,
|
|
21
21
|
|
|
22
|
+
FLAG_EVALUATION_ENDPOINT: '/api/v2/flagevaluation',
|
|
23
|
+
FLAG_EVALUATION_FLUSH_INTERVAL: 10_000,
|
|
24
|
+
FLAG_EVALUATION_QUEUE_CAP: 4096,
|
|
25
|
+
FLAG_EVALUATION_GLOBAL_CAP: 131_072,
|
|
26
|
+
FLAG_EVALUATION_PER_FLAG_CAP: 10_000,
|
|
27
|
+
FLAG_EVALUATION_DEGRADED_CAP: 32_768,
|
|
28
|
+
|
|
29
|
+
// ECMAScript Date's maximum absolute time value, in milliseconds.
|
|
30
|
+
MAX_EVALUATION_TIMESTAMP_MS: 8_640_000_000_000_000,
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* @constant
|
|
24
34
|
* @type {string} Channel name for exposure event submission
|
|
@@ -8,6 +8,7 @@ const configurationSource = require('./configuration_source')
|
|
|
8
8
|
const { EXPOSURE_CHANNEL } = require('./constants/constants')
|
|
9
9
|
const EvalMetricsHook = require('./eval-metrics-hook')
|
|
10
10
|
const SpanEnrichmentHook = require('./span-enrichment-hook')
|
|
11
|
+
const FlagEvalEVPHook = require('./writers/flag-eval-evp-hook')
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* OpenFeature provider that integrates with Datadog's feature flagging system.
|
|
@@ -17,6 +18,9 @@ class FlaggingProvider extends DatadogNodeServerProvider {
|
|
|
17
18
|
/** @type {SpanEnrichmentHook | undefined} */
|
|
18
19
|
#spanEnrichmentHook
|
|
19
20
|
|
|
21
|
+
/** @type {FlagEvalEVPHook | undefined} */
|
|
22
|
+
#flagEvalEVPHook
|
|
23
|
+
|
|
20
24
|
/** @type {{ start: Function, stop: Function } | undefined} */
|
|
21
25
|
#configurationSource
|
|
22
26
|
|
|
@@ -30,7 +34,9 @@ class FlaggingProvider extends DatadogNodeServerProvider {
|
|
|
30
34
|
initializationTimeoutMs: config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS,
|
|
31
35
|
})
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
if (config.DD_METRICS_OTEL_ENABLED === true) {
|
|
38
|
+
this.hooks.push(new EvalMetricsHook(config))
|
|
39
|
+
}
|
|
34
40
|
|
|
35
41
|
if (config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED) {
|
|
36
42
|
this.#spanEnrichmentHook = new SpanEnrichmentHook(tracer)
|
|
@@ -44,6 +50,11 @@ class FlaggingProvider extends DatadogNodeServerProvider {
|
|
|
44
50
|
log.debug('%s created with timeout: %dms', this.constructor.name,
|
|
45
51
|
config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS)
|
|
46
52
|
|
|
53
|
+
if (config.featureFlags?.DD_FLAGGING_EVALUATION_COUNTS_ENABLED !== false) {
|
|
54
|
+
this.#flagEvalEVPHook = new FlagEvalEVPHook(config)
|
|
55
|
+
this.hooks.push(this.#flagEvalEVPHook)
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
this.#configurationSource = configurationSource.create(config, this.setConfiguration.bind(this))
|
|
48
59
|
this.#configurationSource?.start()
|
|
49
60
|
}
|
|
@@ -73,6 +84,8 @@ class FlaggingProvider extends DatadogNodeServerProvider {
|
|
|
73
84
|
this.#configurationSource = undefined
|
|
74
85
|
this.#spanEnrichmentHook?.destroy()
|
|
75
86
|
this.#spanEnrichmentHook = undefined
|
|
87
|
+
this.#flagEvalEVPHook?.destroy()
|
|
88
|
+
this.#flagEvalEVPHook = undefined
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -20,6 +20,7 @@ const EVP_ORIGIN_HEADERS = {
|
|
|
20
20
|
* @property {number} [payloadSizeLimit] - Maximum payload size in bytes
|
|
21
21
|
* @property {number} [eventSizeLimit] - Maximum individual event size in bytes
|
|
22
22
|
* @property {object} [headers] - Additional HTTP headers
|
|
23
|
+
* @property {(error: Error, statusCode?: number) => string} [formatError] - Optional delivery error redaction
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -80,10 +81,14 @@ function shouldSwitchFutureRoute (statusCode) {
|
|
|
80
81
|
*/
|
|
81
82
|
class BaseFFEWriter {
|
|
82
83
|
#destroyer
|
|
84
|
+
#formatError
|
|
83
85
|
/**
|
|
84
86
|
* @param {BaseFFEWriterOptions} options - Writer configuration options
|
|
85
87
|
*/
|
|
86
|
-
constructor ({
|
|
88
|
+
constructor ({
|
|
89
|
+
interval, timeout, config, endpoint, agentUrl, payloadSizeLimit, eventSizeLimit, headers, formatError,
|
|
90
|
+
}) {
|
|
91
|
+
this.#formatError = formatError
|
|
87
92
|
this._interval = interval ?? 1000
|
|
88
93
|
this._timeout = timeout ?? 5000
|
|
89
94
|
|
|
@@ -219,10 +224,11 @@ class BaseFFEWriter {
|
|
|
219
224
|
* @protected
|
|
220
225
|
* @param {string} payload - Encoded event batch
|
|
221
226
|
* @param {number} eventCount - Event count
|
|
227
|
+
* @param {(delivered: boolean) => void} [onComplete] - Final outcome after any safe fallback attempt
|
|
222
228
|
*/
|
|
223
|
-
_sendPayload (payload, eventCount) {
|
|
229
|
+
_sendPayload (payload, eventCount, onComplete) {
|
|
224
230
|
const route = this.#createActiveRoute()
|
|
225
|
-
this.#sendRequest(payload, eventCount, route, this._fallbackRoute)
|
|
231
|
+
this.#sendRequest(payload, eventCount, route, this._fallbackRoute, onComplete)
|
|
226
232
|
}
|
|
227
233
|
|
|
228
234
|
/**
|
|
@@ -301,9 +307,13 @@ class BaseFFEWriter {
|
|
|
301
307
|
* @param {number} eventCount - Event count
|
|
302
308
|
* @param {ActiveWriterRoute} route - Selected route
|
|
303
309
|
* @param {ActiveWriterRoute} [fallbackRoute] - Direct fallback route
|
|
310
|
+
* @param {(delivered: boolean) => void} [onComplete] - True only for a successful final response
|
|
304
311
|
*/
|
|
305
|
-
#sendRequest (payload, eventCount, route, fallbackRoute) {
|
|
306
|
-
request
|
|
312
|
+
#sendRequest (payload, eventCount, route, fallbackRoute, onComplete) {
|
|
313
|
+
// The request helper mutates headers. Concurrent envelopes must not share them.
|
|
314
|
+
const requestOptions = { ...route.requestOptions, headers: { ...route.requestOptions.headers } }
|
|
315
|
+
request(payload, requestOptions, (error, response, statusCode) => {
|
|
316
|
+
const errorMessage = error && (this.#formatError ? this.#formatError(error, statusCode) : error.message)
|
|
307
317
|
if (fallbackRoute && isSafeToReplay(error, statusCode)) {
|
|
308
318
|
log.debug(
|
|
309
319
|
'%s switching from %s%s to direct intake after definitive rejection',
|
|
@@ -311,10 +321,12 @@ class BaseFFEWriter {
|
|
|
311
321
|
route.url.href,
|
|
312
322
|
route.endpoint
|
|
313
323
|
)
|
|
314
|
-
this
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
if (this._requestOptions === route.requestOptions) {
|
|
325
|
+
this.#activateRoute(fallbackRoute)
|
|
326
|
+
this._fallbackRoute = undefined
|
|
327
|
+
route.onFallback?.()
|
|
328
|
+
}
|
|
329
|
+
this.#sendRequest(payload, eventCount, fallbackRoute, undefined, onComplete)
|
|
318
330
|
return
|
|
319
331
|
}
|
|
320
332
|
|
|
@@ -325,10 +337,13 @@ class BaseFFEWriter {
|
|
|
325
337
|
route.url.href,
|
|
326
338
|
route.endpoint
|
|
327
339
|
)
|
|
328
|
-
this
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
if (this._requestOptions === route.requestOptions) {
|
|
341
|
+
this.#activateRoute(fallbackRoute)
|
|
342
|
+
this._fallbackRoute = undefined
|
|
343
|
+
route.onFallback?.()
|
|
344
|
+
}
|
|
345
|
+
log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint, errorMessage)
|
|
346
|
+
onComplete?.(false)
|
|
332
347
|
return
|
|
333
348
|
}
|
|
334
349
|
|
|
@@ -340,15 +355,19 @@ class BaseFFEWriter {
|
|
|
340
355
|
route.endpoint,
|
|
341
356
|
statusCode
|
|
342
357
|
)
|
|
343
|
-
this
|
|
344
|
-
|
|
345
|
-
|
|
358
|
+
if (this._requestOptions === route.requestOptions) {
|
|
359
|
+
this.#activateRoute(fallbackRoute)
|
|
360
|
+
this._fallbackRoute = undefined
|
|
361
|
+
route.onFallback?.()
|
|
362
|
+
}
|
|
346
363
|
log.warn('Events request returned status %d', statusCode)
|
|
364
|
+
onComplete?.(false)
|
|
347
365
|
return
|
|
348
366
|
}
|
|
349
367
|
|
|
350
368
|
if (
|
|
351
369
|
!fallbackRoute &&
|
|
370
|
+
this._requestOptions === route.requestOptions &&
|
|
352
371
|
route.onUnavailable &&
|
|
353
372
|
(isSafeToReplay(error, statusCode) ||
|
|
354
373
|
isTransportFailure(error, statusCode) ||
|
|
@@ -356,20 +375,22 @@ class BaseFFEWriter {
|
|
|
356
375
|
) {
|
|
357
376
|
route.onUnavailable()
|
|
358
377
|
if (error) {
|
|
359
|
-
log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint,
|
|
378
|
+
log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint, errorMessage)
|
|
360
379
|
} else {
|
|
361
380
|
log.warn('Events request returned status %d', statusCode)
|
|
362
381
|
}
|
|
382
|
+
onComplete?.(false)
|
|
363
383
|
return
|
|
364
384
|
}
|
|
365
385
|
|
|
366
386
|
if (error) {
|
|
367
|
-
log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint,
|
|
387
|
+
log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint, errorMessage)
|
|
368
388
|
} else if (statusCode >= 200 && statusCode < 300) {
|
|
369
389
|
log.debug('Successfully sent %d events', eventCount)
|
|
370
390
|
} else {
|
|
371
391
|
log.warn('Events request returned status %d', statusCode)
|
|
372
392
|
}
|
|
393
|
+
onComplete?.(!error && statusCode >= 200 && statusCode < 300)
|
|
373
394
|
})
|
|
374
395
|
}
|
|
375
396
|
}
|
|
@@ -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
|