feat(debugger): implement capture expressions#7431
Conversation
Overall package sizeSelf size: 4.61 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 2.0.6 | 81.92 kB | 813.08 kB | | dc-polyfill | 0.1.10 | 26.73 kB | 26.73 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
BenchmarksBenchmark execution time: 2026-02-13 12:48:48 Comparing candidate commit 819f73e in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 231 metrics, 29 unstable metrics. |
ea82eb7 to
90ae293
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #7431 +/- ##
==========================================
- Coverage 80.23% 80.22% -0.01%
==========================================
Files 731 731
Lines 31194 31194
==========================================
- Hits 25027 25025 -2
- Misses 6167 6169 +2 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js
Show resolved
Hide resolved
|
cc @ajwerner added you to take a peak since you're concurrently working on the Go implementation |
BridgeAR
left a comment
There was a problem hiding this comment.
LGTM, just left some nits.
I can not say much about the domain knowledge though :)
integration-tests/debugger/capture-expressions-time-budget.spec.js
Outdated
Show resolved
Hide resolved
packages/dd-trace/src/debugger/devtools_client/snapshot/index.js
Outdated
Show resolved
Hide resolved
| for (const captureExpr of probe.captureExpressions) { | ||
| const limits = { | ||
| maxReferenceDepth: captureExpr.capture?.maxReferenceDepth ?? | ||
| probe.capture?.maxReferenceDepth ?? DEFAULT_MAX_REFERENCE_DEPTH, |
There was a problem hiding this comment.
Nit: these fallbacks are accessed constantly on each iteration in a worst case while we already have the values if probe.captureSnapshot is defined and we could just unconditionally define the object for easier access and assign probe.capture to that object, if needed. That would be a tad nicer to read and we do not need to access the variables multiple times in a worst case.
There was a problem hiding this comment.
I did it this way to avoid unnecessary object creation. If probe.captureSnapshot is not true, the probe.capture object is never going to be used, so there's no need to create it. I figured this one time penalty when adding the breakpoint was better than the extra object being created.
The best alternative that I can find is this, though it still creates the extra object:
+ const capture = {
+ maxReferenceDepth: probe.capture?.maxReferenceDepth ?? DEFAULT_MAX_REFERENCE_DEPTH,
+ maxCollectionSize: probe.capture?.maxCollectionSize ?? DEFAULT_MAX_COLLECTION_SIZE,
+ maxFieldCount: probe.capture?.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT,
+ maxLength: probe.capture?.maxLength ?? DEFAULT_MAX_LENGTH,
+ }
+
if (probe.captureSnapshot) {
- probe.capture = {
- maxReferenceDepth: probe.capture?.maxReferenceDepth ?? DEFAULT_MAX_REFERENCE_DEPTH,
- maxCollectionSize: probe.capture?.maxCollectionSize ?? DEFAULT_MAX_COLLECTION_SIZE,
- maxFieldCount: probe.capture?.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT,
- maxLength: probe.capture?.maxLength ?? DEFAULT_MAX_LENGTH,
- }
+ probe.capture = capture
}
if (probe.captureExpressions?.length > 0) {
probe.compiledCaptureExpressions = []
for (const captureExpr of probe.captureExpressions) {
const limits = {
- maxReferenceDepth: captureExpr.capture?.maxReferenceDepth ??
- probe.capture?.maxReferenceDepth ?? DEFAULT_MAX_REFERENCE_DEPTH,
- maxCollectionSize: captureExpr.capture?.maxCollectionSize ??
- probe.capture?.maxCollectionSize ?? DEFAULT_MAX_COLLECTION_SIZE,
- maxFieldCount: captureExpr.capture?.maxFieldCount ??
- probe.capture?.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT,
- maxLength: captureExpr.capture?.maxLength ??
- probe.capture?.maxLength ?? DEFAULT_MAX_LENGTH,
+ maxReferenceDepth: captureExpr.capture?.maxReferenceDepth ?? capture.maxReferenceDepth,
+ maxCollectionSize: captureExpr.capture?.maxCollectionSize ?? capture.maxCollectionSize,
+ maxFieldCount: captureExpr.capture?.maxFieldCount ?? capture.maxFieldCount,
+ maxLength: captureExpr.capture?.maxLength ?? capture.maxLength,
}Do you think the tradeoff of this implementation is better than the current one?
There was a problem hiding this comment.
I can not tell how likely either case is, so I think it is best for you to decide upon that
There was a problem hiding this comment.
A probe is not supposed to have both probe.captureSnapshot: true and probe.captureExpressions.length > 0 at the same time. Those should be mutually exclusive. Some customers will lean heavily towards using Capture Expressions all the time, others will only use snapshots, others will not use any of those 🤷 I'll leave it as is for now.
| limits, | ||
| }) | ||
| } catch (err) { | ||
| throw new Error( |
There was a problem hiding this comment.
Are we interested in the stack trace? If not, what about using Error.stackTraceLimit = 0 for these cases? I am unsure how likely these are though.
There was a problem hiding this comment.
We use this pattern throughout the devtools_client worker, and it can be good to have the stack trace so you don't have to rely on the error message being unique enough so that you can find it without the stack trace. Also, it can sometimes help identify which version a given error comes from if the lines have moved around between versions and you somehow don't have the exact version number.
|
|
||
| if (probe.captureSnapshot) { | ||
| if (captureErrors?.length > 0) { | ||
| if (fatalSnapshotErrors && fatalSnapshotErrors.length > 0) { |
There was a problem hiding this comment.
Nit
| if (fatalSnapshotErrors && fatalSnapshotErrors.length > 0) { | |
| if (fatalSnapshotErrors?.length > 0) { |
There was a problem hiding this comment.
😂 Yeah I know. I did this mainly to appease TS, as it complained about doing undefined > 0
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.
90ae293 to
819f73e
Compare
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.
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.

What does this PR do?
This PR implements capture expressions for Live Debugger/Dynamic Instrumentation log probes, providing an alternative to full snapshot capture. Instead of serializing entire object graphs, users can now define specific expressions to capture, giving them precise control over what data gets collected and addressing the 1MB Event Platform payload limit.
Motivation
Log probes currently face a significant limitation: when
captureSnapshot: trueis enabled, the debugger serializes entire object graphs. Even with depth restrictions in place, this approach might hit the 1MB Event Platform limit. The problem becomes frustrating when users need a specific piece of deeply nested data, increasing the capture depth to reach that value inadvertently captures large amounts of uninteresting data from other parts of the object graph.Capture expressions solve this by letting users define exactly which data they need. When
captureExpressionsis defined on a probe, the debugger switches to expression-only mode, evaluating and serializing only the specified expressions. Each expression can optionally override the default capture limits for depth, collection size, field count, and string length, providing fine-grained control over what gets captured.The implementation includes robust error handling that distinguishes between two types of failures. Evaluation errors, like
ReferenceErrorfor undefined variables, are expected JavaScript errors that get reported per-expression while allowing other expressions to continue. Fatal errors, such as protocol failures, indicate something more serious has gone wrong, so the entire capture expressions feature is disabled for that probe until the probe is re-applied, preventing repeated failures on every breakpoint hit.Time budget enforcement remains a critical aspect of the debugger's performance profile: When capture expressions are being evaluated, the timeout applies across all expressions. If the time budget is exceeded, successfully evaluated expressions are included in the snapshot, while remaining unevaluated expressions appear with a
notCapturedReasonfield indicating the timeout was reached. This ensures the snapshot structure remains predictable even when operating under time pressure.Additional Notes