Skip to content

Commit 015654a

Browse files
authored
feat(debugger): implement capture expressions (#7431)
Add support for capturing specific expressions in log probes as an alternative to full snapshot capture. This allows users to precisely define which data they want to collect, addressing the 1MB Event Platform payload limit issue. When capture expressions are defined, only those expressions are evaluated and serialized instead of capturing the entire object graph of local variables, arguments, and fields. Each expression can optionally override the default capture limits (depth, collection size, field count, and string length). The implementation distinguishes between transient evaluation errors (like undefined variables) and fatal errors (like protocol failures), allowing some expressions to succeed even when others fail. Fatal errors disable capture expressions for the probe until it's re-applied, preventing repeated failures. Time budget enforcement is respected across all expressions. If the timeout is reached while evaluating expressions, any remaining unevaluated expressions are still included in the snapshot with a notCapturedReason indicating the timeout was exceeded.
1 parent 0312991 commit 015654a

File tree

19 files changed

+1304
-107
lines changed

19 files changed

+1304
-107
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
const { once } = require('node:events')
5+
6+
const { assertObjectContains } = require('../helpers')
7+
const { setup } = require('./utils')
8+
9+
describe('Dynamic Instrumentation', function () {
10+
describe('captureExpressions', function () {
11+
describe('deadline behavior', function () {
12+
const t = setup({
13+
testApp: 'target-app/time-budget.js',
14+
dependencies: ['fastify'],
15+
env: { DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS: '15' },
16+
})
17+
18+
beforeEach(() => { t.triggerBreakpoint() })
19+
20+
async function captureExpressionsSnapshot (captureExpressions, additionalConfig = {}) {
21+
t.agent.addRemoteConfig(t.generateRemoteConfig({
22+
captureExpressions,
23+
...additionalConfig,
24+
}))
25+
26+
const [{ payload: [{ debugger: { snapshot } }] }] = await once(t.agent, 'debugger-input')
27+
28+
return snapshot
29+
}
30+
31+
it('adds notCapturedReason for expressions after deadline is reached', async function () {
32+
const snapshot = await captureExpressionsSnapshot([
33+
// First expression: simple primitive variable (should succeed quickly before deadline)
34+
{ name: 'capturedStart', expr: { dsl: 'start', json: { ref: 'start' } } },
35+
// Second expression: deeply nested large object (should trigger deadline during property collection)
36+
{ name: 'capturedObj', expr: { dsl: 'obj', json: { ref: 'obj' } }, capture: { maxReferenceDepth: 5 } },
37+
// Remaining expressions: simple variables that won't be evaluated due to deadline
38+
{ name: 'timedOutObj1', expr: { dsl: 'obj', json: { ref: 'obj' } } },
39+
{ name: 'timedOutStart', expr: { dsl: 'start', json: { ref: 'start' } } },
40+
{ name: 'timedOutObj2', expr: { dsl: 'obj', json: { ref: 'obj' } } },
41+
])
42+
43+
const { captureExpressions } = snapshot.captures.lines[t.breakpoint.line]
44+
45+
assert.deepStrictEqual(
46+
Object.keys(captureExpressions).sort(),
47+
['capturedObj', 'capturedStart', 'timedOutObj1', 'timedOutObj2', 'timedOutStart']
48+
)
49+
50+
// First expression should always be captured (simple primitive evaluated before deadline)
51+
assert.deepStrictEqual(captureExpressions.capturedStart, {
52+
type: 'bigint',
53+
value: String(captureExpressions.capturedStart.value),
54+
})
55+
56+
// Second expression (capturedObj) should be present but may have incomplete properties due to deadline
57+
// Verify it captured at least some properties before timing out
58+
assertObjectContains(captureExpressions.capturedObj, {
59+
type: 'Object',
60+
fields: {
61+
p0: { type: 'Object' },
62+
p1: { type: 'Array' },
63+
p2: { type: 'Map' },
64+
},
65+
})
66+
67+
// Verify that the deadline was reached during capturedObj's property collection
68+
// by checking if any nested property has notCapturedReason: 'timeout'
69+
assert.ok(
70+
JSON.stringify(captureExpressions.capturedObj).includes('"notCapturedReason":"timeout"'),
71+
'Expected capturedObj to contain notCapturedReason: "timeout" in nested properties due to deadline'
72+
)
73+
74+
// Remaining expressions should all have notCapturedReason: 'timeout'
75+
const { capturedStart, capturedObj, ...timedOutExpressions } = captureExpressions
76+
assert.deepStrictEqual(timedOutExpressions, {
77+
timedOutObj1: { notCapturedReason: 'timeout' },
78+
timedOutStart: { notCapturedReason: 'timeout' },
79+
timedOutObj2: { notCapturedReason: 'timeout' },
80+
})
81+
})
82+
})
83+
})
84+
})

0 commit comments

Comments
 (0)