dd-trace 6.19.0 → 6.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/index.d.ts +163 -22
  2. package/package.json +2 -2
  3. package/packages/datadog-esbuild/index.js +9 -6
  4. package/packages/datadog-instrumentations/src/ai.js +26 -0
  5. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  9. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  10. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  11. package/packages/dd-trace/src/config/remote_config.js +12 -0
  12. package/packages/dd-trace/src/config/supported-configurations.json +21 -0
  13. package/packages/dd-trace/src/debugger/constants.js +13 -4
  14. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  15. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  16. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  17. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  18. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  19. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  20. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  21. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  22. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  23. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  24. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  25. package/packages/dd-trace/src/debugger/index.js +60 -28
  26. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  27. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  28. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  29. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  30. package/packages/dd-trace/src/knuth-hash.js +17 -0
  31. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  32. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  33. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  34. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  35. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  36. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  37. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  38. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  39. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +2 -2
  40. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  41. package/packages/dd-trace/src/llmobs/span_processor.js +42 -26
  42. package/packages/dd-trace/src/llmobs/tagger.js +35 -0
  43. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  44. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  45. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  46. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  47. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  48. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  49. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  50. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  51. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  52. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  53. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  54. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  55. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  56. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  57. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  58. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  59. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  60. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  61. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  62. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  63. package/packages/dd-trace/src/opentracing/span.js +14 -9
  64. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  65. package/packages/dd-trace/src/otel-sampling.js +171 -0
  66. package/packages/dd-trace/src/plugins/index.js +0 -1
  67. package/packages/dd-trace/src/priority_sampler.js +155 -33
  68. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  69. package/packages/dd-trace/src/sampler.js +2 -5
  70. package/packages/dd-trace/src/sampling_rule.js +4 -8
  71. package/packages/dd-trace/src/span_format.js +6 -0
  72. package/packages/dd-trace/src/span_processor.js +26 -2
  73. package/packages/dd-trace/src/standalone/index.js +5 -4
  74. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  75. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  76. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  77. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -20,28 +20,62 @@ const ddTraceGlobal = /** @type {Record<symbol, SharedArrayBuffer | object | und
20
20
  /** @type {Record<symbol, unknown>} */ (globalThis)[Symbol.for(DD_TRACE_SYMBOL)]
21
21
  )
22
22
 
23
+ /**
24
+ * @typedef {object} ProbeThrottle
25
+ * @property {bigint} untilNs - The probe is skipped at entry until this point in time
26
+ * @property {boolean} timedOut - Whether the throttle was caused by an evaluation exceeding its time budget
27
+ * @property {string | undefined} error - A condition error not yet handed over to the worker
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} ProbeSamplerConfig
32
+ * @property {{ DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS: number }} dynamicInstrumentation
33
+ */
34
+
35
+ let evaluationTimeoutNs = 0n
36
+
23
37
  module.exports = {
38
+ configureProbeSampler,
24
39
  installProbeSampler,
25
40
  uninstallProbeSampler,
26
41
  }
27
42
 
43
+ /**
44
+ * Apply the evaluation time budget from the tracer configuration.
45
+ *
46
+ * @param {ProbeSamplerConfig} config - The tracer configuration.
47
+ */
48
+ function configureProbeSampler (config) {
49
+ evaluationTimeoutNs = BigInt(config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS) *
50
+ 1_000_000n
51
+ }
52
+
28
53
  /**
29
54
  * Install the runtime sampler in the debuggee context.
30
55
  *
31
56
  * @param {import('./guardrail-metrics').GuardrailMetrics} guardrailMetrics - Counters for skipped probe hits.
57
+ * @param {ProbeSamplerConfig} config - The tracer configuration.
32
58
  * @returns {SharedArrayBuffer} The shared sampler buffer to pass to the debugger worker.
33
59
  */
