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,110 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { BaseEvaluator, EvaluatorResult } = require('./evaluator')
|
|
4
|
+
const { hasEntries } = require('./util')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{value?: unknown, reasoning?: string, assessment?: string, status?: string}} RemoteEvaluatorResponse
|
|
8
|
+
* @typedef {{evaluatorInfer: (evalName: string, context: object) => Promise<RemoteEvaluatorResponse>}}
|
|
9
|
+
* RemoteEvaluatorClient
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {import('./evaluator').EvaluatorContext} context
|
|
14
|
+
*/
|
|
15
|
+
function defaultContextTransform (context) {
|
|
16
|
+
const transformed = {
|
|
17
|
+
span_input: context.inputData,
|
|
18
|
+
span_output: context.outputData,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const meta = {}
|
|
22
|
+
let hasMeta = false
|
|
23
|
+
if (context.expectedOutput != null) {
|
|
24
|
+
meta.expected_output = context.expectedOutput
|
|
25
|
+
hasMeta = true
|
|
26
|
+
}
|
|
27
|
+
if (hasEntries(context.metadata)) {
|
|
28
|
+
meta.metadata = context.metadata
|
|
29
|
+
hasMeta = true
|
|
30
|
+
}
|
|
31
|
+
if (hasMeta) transformed.meta = meta
|
|
32
|
+
|
|
33
|
+
if (context.spanId) transformed.span_id = context.spanId
|
|
34
|
+
if (context.traceId) transformed.trace_id = context.traceId
|
|
35
|
+
|
|
36
|
+
return transformed
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class RemoteEvaluatorResult extends EvaluatorResult {
|
|
40
|
+
/**
|
|
41
|
+
* @param {unknown} value
|
|
42
|
+
* @param {{reasoning?: string, assessment?: string, status?: string}} [options]
|
|
43
|
+
*/
|
|
44
|
+
constructor (value, { reasoning, assessment, status } = {}) {
|
|
45
|
+
super(value, { reasoning, assessment })
|
|
46
|
+
this.status = status
|
|
47
|
+
this.evalSourceType = 'managed'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class RemoteEvaluatorError extends Error {
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} message
|
|
54
|
+
* @param {{status?: string, backendError?: object}} [options]
|
|
55
|
+
*/
|
|
56
|
+
constructor (message, { status = 'ERROR', backendError = {} } = {}) {
|
|
57
|
+
super(message)
|
|
58
|
+
this.name = 'RemoteEvaluatorError'
|
|
59
|
+
this.status = status
|
|
60
|
+
this.backendError = backendError
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Evaluator that references an LLM-as-a-judge evaluator configured in Datadog.
|
|
66
|
+
*/
|
|
67
|
+
class RemoteEvaluator extends BaseEvaluator {
|
|
68
|
+
#evalName
|
|
69
|
+
#transformFn
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {{evalName: string, transformFn?: (context: import('./evaluator').EvaluatorContext) => object}} options
|
|
73
|
+
*/
|
|
74
|
+
constructor (options) {
|
|
75
|
+
super()
|
|
76
|
+
const { evalName, transformFn } = options ?? {}
|
|
77
|
+
if (typeof evalName !== 'string' || evalName.trim() === '') {
|
|
78
|
+
throw new TypeError('evalName must be a non-empty string')
|
|
79
|
+
}
|
|
80
|
+
if (transformFn !== undefined && typeof transformFn !== 'function') {
|
|
81
|
+
throw new TypeError('transformFn must be a function')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.name = evalName.trim()
|
|
85
|
+
this.#evalName = this.name
|
|
86
|
+
this.#transformFn = transformFn ?? defaultContextTransform
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {import('./evaluator').EvaluatorContext} context
|
|
91
|
+
* @param {RemoteEvaluatorClient} [client]
|
|
92
|
+
*/
|
|
93
|
+
evaluate (context, client) {
|
|
94
|
+
if (client === undefined || typeof client.evaluatorInfer !== 'function') {
|
|
95
|
+
throw new Error('RemoteEvaluator can only be evaluated as part of an experiment')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const transformed = this.#transformFn(context)
|
|
99
|
+
return client.evaluatorInfer(this.#evalName, transformed).then(result => new RemoteEvaluatorResult(
|
|
100
|
+
result?.value,
|
|
101
|
+
{
|
|
102
|
+
reasoning: result?.reasoning,
|
|
103
|
+
assessment: result?.assessment,
|
|
104
|
+
status: result?.status,
|
|
105
|
+
}
|
|
106
|
+
))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { RemoteEvaluator, RemoteEvaluatorError }
|
|
@@ -62,6 +62,7 @@ function generateRunId () {
|
|
|
62
62
|
function validateEvaluatorName (name) {
|
|
63
63
|
if (typeof name !== 'string') throw new TypeError('Evaluator name must be a string')
|
|
64
64
|
if (name.length === 0) throw new Error('Evaluator name cannot be empty')
|
|
65
|
+
if (name === '__proto__') throw new Error("Evaluator name '__proto__' is reserved")
|
|
65
66
|
if (!EVALUATOR_NAME_PATTERN.test(name)) {
|
|
66
67
|
throw new Error(
|
|
67
68
|
`Evaluator name '${name}' is invalid. Name must contain only alphanumeric characters, underscores, and hyphens.`
|
|
@@ -77,10 +78,34 @@ function functionName (fn, fallback) {
|
|
|
77
78
|
return typeof fn.name === 'string' && fn.name.length > 0 ? fn.name : fallback
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* @param {unknown} evaluator
|
|
83
|
+
* @param {string} kind
|
|
84
|
+
*/
|
|
85
|
+
function isClassEvaluator (evaluator, kind) {
|
|
86
|
+
// Lazy loading avoids a cycle: evaluator.js uses validateEvaluatorName from this module.
|
|
87
|
+
const { BaseEvaluator, BaseSummaryEvaluator } = require('./evaluator')
|
|
88
|
+
return kind === 'summary'
|
|
89
|
+
? evaluator instanceof BaseSummaryEvaluator
|
|
90
|
+
: evaluator instanceof BaseEvaluator
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {unknown} evaluator
|
|
95
|
+
* @param {string} kind
|
|
96
|
+
* @param {number} index
|
|
97
|
+
*/
|
|
98
|
+
function evaluatorName (evaluator, kind, index) {
|
|
99
|
+
if (isClassEvaluator(evaluator, kind)) return evaluator.name
|
|
100
|
+
if (typeof evaluator === 'function') return functionName(evaluator, `${kind}_evaluator_${index}`)
|
|
101
|
+
const baseName = kind === 'summary' ? 'BaseSummaryEvaluator' : 'BaseEvaluator'
|
|
102
|
+
throw new TypeError(`${kind} evaluator must be a function or a ${baseName} instance`)
|
|
103
|
+
}
|
|
104
|
+
|
|
80
105
|
/**
|
|
81
106
|
* @param {unknown} evaluators
|
|
82
107
|
* @param {string} kind
|
|
83
|
-
* @returns {Array<[string,
|
|
108
|
+
* @returns {Array<[string, Function | object]>}
|
|
84
109
|
*/
|
|
85
110
|
function normalizeEvaluators (evaluators, kind) {
|
|
86
111
|
if (evaluators == null) return []
|
|
@@ -90,8 +115,7 @@ function normalizeEvaluators (evaluators, kind) {
|
|
|
90
115
|
const indexesByName = new Map()
|
|
91
116
|
for (let i = 0; i < evaluators.length; i++) {
|
|
92
117
|
const evaluator = evaluators[i]
|
|
93
|
-
|
|
94
|
-
const name = functionName(evaluator, `${kind}_evaluator_${i}`)
|
|
118
|
+
const name = evaluatorName(evaluator, kind, i)
|
|
95
119
|
validateEvaluatorName(name)
|
|
96
120
|
if (indexesByName.has(name)) {
|
|
97
121
|
log.warn('Duplicate %s evaluator name %s; previous evaluator will be overwritten', kind, name)
|
|
@@ -104,13 +128,17 @@ function normalizeEvaluators (evaluators, kind) {
|
|
|
104
128
|
return normalized
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
if (typeof evaluators !== 'object') {
|
|
108
|
-
throw new TypeError(
|
|
131
|
+
if (typeof evaluators !== 'object' || evaluators === null) {
|
|
132
|
+
throw new TypeError(
|
|
133
|
+
`${kind} evaluators must be an array of functions or class instances, or an object keyed by evaluator name`
|
|
134
|
+
)
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
for (const [name, evaluator] of Object.entries(evaluators)) {
|
|
112
138
|
validateEvaluatorName(name)
|
|
113
|
-
if (typeof evaluator !== 'function')
|
|
139
|
+
if (!isClassEvaluator(evaluator, kind) && typeof evaluator !== 'function') {
|
|
140
|
+
throw new TypeError(`${kind} evaluator '${name}' must be a function or a class instance`)
|
|
141
|
+
}
|
|
114
142
|
normalized.push([name, evaluator])
|
|
115
143
|
}
|
|
116
144
|
return normalized
|
|
@@ -94,8 +94,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
94
94
|
if (!methodName) return // we will not trace all openai methods for llmobs
|
|
95
95
|
|
|
96
96
|
const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument
|
|
97
|
-
const response = ctx.result?.data // no result if error
|
|
98
|
-
const error = !!span.context().getTag('error')
|
|
97
|
+
const response = ctx.result?.data // no result if error, or if a stream ended before any response arrived
|
|
98
|
+
const error = !!span.context().getTag('error') || response == null
|
|
99
99
|
|
|
100
100
|
const operation = getOperation(methodName)
|
|
101
101
|
|
|
@@ -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,
|
|
@@ -182,8 +184,13 @@ class LLMObsSpanProcessor {
|
|
|
182
184
|
}
|
|
183
185
|
|
|
184
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)
|
|
185
190
|
|
|
186
|
-
if (
|
|
191
|
+
if (hasExperimentInput) {
|
|
192
|
+
llmObsSpan.input = [{ role: '', content: mlObsTags[EXPERIMENT_INPUT] }]
|
|
193
|
+
} else if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
|
|
187
194
|
llmObsSpan.input = mlObsTags[INPUT_MESSAGES]
|
|
188
195
|
inputType = 'messages'
|
|
189
196
|
} else if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
|
|
@@ -194,7 +201,9 @@ class LLMObsSpanProcessor {
|
|
|
194
201
|
inputType = 'value'
|
|
195
202
|
}
|
|
196
203
|
|
|
197
|
-
if (
|
|
204
|
+
if (hasExperimentOutput) {
|
|
205
|
+
llmObsSpan.output = [{ role: '', content: mlObsTags[EXPERIMENT_OUTPUT] }]
|
|
206
|
+
} else if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
|
|
198
207
|
llmObsSpan.output = mlObsTags[OUTPUT_MESSAGES]
|
|
199
208
|
outputType = 'messages'
|
|
200
209
|
} else if (spanKind === 'retrieval' && mlObsTags[OUTPUT_DOCUMENTS]) {
|
|
@@ -226,34 +235,41 @@ class LLMObsSpanProcessor {
|
|
|
226
235
|
const processedSpan = this.#runProcessor(llmObsSpan)
|
|
227
236
|
if (processedSpan === undefined) return null
|
|
228
237
|
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
})
|
|
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
|
+
}
|
|
239
255
|
}
|
|
240
|
-
}
|
|
241
256
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}
|
|
252
268
|
}
|
|
253
|
-
}
|
|
254
269
|
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
meta.input = input
|
|
271
|
+
meta.output = output
|
|
272
|
+
}
|
|
257
273
|
|
|
258
274
|
const prompt = mlObsTags[INPUT_PROMPT]
|
|
259
275
|
if (prompt && spanKind === 'llm') {
|
|
@@ -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,
|
|
@@ -273,6 +275,18 @@ class LLMObsTagger {
|
|
|
273
275
|
this.#tagDocuments(span, outputData, OUTPUT_DOCUMENTS)
|
|
274
276
|
}
|
|
275
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
|
+
|
|
276
290
|
tagTextIO (span, inputData, outputData) {
|
|
277
291
|
this.#tagText(span, inputData, INPUT_VALUE)
|
|
278
292
|
this.#tagText(span, outputData, OUTPUT_VALUE)
|
|
@@ -562,6 +576,27 @@ class LLMObsTagger {
|
|
|
562
576
|
}
|
|
563
577
|
}
|
|
564
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
|
+
|
|
565
600
|
#tagDocuments (span, data, key) {
|
|
566
601
|
if (!data) {
|
|
567
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
|
}
|