dd-trace 5.86.0 → 5.87.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 +18 -3
- package/package.json +1 -1
- package/packages/datadog-instrumentations/src/cucumber.js +14 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +119 -1
- package/packages/datadog-instrumentations/src/jest.js +104 -4
- package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
- package/packages/datadog-instrumentations/src/mysql2.js +131 -64
- package/packages/datadog-instrumentations/src/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/stripe.js +92 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -0
- package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
- package/packages/dd-trace/src/appsec/addresses.js +11 -0
- package/packages/dd-trace/src/appsec/channels.js +5 -1
- package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
- package/packages/dd-trace/src/appsec/index.js +103 -0
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
- package/packages/dd-trace/src/config/defaults.js +2 -0
- package/packages/dd-trace/src/config/index.js +6 -0
- package/packages/dd-trace/src/config/supported-configurations.json +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
- package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
- package/packages/dd-trace/src/exporters/common/agents.js +1 -1
- package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +34 -5
- package/packages/dd-trace/src/plugins/database.js +42 -43
- package/packages/dd-trace/src/plugins/outbound.js +27 -2
- package/packages/dd-trace/src/plugins/tracing.js +39 -4
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
- package/packages/dd-trace/src/plugins/util/web.js +8 -7
- package/packages/dd-trace/src/startup-log.js +2 -2
- package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
|
@@ -6,7 +6,7 @@ const { NODE_MAJOR } = require('../../../../../version')
|
|
|
6
6
|
const processTags = require('../../process-tags')
|
|
7
7
|
const { breakpointToProbes } = require('./state')
|
|
8
8
|
const session = require('./session')
|
|
9
|
-
const { getLocalStateForCallFrame } = require('./snapshot')
|
|
9
|
+
const { getLocalStateForCallFrame, evaluateCaptureExpressions } = require('./snapshot')
|
|
10
10
|
const send = require('./send')
|
|
11
11
|
const { getStackFromCallFrames } = require('./state')
|
|
12
12
|
const { ackEmitting } = require('./status')
|
|
@@ -67,9 +67,13 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
67
67
|
throw new Error(`Unexpected Debugger.paused reason: ${params.reason}`)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
let maxReferenceDepth
|
|
70
|
+
let maxReferenceDepth = 0
|
|
71
|
+
let maxCollectionSize = 0
|
|
72
|
+
let maxFieldCount = 0
|
|
73
|
+
let maxLength = 0
|
|
71
74
|
let sampled = false
|
|
72
75
|
let numberOfProbesWithSnapshots = 0
|
|
76
|
+
let probesWithCaptureExpressions = false
|
|
73
77
|
const probes = []
|
|
74
78
|
let templateExpressions = ''
|
|
75
79
|
|
|
@@ -104,7 +108,7 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
104
108
|
continue
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
if (probe.captureSnapshot === true) {
|
|
111
|
+
if (probe.captureSnapshot === true || probe.compiledCaptureExpressions !== undefined) {
|
|
108
112
|
// This algorithm to calculate number of sampled snapshots within the last second is not perfect, as it's not a
|
|
109
113
|
// sliding window. But it's quick and easy :)
|
|
110
114
|
if (i === 0 && start - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
|
|
@@ -116,11 +120,15 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
116
120
|
snapshotsSampledWithinTheLastSecond++
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
if (probe.captureSnapshot === true) {
|
|
124
|
+
snapshotProbeIndex[numberOfProbesWithSnapshots++] = probes.length
|
|
125
|
+
maxReferenceDepth = Math.max(probe.capture.maxReferenceDepth, maxReferenceDepth)
|
|
126
|
+
maxCollectionSize = Math.max(probe.capture.maxCollectionSize, maxCollectionSize)
|
|
127
|
+
maxFieldCount = Math.max(probe.capture.maxFieldCount, maxFieldCount)
|
|
128
|
+
maxLength = Math.max(probe.capture.maxLength, maxLength)
|
|
129
|
+
} else {
|
|
130
|
+
probesWithCaptureExpressions = true
|
|
131
|
+
}
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
if (probe.condition !== undefined) {
|
|
@@ -173,16 +181,32 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
// TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
|
|
176
|
-
let processLocalState
|
|
184
|
+
let processLocalState
|
|
185
|
+
/** @type {Error[] | undefined} */
|
|
186
|
+
let fatalSnapshotErrors
|
|
177
187
|
if (numberOfProbesWithSnapshots !== 0) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
maxCollectionSize,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
const result = await getLocalStateForCallFrame(
|
|
189
|
+
params.callFrames[0],
|
|
190
|
+
{ maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength },
|
|
191
|
+
start + config.dynamicInstrumentation.captureTimeoutNs
|
|
192
|
+
)
|
|
193
|
+
processLocalState = result.processLocalState
|
|
194
|
+
fatalSnapshotErrors = result.fatalErrors
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Evaluate capture expressions for probes that have them
|
|
198
|
+
let captureExpressionResults = null
|
|
199
|
+
if (probesWithCaptureExpressions === true) {
|
|
200
|
+
captureExpressionResults = new Map()
|
|
201
|
+
for (const probe of probes) {
|
|
202
|
+
if (probe.compiledCaptureExpressions === undefined) continue
|
|
203
|
+
// eslint-disable-next-line no-await-in-loop
|
|
204
|
+
captureExpressionResults.set(probe.id, await evaluateCaptureExpressions(
|
|
205
|
+
params.callFrames[0],
|
|
206
|
+
probe.compiledCaptureExpressions,
|
|
207
|
+
start + config.dynamicInstrumentation.captureTimeoutNs
|
|
208
|
+
))
|
|
184
209
|
}
|
|
185
|
-
;({ processLocalState, captureErrors } = await getLocalStateForCallFrame(params.callFrames[0], opts))
|
|
186
210
|
}
|
|
187
211
|
|
|
188
212
|
await session.post('Debugger.resume')
|
|
@@ -228,16 +252,48 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
228
252
|
}
|
|
229
253
|
|
|
230
254
|
if (probe.captureSnapshot) {
|
|
231
|
-
if (
|
|
255
|
+
if (fatalSnapshotErrors && fatalSnapshotErrors.length > 0) {
|
|
232
256
|
// There was an error collecting the snapshot for this probe, let's not try again
|
|
233
257
|
probe.captureSnapshot = false
|
|
234
|
-
probe.permanentEvaluationErrors =
|
|
258
|
+
probe.permanentEvaluationErrors = fatalSnapshotErrors.map(error => ({
|
|
235
259
|
expr: '',
|
|
236
260
|
message: error.message,
|
|
237
261
|
}))
|
|
238
262
|
}
|
|
239
263
|
snapshot.captures = {
|
|
240
|
-
lines: { [probe.location.lines[0]]: { locals: processLocalState() } },
|
|
264
|
+
lines: { [probe.location.lines[0]]: { locals: /** @type {Function} */ (processLocalState)() } },
|
|
265
|
+
}
|
|
266
|
+
} else if (probe.compiledCaptureExpressions !== undefined) {
|
|
267
|
+
const expressionResult = /** @type {Map} */ (captureExpressionResults).get(probe.id)
|
|
268
|
+
if (expressionResult) {
|
|
269
|
+
// Handle fatal capture errors - disable capture expressions for this probe permanently
|
|
270
|
+
if (expressionResult.fatalErrors?.length > 0) {
|
|
271
|
+
probe.compiledCaptureExpressions = undefined
|
|
272
|
+
probe.permanentEvaluationErrors = expressionResult.fatalErrors.map(error => ({
|
|
273
|
+
expr: '',
|
|
274
|
+
message: error.message,
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
snapshot.captures = {
|
|
279
|
+
lines: { [probe.location.lines[0]]: { captureExpressions: expressionResult.processCaptureExpressions() } },
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Handle transient evaluation errors - include in snapshot for this capture
|
|
283
|
+
if (expressionResult.evaluationErrors?.length > 0) {
|
|
284
|
+
if (snapshot.evaluationErrors === undefined) {
|
|
285
|
+
snapshot.evaluationErrors = expressionResult.evaluationErrors
|
|
286
|
+
} else {
|
|
287
|
+
snapshot.evaluationErrors.push(...expressionResult.evaluationErrors)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
log.error('[debugger:devtools_client] Missing capture expression results for probe %s (version: %s)',
|
|
292
|
+
probe.id, probe.version)
|
|
293
|
+
snapshot.evaluationErrors = [{
|
|
294
|
+
expr: '',
|
|
295
|
+
message: 'Internal error: capture expression results not found',
|
|
296
|
+
}]
|
|
241
297
|
}
|
|
242
298
|
}
|
|
243
299
|
|
|
@@ -275,10 +331,6 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
275
331
|
}
|
|
276
332
|
})
|
|
277
333
|
|
|
278
|
-
function highestOrUndefined (num, max) {
|
|
279
|
-
return num === undefined ? max : Math.max(num, max ?? 0)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
334
|
function processDD (result) {
|
|
283
335
|
return result?.trace_id === undefined ? undefined : result
|
|
284
336
|
}
|
|
@@ -5,7 +5,7 @@ const { addBreakpoint, removeBreakpoint, modifyBreakpoint } = require('./breakpo
|
|
|
5
5
|
const { ackReceived, ackInstalled, ackError } = require('./status')
|
|
6
6
|
const log = require('./log')
|
|
7
7
|
|
|
8
|
-
// Example log line probe (simplified):
|
|
8
|
+
// Example log line probe with captureSnapshot (simplified):
|
|
9
9
|
// {
|
|
10
10
|
// id: '100c9a5c-45ad-49dc-818b-c570d31e11d1',
|
|
11
11
|
// version: 0,
|
|
@@ -19,6 +19,23 @@ const log = require('./log')
|
|
|
19
19
|
// evaluateAt: 'EXIT' // only used for method probes
|
|
20
20
|
// }
|
|
21
21
|
//
|
|
22
|
+
// Example log line probe with captureExpressions (simplified):
|
|
23
|
+
// Note: captureSnapshot and captureExpressions are mutually exclusive
|
|
24
|
+
// {
|
|
25
|
+
// id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
26
|
+
// version: 0,
|
|
27
|
+
// type: 'LOG_PROBE',
|
|
28
|
+
// where: { sourceFile: 'index.js', lines: ['25'] },
|
|
29
|
+
// template: 'Captured expressions',
|
|
30
|
+
// segments: [{ str: 'Captured expressions' }],
|
|
31
|
+
// captureExpressions: [
|
|
32
|
+
// { name: 'myVar', expr: { dsl: 'myVar', json: { ref: 'myVar' } }, capture: { maxReferenceDepth: 2 } },
|
|
33
|
+
// { name: 'obj.foo', expr: { dsl: 'obj.foo', json: { getmember: [{ ref: 'obj' }, 'foo'] } } }
|
|
34
|
+
// ],
|
|
35
|
+
// capture: { maxReferenceDepth: 3 }, // default limits for expressions without explicit capture
|
|
36
|
+
// sampling: { snapshotsPerSecond: 1 }
|
|
37
|
+
// }
|
|
38
|
+
//
|
|
22
39
|
// Example log method probe (simplified):
|
|
23
40
|
// {
|
|
24
41
|
// id: 'd692ee6d-5734-4df7-9d86-e3bc6449cc8c',
|
|
@@ -63,6 +80,11 @@ async function processMsg (action, probe) {
|
|
|
63
80
|
`Unsupported probe insertion point! Only line-based probes are supported (id: ${probe.id}, version: ${probe.version})`
|
|
64
81
|
)
|
|
65
82
|
}
|
|
83
|
+
if (probe.captureSnapshot && probe.captureExpressions?.length > 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Cannot set both captureSnapshot and captureExpressions (probe: ${probe.id}, version: ${probe.version})`
|
|
86
|
+
)
|
|
87
|
+
}
|
|
66
88
|
|
|
67
89
|
switch (action) {
|
|
68
90
|
case 'unapply':
|
|
@@ -56,13 +56,13 @@ module.exports = {
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* @typedef {object} GetObjectOptions
|
|
59
|
-
* @property {
|
|
59
|
+
* @property {number} maxReferenceDepth - The maximum depth of the object to traverse
|
|
60
60
|
* @property {number} maxCollectionSize - The maximum size of a collection to include in the snapshot
|
|
61
61
|
* @property {number} maxFieldCount - The maximum number of properties on an object to include in the snapshot
|
|
62
62
|
* @property {bigint} deadlineNs - The deadline in nanoseconds compared to `process.hrtime.bigint()`
|
|
63
63
|
* @property {object} ctx - A context object to track the state/progress of the snapshot collection.
|
|
64
64
|
* @property {boolean} ctx.deadlineReached - Will be set to `true` if the deadline has been reached.
|
|
65
|
-
* @property {Error[]} ctx.
|
|
65
|
+
* @property {Error[]} ctx.fatalErrors - An array on which errors can be pushed if an issue is detected while
|
|
66
66
|
* collecting the snapshot.
|
|
67
67
|
*/
|
|
68
68
|
|
|
@@ -99,7 +99,7 @@ async function collectObjectProperties (objectId, opts, depth = 0, collection =
|
|
|
99
99
|
// Trim the number of properties on the object if there's too many.
|
|
100
100
|
const size = result.length
|
|
101
101
|
if (size > LARGE_OBJECT_SKIP_THRESHOLD) {
|
|
102
|
-
opts.ctx.
|
|
102
|
+
opts.ctx.fatalErrors.push(new Error(
|
|
103
103
|
`An object with ${size} properties was detected while collecting a snapshot. ` +
|
|
104
104
|
`This exceeds the maximum number of allowed properties of ${LARGE_OBJECT_SKIP_THRESHOLD}. ` +
|
|
105
105
|
'Future snapshots for existing probes in this location will be skipped until the Node.js process is restarted'
|
|
@@ -1,55 +1,41 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
DEFAULT_MAX_REFERENCE_DEPTH,
|
|
5
|
-
DEFAULT_MAX_COLLECTION_SIZE,
|
|
6
|
-
DEFAULT_MAX_FIELD_COUNT,
|
|
7
|
-
DEFAULT_MAX_LENGTH,
|
|
8
|
-
} = require('./constants')
|
|
3
|
+
const session = require('../session')
|
|
9
4
|
const { collectObjectProperties } = require('./collector')
|
|
10
|
-
const { processRawState } = require('./processor')
|
|
5
|
+
const { processRawState, processRemoteObject } = require('./processor')
|
|
11
6
|
|
|
12
7
|
const BIGINT_MAX = (1n << 256n) - 1n
|
|
13
8
|
|
|
14
9
|
module.exports = {
|
|
15
10
|
getLocalStateForCallFrame,
|
|
11
|
+
evaluateCaptureExpressions,
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
/**
|
|
19
|
-
* @typedef {object}
|
|
20
|
-
* @property {number}
|
|
21
|
-
*
|
|
22
|
-
* @property {number}
|
|
23
|
-
*
|
|
24
|
-
* @property {number} [maxFieldCount] - The maximum number of properties on an object to include in the snapshot.
|
|
25
|
-
* Defaults to {@link DEFAULT_MAX_FIELD_COUNT}.
|
|
26
|
-
* @property {number} [maxLength] - The maximum length of a string to include in the snapshot. Defaults to
|
|
27
|
-
* {@link DEFAULT_MAX_LENGTH}.
|
|
28
|
-
* @property {bigint} [deadlineNs] - The deadline in nanoseconds compared to `process.hrtime.bigint()`. Defaults to
|
|
29
|
-
* {@link BIGINT_MAX}. If the deadline is reached, the snapshot will be truncated.
|
|
15
|
+
* @typedef {object} CaptureLimits - Fully resolved capture limits (all fallbacks already applied)
|
|
16
|
+
* @property {number} maxReferenceDepth - The maximum depth of the object to traverse
|
|
17
|
+
* @property {number} maxCollectionSize - The maximum size of a collection to include in the snapshot
|
|
18
|
+
* @property {number} maxFieldCount - The maximum number of properties on an object to include in the snapshot
|
|
19
|
+
* @property {number} maxLength - The maximum length of a string to include in the snapshot
|
|
30
20
|
*/
|
|
31
21
|
|
|
32
22
|
/**
|
|
33
23
|
* Get the local state for a call frame.
|
|
34
24
|
*
|
|
35
25
|
* @param {import('inspector').Debugger.CallFrame} callFrame - The call frame to get the local state for
|
|
36
|
-
* @param {
|
|
37
|
-
* @
|
|
26
|
+
* @param {CaptureLimits} limits - The capture limits
|
|
27
|
+
* @param {bigint} [deadlineNs] - The deadline in nanoseconds compared to `process.hrtime.bigint()`. Defaults to
|
|
28
|
+
* {@link BIGINT_MAX}. If the deadline is reached, the snapshot will be truncated.
|
|
29
|
+
* @returns {Promise<{ processLocalState: () => ReturnType<typeof processRawState>, fatalErrors: Error[] }>} The local
|
|
30
|
+
* state for the call frame
|
|
38
31
|
*/
|
|
39
|
-
async function getLocalStateForCallFrame (
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
maxCollectionSize = DEFAULT_MAX_COLLECTION_SIZE,
|
|
44
|
-
maxFieldCount = DEFAULT_MAX_FIELD_COUNT,
|
|
45
|
-
maxLength = DEFAULT_MAX_LENGTH,
|
|
46
|
-
deadlineNs = BIGINT_MAX,
|
|
47
|
-
} = {}
|
|
48
|
-
) {
|
|
49
|
-
/** @type {{ deadlineReached: boolean, captureErrors: Error[] }} */
|
|
50
|
-
const ctx = { deadlineReached: false, captureErrors: [] }
|
|
32
|
+
async function getLocalStateForCallFrame (callFrame, limits, deadlineNs = BIGINT_MAX) {
|
|
33
|
+
const { maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength } = limits
|
|
34
|
+
/** @type {{ deadlineReached: boolean, fatalErrors: Error[] }} */
|
|
35
|
+
const ctx = { deadlineReached: false, fatalErrors: [] }
|
|
51
36
|
const opts = { maxReferenceDepth, maxCollectionSize, maxFieldCount, deadlineNs, ctx }
|
|
52
37
|
const rawState = []
|
|
38
|
+
/** @type {ReturnType<typeof processRawState> | null} */
|
|
53
39
|
let processedState = null
|
|
54
40
|
|
|
55
41
|
for (const scope of callFrame.scopeChain) {
|
|
@@ -63,21 +49,167 @@ async function getLocalStateForCallFrame (
|
|
|
63
49
|
// eslint-disable-next-line no-await-in-loop
|
|
64
50
|
rawState.push(...await collectObjectProperties(objectId, opts))
|
|
65
51
|
} catch (err) {
|
|
66
|
-
ctx.
|
|
52
|
+
ctx.fatalErrors.push(new Error(
|
|
67
53
|
`Error getting local state for closure scope (type: ${scope.type}). ` +
|
|
68
|
-
'Future snapshots for existing probes in this location will be skipped until the
|
|
54
|
+
'Future snapshots for existing probes in this location will be skipped until the probes are re-applied',
|
|
69
55
|
{ cause: err } // TODO: The cause is not used by the backend
|
|
70
56
|
))
|
|
71
57
|
}
|
|
72
58
|
if (ctx.deadlineReached === true) break // TODO: Bad UX; Variables in remaining scopes are silently dropped
|
|
73
59
|
}
|
|
74
60
|
|
|
75
|
-
// Delay calling `processRawState` so
|
|
61
|
+
// Delay calling `processRawState` so caller can resume the main thread before processing `rawState`
|
|
76
62
|
return {
|
|
77
63
|
processLocalState () {
|
|
78
64
|
processedState = processedState ?? processRawState(rawState, maxLength)
|
|
79
65
|
return processedState
|
|
80
66
|
},
|
|
81
|
-
|
|
67
|
+
fatalErrors: ctx.fatalErrors,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {object} CompiledCaptureExpression
|
|
73
|
+
* @property {string} name - The name of the expression (used as key in snapshot)
|
|
74
|
+
* @property {string} expression - The compiled expression string to evaluate
|
|
75
|
+
* @property {CaptureLimits} limits - Fully resolved capture limits (precomputed at probe setup)
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @typedef {object} CaptureExpressionResult
|
|
80
|
+
* @property {() => Record<string, ReturnType<typeof processRemoteObject>>} processCaptureExpressions - Callback to
|
|
81
|
+
* process raw data into snapshot format
|
|
82
|
+
* @property {{ expr: string, message: string }[]} evaluationErrors - Transient errors from expression evaluation
|
|
83
|
+
* (safe to retry)
|
|
84
|
+
* @property {Error[]} fatalErrors - Fatal errors that should disable capture expressions for this probe permanently
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {object} EvaluateOnCallFrameResult
|
|
89
|
+
* @property {import('./processor').RemoteObjectWithProperties} result - The result of the evaluation
|
|
90
|
+
* @property {import('inspector').Runtime.ExceptionDetails} [exceptionDetails] - Exception details if evaluation failed
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Evaluate capture expressions for a call frame.
|
|
95
|
+
*
|
|
96
|
+
* Collects raw data while paused, returns a callback to process after resume.
|
|
97
|
+
*
|
|
98
|
+
* @param {import('inspector').Debugger.CallFrame} callFrame - The call frame to evaluate expressions on
|
|
99
|
+
* @param {CompiledCaptureExpression[]} expressions - The compiled expressions with precomputed capture limits
|
|
100
|
+
* @param {bigint} [deadlineNs] - The deadline in nanoseconds. Defaults to {@link BIGINT_MAX}. If the deadline is
|
|
101
|
+
* reached, the snapshot will be truncated.
|
|
102
|
+
* @returns {Promise<CaptureExpressionResult>} Raw results with deferred processing callback
|
|
103
|
+
*/
|
|
104
|
+
async function evaluateCaptureExpressions (callFrame, expressions, deadlineNs = BIGINT_MAX) {
|
|
105
|
+
/** @type {{ name: string, remoteObject: object, maxLength: number }[]} */
|
|
106
|
+
const rawResults = []
|
|
107
|
+
/** @type {{ expr: string, message: string }[]} */
|
|
108
|
+
const evaluationErrors = []
|
|
109
|
+
/** @type {Error[]} */
|
|
110
|
+
const fatalErrors = []
|
|
111
|
+
/** @type {Record<string, ReturnType<typeof processRemoteObject>> | null} */
|
|
112
|
+
let processedResult = null
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < expressions.length; i++) {
|
|
115
|
+
const { name, expression, limits } = expressions[i]
|
|
116
|
+
const { maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength } = limits
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const { result, exceptionDetails } = /** @type {EvaluateOnCallFrameResult} */ (
|
|
120
|
+
// eslint-disable-next-line no-await-in-loop
|
|
121
|
+
await session.post('Debugger.evaluateOnCallFrame', {
|
|
122
|
+
callFrameId: callFrame.callFrameId,
|
|
123
|
+
expression,
|
|
124
|
+
})
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// Handle evaluation exceptions (maybe transient - bad expression, undefined var, etc.)
|
|
128
|
+
if (exceptionDetails) {
|
|
129
|
+
evaluationErrors.push({ expr: name, message: extractErrorMessage(exceptionDetails) })
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Collect raw properties for objects/functions while still paused
|
|
134
|
+
if ((result.type === 'object' || result.type === 'function') && result.objectId && maxReferenceDepth > 0) {
|
|
135
|
+
const ctx = { deadlineReached: false, fatalErrors: [] }
|
|
136
|
+
const isCollection = result.subtype === 'array' || result.subtype === 'typedarray'
|
|
137
|
+
|
|
138
|
+
// eslint-disable-next-line no-await-in-loop
|
|
139
|
+
result.properties = await collectObjectProperties(
|
|
140
|
+
result.objectId,
|
|
141
|
+
{
|
|
142
|
+
// The expression result itself is depth 0, so we subtract 1 when collecting its properties (depth 1+)
|
|
143
|
+
maxReferenceDepth: maxReferenceDepth - 1,
|
|
144
|
+
maxCollectionSize,
|
|
145
|
+
maxFieldCount,
|
|
146
|
+
deadlineNs,
|
|
147
|
+
ctx,
|
|
148
|
+
},
|
|
149
|
+
0,
|
|
150
|
+
isCollection
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
// Propagate fatal errors from nested collection
|
|
154
|
+
if (ctx.fatalErrors.length > 0) {
|
|
155
|
+
fatalErrors.push(...ctx.fatalErrors)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (ctx.deadlineReached === true) {
|
|
159
|
+
// Add the current expression (properties may be incomplete due to timeout)
|
|
160
|
+
rawResults.push({ name, remoteObject: result, maxLength })
|
|
161
|
+
// Add stub entries for remaining uncaptured expressions
|
|
162
|
+
for (let j = i + 1; j < expressions.length; j++) {
|
|
163
|
+
rawResults.push({
|
|
164
|
+
name: expressions[j].name,
|
|
165
|
+
remoteObject: { notCapturedReason: 'timeout' },
|
|
166
|
+
maxLength: 0,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
rawResults.push({ name, remoteObject: result, maxLength })
|
|
174
|
+
} catch (err) {
|
|
175
|
+
fatalErrors.push(new Error(
|
|
176
|
+
`Error capturing expression "${name}". ` +
|
|
177
|
+
'Capture expressions for this probe will be skipped until the probe is re-applied',
|
|
178
|
+
{ cause: err } // TODO: The cause is not used by the backend
|
|
179
|
+
))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Delay calling `processRemoteObject` so caller can resume the main thread before processing `remoteObject`
|
|
184
|
+
return {
|
|
185
|
+
processCaptureExpressions () {
|
|
186
|
+
if (processedResult !== null) return processedResult
|
|
187
|
+
|
|
188
|
+
processedResult = {}
|
|
189
|
+
for (const { name, remoteObject, maxLength } of rawResults) {
|
|
190
|
+
// If the remote object has notCapturedReason (e.g., timeout), use it as-is without processing
|
|
191
|
+
processedResult[name] = remoteObject.notCapturedReason === undefined
|
|
192
|
+
? processRemoteObject(remoteObject, maxLength)
|
|
193
|
+
: remoteObject
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return processedResult
|
|
197
|
+
},
|
|
198
|
+
evaluationErrors,
|
|
199
|
+
fatalErrors,
|
|
82
200
|
}
|
|
83
201
|
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Extract the error message from the exception details.
|
|
205
|
+
*
|
|
206
|
+
* @param {import('inspector').Runtime.ExceptionDetails} exceptionDetails - The exception details
|
|
207
|
+
* @returns {string} The error message
|
|
208
|
+
*/
|
|
209
|
+
function extractErrorMessage (exceptionDetails) {
|
|
210
|
+
const description = exceptionDetails.exception?.description
|
|
211
|
+
if (!description) return 'Unknown evaluation error'
|
|
212
|
+
const startOfStackTraceIndex = description.indexOf('\n at ')
|
|
213
|
+
if (startOfStackTraceIndex === -1) return description
|
|
214
|
+
return description.slice(0, startOfStackTraceIndex)
|
|
215
|
+
}
|
|
@@ -6,6 +6,24 @@ const { normalizeName, REDACTED_IDENTIFIERS } = require('./redaction')
|
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
processRawState: processProperties,
|
|
9
|
+
processRemoteObject,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A RemoteObject with collected properties attached.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {import('inspector').Runtime.RemoteObject & { properties?: object[] }} RemoteObjectWithProperties
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Process a RemoteObject into the snapshot format.
|
|
20
|
+
*
|
|
21
|
+
* @param {RemoteObjectWithProperties} remoteObject
|
|
22
|
+
* @param {number} maxLength - Maximum string length
|
|
23
|
+
* @returns {object} The processed value in snapshot format
|
|
24
|
+
*/
|
|
25
|
+
function processRemoteObject (remoteObject, maxLength) {
|
|
26
|
+
return getPropertyValueRaw({ value: remoteObject }, maxLength)
|
|
9
27
|
}
|
|
10
28
|
|
|
11
29
|
// Matches classes in source code, no matter how it's written:
|
|
@@ -10,7 +10,7 @@ module.exports = {
|
|
|
10
10
|
|
|
11
11
|
EVALUATIONS_INTAKE: 'api',
|
|
12
12
|
EVALUATIONS_EVENT_TYPE: 'evaluation_metric',
|
|
13
|
-
EVALUATIONS_ENDPOINT: '/api/intake/llm-obs/
|
|
13
|
+
EVALUATIONS_ENDPOINT: '/api/intake/llm-obs/v2/eval-metric',
|
|
14
14
|
|
|
15
15
|
EVP_PAYLOAD_SIZE_LIMIT: 5 << 20, // 5MB (actual limit is 5.1MB)
|
|
16
16
|
EVP_EVENT_SIZE_LIMIT: (1 << 20) - 1024, // 999KB (actual limit is 1MB)
|
|
@@ -359,15 +359,15 @@ class LLMObs extends NoopLLMObs {
|
|
|
359
359
|
throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
const { label, value, tags } = options
|
|
362
|
+
const { label, value, tags, reasoning, assessment, metadata } = options
|
|
363
363
|
const metricType = options.metricType?.toLowerCase()
|
|
364
364
|
if (!label) {
|
|
365
365
|
err = 'invalid_metric_label'
|
|
366
366
|
throw new Error('label must be the specified name of the evaluation metric')
|
|
367
367
|
}
|
|
368
|
-
if (!metricType || !['categorical', 'score', 'boolean'].includes(metricType)) {
|
|
368
|
+
if (!metricType || !['categorical', 'score', 'boolean', 'json'].includes(metricType)) {
|
|
369
369
|
err = 'invalid_metric_type'
|
|
370
|
-
throw new Error('metricType must be one of "categorical" or "
|
|
370
|
+
throw new Error('metricType must be one of "categorical", "score", "boolean" or "json"')
|
|
371
371
|
}
|
|
372
372
|
if (metricType === 'categorical' && typeof value !== 'string') {
|
|
373
373
|
err = 'invalid_metric_value'
|
|
@@ -381,6 +381,22 @@ class LLMObs extends NoopLLMObs {
|
|
|
381
381
|
err = 'invalid_metric_value'
|
|
382
382
|
throw new Error('value must be a boolean for a boolean metric')
|
|
383
383
|
}
|
|
384
|
+
if (metricType === 'json' && !(typeof value === 'object' && value != null && !Array.isArray(value))) {
|
|
385
|
+
err = 'invalid_metric_value'
|
|
386
|
+
throw new Error('value must be a JSON object for a json metric')
|
|
387
|
+
}
|
|
388
|
+
if (assessment != null && assessment !== 'pass' && assessment !== 'fail') {
|
|
389
|
+
err = 'invalid_assessment'
|
|
390
|
+
throw new Error('assessment must be pass or fail')
|
|
391
|
+
}
|
|
392
|
+
if (reasoning != null && typeof reasoning !== 'string') {
|
|
393
|
+
err = 'invalid_reasoning'
|
|
394
|
+
throw new Error('reasoning must be a string')
|
|
395
|
+
}
|
|
396
|
+
if (metadata != null && (typeof metadata !== 'object' || Array.isArray(metadata))) {
|
|
397
|
+
err = 'invalid_metadata'
|
|
398
|
+
throw new Error('metadata must be a JSON object')
|
|
399
|
+
}
|
|
384
400
|
|
|
385
401
|
const evaluationTags = {
|
|
386
402
|
'ddtrace.version': tracerVersion,
|
|
@@ -412,8 +428,12 @@ class LLMObs extends NoopLLMObs {
|
|
|
412
428
|
}
|
|
413
429
|
|
|
414
430
|
const payload = {
|
|
415
|
-
|
|
416
|
-
|
|
431
|
+
join_on: {
|
|
432
|
+
span: {
|
|
433
|
+
span_id: spanId,
|
|
434
|
+
trace_id: traceId,
|
|
435
|
+
},
|
|
436
|
+
},
|
|
417
437
|
label,
|
|
418
438
|
metric_type: metricType,
|
|
419
439
|
ml_app: mlApp,
|
|
@@ -421,6 +441,15 @@ class LLMObs extends NoopLLMObs {
|
|
|
421
441
|
timestamp_ms: timestampMs,
|
|
422
442
|
tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`),
|
|
423
443
|
}
|
|
444
|
+
if (reasoning != null) {
|
|
445
|
+
payload.reasoning = reasoning
|
|
446
|
+
}
|
|
447
|
+
if (metadata != null) {
|
|
448
|
+
payload.metadata = metadata
|
|
449
|
+
}
|
|
450
|
+
if (assessment != null) {
|
|
451
|
+
payload.assessment = assessment
|
|
452
|
+
}
|
|
424
453
|
const currentStore = storage.getStore()
|
|
425
454
|
const routing = currentStore?.routingContext
|
|
426
455
|
evalMetricAppendCh.publish({ payload, routing })
|