34
- function installProbeSampler (guardrailMetrics) {
60
+ function installProbeSampler (guardrailMetrics, config) {
61
+ configureProbeSampler(config)
62
+
35
63
  const buffer = createProbeSamplerBuffer()
36
64
 
37
65
  const lastCaptureNsByProbeId = new Map()
38
66
  /**
39
- * Probes whose condition recently failed to evaluate, keyed by probe id. The error is kept until the worker picks it
40
- * up for the error result.
67
+ * Probes that are skipped at entry because a recent evaluation failed or exceeded its time budget. One error result
68
+ * is reported per throttle window, so the probe stays visible without repeatedly paying for the evaluation.
41
69
  *
42
- * @type {Map<string, { throttledUntilNs: bigint, error: string | undefined }>}
70
+ * @type {Map<string, ProbeThrottle>}
71
+ */
72
+ const throttleByProbeId = new Map()
73
+ /**
74
+ * When the condition currently being evaluated started. V8 doesn't evaluate breakpoint conditions re-entrantly, and
75
+ * the conditions of probes at the same location are evaluated one after another, so one start time is enough. It is
76
+ * kept here rather than in the compiled condition so the condition can't shadow a variable of the paused frame.
43
77
  */
44
- const conditionErrorByProbeId = new Map()
78
+ let conditionStartNs = 0n
45
79
  const sampledProbeIndexes = new Int32Array(buffer)
46
80
  Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_COUNT_INDEX, 0)
47
81
  Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_OVERFLOW_INDEX, 0)
@@ -52,7 +86,7 @@ function installProbeSampler (guardrailMetrics) {
52
86
 
53
87
  ddTraceGlobal[Symbol.for(PROBE_SAMPLER_SYMBOL)] = {
54
88
  /**
55
- * Decide if a probe should be sampled and store sampled probe indexes for the debugger worker.
89
+ * Decide if a probe without a condition should be sampled and store sampled probe indexes for the debugger worker.
56
90
  *
57
91
  * @param {number} probeIndex - The worker-side probe sampling index.
58
92
  * @param {string} probeId - The probe id.
@@ -61,55 +95,46 @@ function installProbeSampler (guardrailMetrics) {
61
95
  */
62
96
  makeSampleDecision (probeIndex, probeId, nsBetweenSampling, isSnapshotProducingProbe) {
63
97
  const now = process.hrtime.bigint()
64
- const lastCaptureNs = lastCaptureNsByProbeId.get(probeId)
65
- if (lastCaptureNs !== undefined && now - lastCaptureNs < nsBetweenSampling) {
66
- guardrailMetrics.eventSkipped(
67
- SKIPPED_REASON.RATE_LIMIT_PROBE,
68
- isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
69
- )
70
- return false
71
- }
72
-
73
- let shouldResetGlobalSnapshotRateWindow = false
74
- if (isSnapshotProducingProbe === true) {
75
- if (now - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
76
- shouldResetGlobalSnapshotRateWindow = true
77
- } else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
78
- guardrailMetrics.eventSkipped(SKIPPED_REASON.RATE_LIMIT_GLOBAL, EVENT_TYPE.SNAPSHOT)
79
- return false
80
- }
81
- }
82
-
83
- if (!storeSampledProbeIndex(probeIndex)) return false
84
-
85
- if (isSnapshotProducingProbe === true) {
86
- if (shouldResetGlobalSnapshotRateWindow === true) {
87
- snapshotsSampledWithinTheLastSecond = 1
88
- globalSnapshotSamplingRateWindowStart = now
89
- } else {
90
- snapshotsSampledWithinTheLastSecond++
91
- }
92
- }
98
+ if (isThrottled(probeId, now, isSnapshotProducingProbe)) return false
99
+ return sample(probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe)
100
+ },
93
101
 
94
- lastCaptureNsByProbeId.set(probeId, now)
102
+ /**
103
+ * Decide if a probe's condition should be evaluated, or skipped because a recent evaluation error throttled it. If
104
+ * it should be evaluated, this starts timing the evaluation against the evaluation time budget.
105
+ *
106
+ * @param {string} probeId - The probe id.
107
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe produces snapshots.
108
+ */
109
+ shouldEvaluateCondition (probeId, isSnapshotProducingProbe) {
110
+ const now = process.hrtime.bigint()
111
+ if (isThrottled(probeId, now, isSnapshotProducingProbe)) return false
112
+ conditionStartNs = now
95
113
  return true
96
114
  },
97
115
 
98
116
  /**
99
- * Decide if a probe's condition should be evaluated, or skipped because a recent evaluation error throttled it.
117
+ * Decide if a probe should be sampled now that its condition has been evaluated. A condition that exceeded the
118
+ * evaluation time budget is reported like a condition error instead, regardless of its result.
100
119
  *
120
+ * @param {number} probeIndex - The worker-side probe sampling index.
101
121
  * @param {string} probeId - The probe id.
122
+ * @param {boolean} matched - Whether the condition evaluated to `true`.
123
+ * @param {bigint} nsBetweenSampling - Minimum nanoseconds between samples for this probe.
124
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe counts toward the global snapshot sample limit.
102
125
  */
103
- shouldEvaluateCondition (probeId) {
104
- const state = conditionErrorByProbeId.get(probeId)
105
- if (state === undefined) return true
106
- if (process.hrtime.bigint() < state.throttledUntilNs) return false
107
- conditionErrorByProbeId.delete(probeId)
108
- return true
126
+ conditionEvaluated (probeIndex, probeId, matched, nsBetweenSampling, isSnapshotProducingProbe) {
127
+ const now = process.hrtime.bigint()
128
+ const elapsedNs = now - conditionStartNs
129
+ if (elapsedNs > evaluationTimeoutNs) {
130
+ return recordConditionError(probeIndex, probeId, now, describeTimeout(elapsedNs), true)
131
+ }
132
+ return matched === true && sample(probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe)
109
133
  },
110
134
 
111
135
  /**
112
- * Record that a probe's condition threw, throttle the probe, and request a pause if the error can be reported.
136
+ * Record that a probe's condition threw, throttle the probe, and request a pause if the error can be reported. A
137
+ * condition that exceeded the evaluation time budget before throwing is reported as a timeout instead.
113
138
  *
114
139
  * Error results bypass the per-probe and global rate limits: they are rate limited by the throttle instead, which
115
140
  * allows one error result per probe per window. If the shared buffer is full, the error is dropped but the throttle
@@ -120,12 +145,12 @@ function installProbeSampler (guardrailMetrics) {
120
145
  * @param {unknown} error - The value thrown by the condition.
121
146
  */
122
147
  conditionError (probeIndex, probeId, error) {
123
- const stored = storeSampledProbeIndex(probeIndex | CONDITION_ERROR_FLAG)
124
- conditionErrorByProbeId.set(probeId, {
125
- throttledUntilNs: process.hrtime.bigint() + CONDITION_ERROR_THROTTLE_NS,
126
- error: stored ? describeError(error) : undefined,
127
- })
128
- return stored
148
+ const now = process.hrtime.bigint()
149
+ const elapsedNs = now - conditionStartNs
150
+ if (elapsedNs > evaluationTimeoutNs) {
151
+ return recordConditionError(probeIndex, probeId, now, describeTimeout(elapsedNs), true)
152
+ }
153
+ return recordConditionError(probeIndex, probeId, now, describeError(error), false)
129
154
  },
130
155
 
131
156
  /**
@@ -135,13 +160,28 @@ function installProbeSampler (guardrailMetrics) {
135
160
  * @returns {string | undefined} The error description, if any.
136
161
  */
137
162
  takeConditionError (probeId) {
138
- const state = conditionErrorByProbeId.get(probeId)
163
+ const state = throttleByProbeId.get(probeId)
139
164
  if (state === undefined) return
140
165
  const { error } = state
141
166
  state.error = undefined
142
167
  return error
143
168
  },
144
169
 
170
+ /**
171
+ * Throttle a probe whose evaluation exceeded its time budget in the worker, e.g. while evaluating its template.
172
+ * Called by the worker after the result has been reported.
173
+ *
174
+ * @param {string} probeId - The probe id.
175
+ */
176
+ evaluationTimedOut (probeId) {
177
+ throttleByProbeId.set(probeId, {
178
+ untilNs: process.hrtime.bigint() + CONDITION_ERROR_THROTTLE_NS,
179
+ timedOut: true,
180
+ // A hit that raced this request may have recorded a condition error that its pause has yet to report
181
+ error: throttleByProbeId.get(probeId)?.error,
182
+ })
183
+ },
184
+
145
185
  /**
146
186
  * Remove cached sampling state for a probe.
147
187
  *
@@ -149,10 +189,100 @@ function installProbeSampler (guardrailMetrics) {
149
189
  */
150
190
  remove (probeId) {
151
191
  lastCaptureNsByProbeId.delete(probeId)
152
- conditionErrorByProbeId.delete(probeId)
192
+ throttleByProbeId.delete(probeId)
153
193
  },
154
194
  }
155
195
 
196
+ /**
197
+ * Check if a probe is skipped at entry because of a recent evaluation error, recording the skip when the error was
198
+ * an exceeded time budget.
199
+ *
200
+ * @param {string} probeId - The probe id.
201
+ * @param {bigint | undefined} now - The current time, if already available.
202
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe produces snapshots.
203
+ */
204
+ function isThrottled (probeId, now, isSnapshotProducingProbe) {
205
+ const state = throttleByProbeId.get(probeId)
206
+ if (state === undefined) return false
207
+ now ??= process.hrtime.bigint()
208
+ if (now >= state.untilNs) {
209
+ throttleByProbeId.delete(probeId)
210
+ return false
211
+ }
212
+ if (state.timedOut) {
213
+ guardrailMetrics.eventSkipped(
214
+ SKIPPED_REASON.EVALUATION_TIMEOUT,
215
+ isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
216
+ )
217
+ }
218
+ return true
219
+ }
220
+
221
+ /**
222
+ * Apply the per-probe and global rate limits and store the sampled probe index for the debugger worker.
223
+ *
224
+ * @param {number} probeIndex - The worker-side probe sampling index.
225
+ * @param {string} probeId - The probe id.
226
+ * @param {bigint} now - The current time.
227
+ * @param {bigint} nsBetweenSampling - Minimum nanoseconds between samples for this probe.
228
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe counts toward the global snapshot sample limit.
229
+ */
230
+ function sample (probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe) {
231
+ const lastCaptureNs = lastCaptureNsByProbeId.get(probeId)
232
+ if (lastCaptureNs !== undefined && now - lastCaptureNs < nsBetweenSampling) {
233
+ guardrailMetrics.eventSkipped(
234
+ SKIPPED_REASON.RATE_LIMIT_PROBE,
235
+ isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
236
+ )
237
+ return false
238
+ }
239
+
240
+ let shouldResetGlobalSnapshotRateWindow = false
241
+ if (isSnapshotProducingProbe === true) {
242
+ if (now - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
243
+ shouldResetGlobalSnapshotRateWindow = true
244
+ } else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
245
+ guardrailMetrics.eventSkipped(SKIPPED_REASON.RATE_LIMIT_GLOBAL, EVENT_TYPE.SNAPSHOT)
246
+ return false
247
+ }
248
+ }
249
+
250
+ if (!storeSampledProbeIndex(probeIndex)) return false
251
+
252
+ if (isSnapshotProducingProbe === true) {
253
+ if (shouldResetGlobalSnapshotRateWindow === true) {
254
+ snapshotsSampledWithinTheLastSecond = 1
255
+ globalSnapshotSamplingRateWindowStart = now
256
+ } else {
257
+ snapshotsSampledWithinTheLastSecond++
258
+ }
259
+ }
260
+
261
+ lastCaptureNsByProbeId.set(probeId, now)
262
+ return true
263
+ }
264
+
265
+ /**
266
+ * Throttle a probe whose condition failed and request a pause so the error can be reported. Error results bypass the
267
+ * per-probe and global rate limits: they are rate limited by the throttle instead, which allows one error result per
268
+ * probe per window.
269
+ *
270
+ * @param {number} probeIndex - The worker-side probe sampling index.
271
+ * @param {string} probeId - The probe id.
272
+ * @param {bigint} now - The current time.
273
+ * @param {string} error - The error description.
274
+ * @param {boolean} timedOut - Whether the error is an exceeded time budget.
275
+ */
276
+ function recordConditionError (probeIndex, probeId, now, error, timedOut) {
277
+ const stored = storeSampledProbeIndex(probeIndex | CONDITION_ERROR_FLAG)
278
+ throttleByProbeId.set(probeId, {
279
+ untilNs: now + CONDITION_ERROR_THROTTLE_NS,
280
+ timedOut,
281
+ error: stored ? error : undefined,
282
+ })
283
+ return stored
284
+ }
285
+
156
286
  /**
157
287
  * Hand a sampled probe index over to the worker for the upcoming pause.
158
288
  *
@@ -224,6 +354,16 @@ function getErrorStringProperty (error, property) {
224
354
  }
225
355
  }
226
356
 
357
+ /**
358
+ * Describe a condition evaluation that exceeded its time budget.
359
+ *
360
+ * @param {bigint} elapsedNs - The time the evaluation took.
361
+ */
362
+ function describeTimeout (elapsedNs) {
363
+ return `Condition evaluation exceeded its time budget of ${evaluationTimeoutNs / 1_000_000n}ms ` +
364
+ `(took ${(Number(elapsedNs) / 1_000_000).toFixed(1)}ms)`
365
+ }
366
+
227
367
  /**
228
368
  * Create the shared buffer used to hand sampled probe indexes from breakpoint conditions to the debugger worker.
229
369
  *
@@ -0,0 +1,139 @@
1
+ 'use strict'
2
+
3
+ // Used by both the main thread, which renders log template values, and the devtools client worker, which captures
4
+ // snapshots and compiles probe expressions, so that both redact the same identifiers.
5
+
6
+ // What a redacted value is rendered as in a log message.
7
+ const REDACTED_PLACEHOLDER = '{redacted}'
8
+
9
+ const DEFAULT_REDACTED_IDENTIFIERS = [
10
+ '2fa',
11
+ '_csrf',
12
+ '_csrf_token',
13
+ '_session',
14
+ '_xsrf',
15
+ 'access_token',
16
+ 'aiohttp_session',
17
+ 'api_key',
18
+ 'apisecret',
19
+ 'apisignature',
20
+ 'applicationkey',
21
+ 'appkey',
22
+ 'auth',
23
+ 'authtoken',
24
+ 'authorization',
25
+ 'cc_number',
26
+ 'certificatepin',
27
+ 'cipher',
28
+ 'client_secret',
29
+ 'clientid',
30
+ 'connect.sid',
31
+ 'connectionstring',
32
+ 'cookie',
33
+ 'credentials',
34
+ 'creditcard',
35
+ 'csrf',
36
+ 'csrf_token',
37
+ 'cvv',
38
+ 'databaseurl',
39
+ 'db_url',
40
+ 'encryption_key',
41
+ 'encryptionkeyid',
42
+ 'geo_location',
43
+ 'gpg_key',
44
+ 'ip_address',
45
+ 'jti',
46
+ 'jwt',
47
+ 'license_key',
48
+ 'masterkey',
49
+ 'mysql_pwd',
50
+ 'nonce',
51
+ 'oauth',
52
+ 'oauthtoken',
53
+ 'otp',
54
+ 'passhash',
55
+ 'passwd',
56
+ 'password',
57
+ 'passwordb',
58
+ 'pem_file',
59
+ 'pgp_key',
60
+ 'PHPSESSID',
61
+ 'pin',
62
+ 'pincode',
63
+ 'pkcs8',
64
+ 'private_key',
65
+ 'publickey',
66
+ 'pwd',
67
+ 'recaptcha_key',
68
+ 'refresh_token',
69
+ 'routingnumber',
70
+ 'salt',
71
+ 'secret',
72
+ 'secretKey',
73
+ 'secrettoken',
74
+ 'securitycode',
75
+ 'security_answer',
76
+ 'security_question',
77
+ 'serviceaccountcredentials',
78
+ 'session',
79
+ 'sessionid',
80
+ 'sessionkey',
81
+ 'set_cookie',
82
+ 'signature',
83
+ 'signaturekey',
84
+ 'ssh_key',
85
+ 'ssn',
86
+ 'symfony',
87
+ 'token',
88
+ 'transactionid',
89
+ 'twilio_token',
90
+ 'user_session',
91
+ 'voterid',
92
+ 'x-auth-token',
93
+ 'x_api_key',
94
+ 'x_csrftoken',
95
+ 'x_forwarded_for',
96
+ 'x_real_ip',
97
+ 'XSRF-TOKEN',
98
+ ]
99
+
100
+ module.exports = {
101
+ createIsRedactedIdentifier,
102
+ REDACTED_PLACEHOLDER,
103
+ }
104
+
105
+ /**
106
+ * Create a predicate for whether the value of an identifier must be redacted, based on the default list of redacted
107
+ * identifiers and the user's configuration.
108
+ *
109
+ * @param {object} config - The `dynamicInstrumentation` config.
110
+ * @param {string[]} config.DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS - Identifiers to redact in addition to the
111
+ * defaults.
112
+ * @param {string[]} config.DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS - Identifiers to never redact.
113
+ * @returns {(name: string, isSymbol?: boolean) => boolean} Whether the identifier must be redacted. If `isSymbol` is
114
+ * `true`, `name` is a `Symbol(...)` description.
115
+ */
116
+ function createIsRedactedIdentifier ({
117
+ DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS: redactedIdentifiers,
118
+ DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS: excludedIdentifiers,
119
+ }) {
120
+ const excluded = new Set(excludedIdentifiers.map((name) => normalizeName(name)))
121
+ const redacted = new Set()
122
+ for (const name of [...DEFAULT_REDACTED_IDENTIFIERS, ...redactedIdentifiers]) {
123
+ const normalized = normalizeName(name)
124
+ if (!excluded.has(normalized)) redacted.add(normalized)
125
+ }
126
+
127
+ return function isRedactedIdentifier (name, isSymbol) {
128
+ return redacted.has(normalizeName(name, isSymbol))
129
+ }
130
+ }
131
+
132
+ /**
133
+ * @param {string} name - The identifier name.
134
+ * @param {boolean} [isSymbol] - Whether the name is a `Symbol(...)` description.
135
+ */
136
+ function normalizeName (name, isSymbol) {
137
+ if (isSymbol) name = name.slice(7, -1) // Remove `Symbol(` and `)`
138
+ return name.toLowerCase().replaceAll(/[-_@$.]/g, '')
139
+ }
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const UINT64_MODULO = 2n ** 64n
4
+
5
+ // Knuth's factor for the sampling algorithm shared across Datadog tracers.
6
+ const SAMPLING_KNUTH_FACTOR = 1_111_111_111_111_111_111n
7
+
8
+ /**
9
+ * Hashes the lower 64 bits of a trace ID for deterministic trace sampling.
10
+ *
11
+ * @param {bigint} traceId
12
+ */
13
+ function knuthHash (traceId) {
14
+ return (traceId * SAMPLING_KNUTH_FACTOR) % UINT64_MODULO
15
+ }
16
+
17
+ module.exports = knuthHash
@@ -54,11 +54,13 @@ module.exports = {
54
54
  DEFAULT_MODEL: 'custom',
55
55
 
56
56
  INPUT_DOCUMENTS: '_ml_obs.meta.input.documents',
57
+ EXPERIMENT_INPUT: '_ml_obs.meta.input',
57
58
  INPUT_MESSAGES: '_ml_obs.meta.input.messages',
58
59
  INPUT_VALUE: '_ml_obs.meta.input.value',
59
60
  INPUT_PROMPT: '_ml_obs.meta.input.prompt',
60
61
 
61
62
  OUTPUT_DOCUMENTS: '_ml_obs.meta.output.documents',
63
+ EXPERIMENT_OUTPUT: '_ml_obs.meta.output',
62
64
  OUTPUT_MESSAGES: '_ml_obs.meta.output.messages',
63
65
  OUTPUT_VALUE: '_ml_obs.meta.output.value',
64
66
 
@@ -5,8 +5,23 @@
5
5
 
6
6
  const { Dataset, DatasetRecord } = require('./dataset')
7
7
  const { ExperimentResult } = require('./result')
8
+ const { RemoteEvaluatorError } = require('./remote-evaluator')
8
9
 
9
10
  const API_BASE_PATH = '/api/v2/llm-obs/v1'
11
+ const EVALUATOR_INFERENCE_API_BASE_PATH = '/api/unstable/llm-obs/v1'
12
+
13
+ class ExperimentsRequestError extends Error {
14
+ /**
15
+ * @param {string} message
16
+ * @param {number} statusCode
17
+ * @param {object} responseBody
18
+ */
19
+ constructor (message, statusCode, responseBody) {
20
+ super(message)
21
+ this.statusCode = statusCode
22
+ this.responseBody = responseBody
23
+ }
24
+ }
10
25
 
11
26
  // Control-plane host for a Datadog site, e.g.
12
27
  // datadoghq.com -> api.datadoghq.com
@@ -151,7 +166,15 @@ class ExperimentsClient {
151
166
 
152
167
  const text = await response.text()
153
168
  if (!response.ok) {
154
- throw new Error(`${method} ${path} failed: HTTP ${response.status} ${text}`)
169
+ let responseBody = {}
170
+ try {
171
+ responseBody = text ? JSON.parse(text) : {}
172
+ } catch {}
173
+ throw new ExperimentsRequestError(
174
+ `${method} ${path} failed: HTTP ${response.status} ${text}`,
175
+ response.status,
176
+ responseBody
177
+ )
155
178
  }
156
179
  return text ? JSON.parse(text) : {}
157
180
  }
@@ -247,6 +270,41 @@ class ExperimentsClient {
247
270
  return experimentFromResource(this, response?.data ?? null)
248
271
  }
249
272
 
273
+ /**
274
+ * @param {string} evalName
275
+ * @param {object} context
276
+ */
277
+ evaluatorInfer (evalName, context) {
278
+ const encodedName = encodeURIComponent(evalName)
279
+ const path = `${EVALUATOR_INFERENCE_API_BASE_PATH}/evaluators/${encodedName}/infer`
280
+ return this.jsonApiRequest('POST', path, 'evaluator_inference', { context }).then(response => {
281
+ const attributes = response?.data?.attributes ?? {}
282
+ return {
283
+ value: attributes.value,
284
+ reasoning: attributes.reasoning,
285
+ assessment: attributes.assessment,
286
+ status: attributes.status,
287
+ }
288
+ }).catch(err => {
289
+ const attributes = err.responseBody?.data?.attributes ?? {}
290
+ const backendError = attributes.error
291
+ if (backendError !== null && typeof backendError === 'object' && !Array.isArray(backendError)) {
292
+ const fallback = err.statusCode === undefined ? err.message : `HTTP ${err.statusCode}`
293
+ throw new RemoteEvaluatorError(
294
+ `Remote evaluator '${evalName}' failed: ${backendError.message ?? fallback}`,
295
+ { status: attributes.status, backendError }
296
+ )
297
+ }
298
+
299
+ const apiError = Array.isArray(err.responseBody?.errors) ? err.responseBody.errors[0] : undefined
300
+ const detail = apiError !== null && typeof apiError === 'object'
301
+ ? apiError.detail ?? apiError.title
302
+ : undefined
303
+ const fallback = err.statusCode === undefined ? err.message : `HTTP ${err.statusCode}`
304
+ throw new Error(`Failed to call evaluator '${evalName}': ${detail ?? fallback}`)
305
+ })
306
+ }
307
+
250
308
  postExperimentEvents (experimentId, attributes) {
251
309
  return this.jsonApiRequest(
252
310
  'POST',
@@ -278,4 +336,10 @@ class ExperimentsClient {
278
336
  }
279
337
  }
280
338
 
281
- module.exports = { ExperimentsClient, apiHost, appHost, API_BASE_PATH }
339
+ module.exports = {
340
+ ExperimentsClient,
341
+ apiHost,
342
+ appHost,
343
+ API_BASE_PATH,
344
+ EVALUATOR_INFERENCE_API_BASE_PATH,
345
+ }