Skip to content

DI: capture expressions#5845

Draft
p-datadog wants to merge 25 commits into
masterfrom
di-capture-expressions
Draft

DI: capture expressions#5845
p-datadog wants to merge 25 commits into
masterfrom
di-capture-expressions

Conversation

@p-datadog

@p-datadog p-datadog commented Jun 1, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Adds capture-expression support to Ruby log probes in Dynamic Instrumentation. A capture expression is a named DSL expression attached to a log probe; at probe-fire time, the expression is evaluated against the probe scope and its serialized value is emitted in the snapshot payload under captures.…​.captureExpressions.<name>. Capture expressions are an alternative to captureSnapshot: true: they let the user select exactly which values to capture instead of the whole local/argument scope.

Motivation:

Capture expressions are already shipped in Python, Java, .NET, Node.js, PHP, and Go DI. Ruby was the gap. This PR closes that gap so the Live Debugger UI can offer the feature on Ruby services once the corresponding dd-source version_matrix.go row is updated.

Behavior choices for Ruby (cross-tracer comparison drove each pick):

  • Mutual exclusion: snapshot wins at fire time when both captureSnapshot and captureExpressions are set, matching Python/Java/Go.
  • Per-expression capture fallback: per-field expr ?? probe ?? settings, matching Node.js (most ergonomic).
  • Rate-limit bucket: snapshot-class (1/sec default), matching Python/.NET/Go. Override via sampling.snapshotsPerSecond on the probe.
  • Per-fire time budget: 200 ms, configurable via DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE. Remaining expressions emit a {notCapturedReason: "timeout"} stub so the truncation is visible in the snapshot UI.
  • Error wire format: { expr: <name>, message } appended to the snapshot's evaluationErrors array; the failed key is omitted from captureExpressions.
  • Snapshot shape for method probes: captureExpressions is emitted under captures.entry when the probe's evaluateAt is ENTRY, and under captures.return otherwise (default EXIT, matching the libdatadog ProbeCommon JSON-parse default that PHP also shares). The evaluator runs once per fire — at the entry hook against the pre-super scope (args / kwargs / self only; @return / @duration / @exception are nil there) for evaluateAt: ENTRY, and at the exit hook against the full exit-time scope for the default. Matches Python, .NET, and PHP.

All four per-field limits (maxReferenceDepth, maxFieldCount, maxCollectionSize, maxLength) flow through the documented expr → probe → settings fallback chain end-to-end for capture expressions. As part of this work the same four probe-level overrides now also flow through the snapshot serialization path (entry args, @return, self, and line-probe locals), where probe-level maxLength / maxCollectionSize were previously silently dropped and the self serialization had ignored all four overrides.

Change log entry

Yes. Dynamic Instrumentation log probes now support capture expressions — named expressions whose values are emitted in the snapshot under captureExpressions, as an alternative to capturing the full local/argument scope. Also fixes probe-level capture-limit overrides (maxLength, maxCollectionSize, maxFieldCount, maxReferenceDepth) being silently dropped when serializing entry args, self, @return, and line-probe locals under captureSnapshot.

@p-datadog p-datadog added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Jun 1, 2026
@dd-octo-sts dd-octo-sts Bot added core Involves Datadog core libraries debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database) labels Jun 1, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Typing analysis

Note: Ignored files are excluded from the next sections.

steep:ignore comments

This PR introduces 11 steep:ignore comments, and clears 11 steep:ignore comments.

steep:ignore comments (+11-11)Introduced:
lib/datadog/di/instrumenter.rb:121
lib/datadog/di/instrumenter.rb:135
lib/datadog/di/instrumenter.rb:405
lib/datadog/di/instrumenter.rb:407
lib/datadog/di/instrumenter.rb:730
lib/datadog/di/probe_builder.rb:26
lib/datadog/di/probe_notification_builder.rb:215
lib/datadog/di/probe_notification_builder.rb:450
lib/datadog/di/serializer.rb:282
lib/datadog/di/serializer.rb:414
lib/datadog/di/serializer.rb:527
Cleared:
lib/datadog/di/instrumenter.rb:109
lib/datadog/di/instrumenter.rb:123
lib/datadog/di/instrumenter.rb:346
lib/datadog/di/instrumenter.rb:348
lib/datadog/di/instrumenter.rb:671
lib/datadog/di/probe_builder.rb:24
lib/datadog/di/probe_notification_builder.rb:137
lib/datadog/di/probe_notification_builder.rb:372
lib/datadog/di/serializer.rb:243
lib/datadog/di/serializer.rb:372
lib/datadog/di/serializer.rb:485

Untyped methods

This PR introduces 2 untyped methods and 32 partially typed methods, and clears 2 untyped methods and 29 partially typed methods. It increases the percentage of typed methods from 65.28% to 65.32% (+0.04%).

Untyped methods (+2-2)Introduced:
sig/datadog/di/probe_notification_builder.rbs:58
└── def tags: () -> untyped
sig/datadog/di/probe_notification_builder.rbs:60
└── def serialized_tags: () -> untyped
Cleared:
sig/datadog/di/probe_notification_builder.rbs:52
└── def tags: () -> untyped
sig/datadog/di/probe_notification_builder.rbs:54
└── def serialized_tags: () -> untyped
Partially typed methods (+32-29)Introduced:
sig/datadog/di/capture_expression_evaluator.rbs:18
└── def evaluate: (Probe probe, Context context) -> [Hash[String, untyped], Array[Hash[Symbol, String]]]
sig/datadog/di/context.rbs:29
└── def initialize: (probe: Probe, settings: Datadog::Core::Configuration::Settings, serializer: Serializer, ?locals: Hash[Symbol, untyped]?, ?target_self: untyped?, ?path: String?, ?caller_locations: Array[Thread::Backtrace::Location]?, ?serialized_entry_args: Hash[Symbol, untyped]?, ?entry_capture_expressions: Hash[String, untyped]?, ?entry_capture_evaluation_errors: Array[Hash[Symbol, String]]?, ?return_value: untyped?, ?duration: Float?, ?exception: Exception?) -> void
sig/datadog/di/context.rbs:50
└── def serialized_locals: () -> Hash[Symbol, untyped]?
sig/datadog/di/context.rbs:52
└── def fetch: ((Symbol | String) var_name) -> untyped
sig/datadog/di/context.rbs:54
└── def fetch_ivar: ((Symbol | String) var_name) -> untyped
sig/datadog/di/instrumenter.rbs:40
└── def hook_method: (Probe probe, untyped responder) -> void
sig/datadog/di/instrumenter.rbs:43
└── def hook_line: (Probe probe, untyped responder) -> bool?
sig/datadog/di/instrumenter.rbs:47
└── def hook: (Probe probe, untyped responder) -> void
sig/datadog/di/instrumenter.rbs:51
└── def self.get_local_variables: (TracePoint trace_point) -> Hash[Symbol, untyped]
sig/datadog/di/instrumenter.rbs:52
└── def self.get_instance_variables: (Object self) -> Hash[Symbol, untyped]
sig/datadog/di/instrumenter.rbs:58
└── def line_trace_point_callback: (Probe probe, RubyVM::InstructionSequence? iseq, untyped responder, TracePoint tp) -> void
sig/datadog/di/instrumenter.rbs:62
└── def check_and_disable_if_exceeded: (Probe probe, untyped responder, Float di_start_time, ?Float accumulated_duration) -> void
sig/datadog/di/probe_builder.rbs:10
└── def self?.build_from_remote_config: (Hash[String, untyped] config) -> Probe
sig/datadog/di/probe_builder.rbs:12
└── def self?.build_template_segments: (Array[Hash[String, untyped]]? segments) -> Array[String | DI::EL::Expression]?
sig/datadog/di/probe_builder.rbs:14
└── def self?.build_capture_expressions: (Array[Hash[String, untyped]]? raw) -> Array[CaptureExpression]
sig/datadog/di/probe_builder.rbs:16
└── def self?.build_capture_limits: (Hash[String, untyped]? raw) -> CaptureLimits?
sig/datadog/di/probe_notification_builder.rbs:22
└── def build_received: (Probe probe) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:24
└── def build_installed: (Probe probe) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:26
└── def build_emitting: (Probe probe) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:28
└── def build_errored: (Probe probe, Exception exception) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:30
└── def build_disabled: (Probe probe, Float duration) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:32
└── def build_executed: (Context context) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:34
└── def build_snapshot: (Context context) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:39
└── def build_snapshot_base: (Context context, ?evaluation_errors: Array[Hash[Symbol, String]]?, ?captures: Hash[Symbol, untyped]?, ?message: String?) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:41
└── def build_condition_evaluation_failed: (Context context, EL::Expression expr, Exception exception) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:43
└── def build_status: (Probe probe, message: String, status: String, ?exception: Exception?) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:45
└── def format_caller_locations: (Array[Thread::Backtrace::Location] callers) -> Array[Hash[Symbol,untyped]]
sig/datadog/di/probe_notification_builder.rbs:49
└── def tag_process_tags!: (Hash[Symbol | String, untyped] payload, Core::Configuration::Settings settings) -> void
sig/datadog/di/probe_notification_builder.rbs:53
└── def get_local_variables: (TracePoint trace_point) -> Hash[Symbol,untyped]
sig/datadog/di/serializer.rbs:25
└── def serialize_args: (Array[untyped] args, Hash[Symbol, untyped] kwargs, untyped instance_vars, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:26
└── def serialize_vars: (Hash[Symbol, untyped] vars, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:27
└── def serialize_value: (untyped value, ?name: (Symbol | String)?, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?, ?type: Class?) -> Hash[Symbol, untyped]
Cleared:
sig/datadog/di/context.rbs:26
└── def initialize: (probe: Probe, settings: Datadog::Core::Configuration::Settings, serializer: Serializer, ?locals: Hash[Symbol, untyped]?, ?target_self: untyped?, ?path: String?, ?caller_locations: Array[Thread::Backtrace::Location]?, ?serialized_entry_args: Hash[Symbol, untyped]?, ?return_value: untyped?, ?duration: Float?, ?exception: Exception?) -> void
sig/datadog/di/context.rbs:45
└── def serialized_locals: () -> Hash[Symbol, untyped]?
sig/datadog/di/context.rbs:47
└── def fetch: ((Symbol | String) var_name) -> untyped
sig/datadog/di/context.rbs:49
└── def fetch_ivar: ((Symbol | String) var_name) -> untyped
sig/datadog/di/instrumenter.rbs:36
└── def hook_method: (Probe probe, untyped responder) -> void
sig/datadog/di/instrumenter.rbs:39
└── def hook_line: (Probe probe, untyped responder) -> bool?
sig/datadog/di/instrumenter.rbs:43
└── def hook: (Probe probe, untyped responder) -> void
sig/datadog/di/instrumenter.rbs:47
└── def self.get_local_variables: (TracePoint trace_point) -> Hash[Symbol, untyped]
sig/datadog/di/instrumenter.rbs:48
└── def self.get_instance_variables: (Object self) -> Hash[Symbol, untyped]
sig/datadog/di/instrumenter.rbs:54
└── def line_trace_point_callback: (Probe probe, RubyVM::InstructionSequence? iseq, untyped responder, TracePoint tp) -> void
sig/datadog/di/instrumenter.rbs:58
└── def check_and_disable_if_exceeded: (Probe probe, untyped responder, Float di_start_time, ?Float accumulated_duration) -> void
sig/datadog/di/probe_builder.rbs:6
└── def self?.build_from_remote_config: (Hash[String, untyped] config) -> Probe
sig/datadog/di/probe_builder.rbs:8
└── def self?.build_template_segments: (Array[Hash[String, untyped]]? segments) -> Array[String | DI::EL::Expression]?
sig/datadog/di/probe_notification_builder.rbs:16
└── def build_received: (Probe probe) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:18
└── def build_installed: (Probe probe) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:20
└── def build_emitting: (Probe probe) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:22
└── def build_errored: (Probe probe, Exception exception) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:24
└── def build_disabled: (Probe probe, Float duration) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:26
└── def build_executed: (Context context) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:28
└── def build_snapshot: (Context context) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:33
└── def build_snapshot_base: (Context context, ?evaluation_errors: Array[Hash[Symbol, String]]?, ?captures: Hash[Symbol, untyped]?, ?message: String?) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:35
└── def build_condition_evaluation_failed: (Context context, EL::Expression expr, Exception exception) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:37
└── def build_status: (Probe probe, message: String, status: String, ?exception: Exception?) -> Hash[Symbol,untyped]
sig/datadog/di/probe_notification_builder.rbs:39
└── def format_caller_locations: (Array[Thread::Backtrace::Location] callers) -> Array[Hash[Symbol,untyped]]
sig/datadog/di/probe_notification_builder.rbs:43
└── def tag_process_tags!: (Hash[Symbol | String, untyped] payload, Core::Configuration::Settings settings) -> void
sig/datadog/di/probe_notification_builder.rbs:47
└── def get_local_variables: (TracePoint trace_point) -> Hash[Symbol,untyped]
sig/datadog/di/serializer.rbs:25
└── def serialize_args: (Array[untyped] args, Hash[Symbol, untyped] kwargs, untyped instance_vars, ?depth: Integer, ?attribute_count: Integer?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:26
└── def serialize_vars: (Hash[Symbol, untyped] vars, ?depth: Integer, ?attribute_count: Integer?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:27
└── def serialize_value: (untyped value, ?name: (Symbol | String)?, ?depth: Integer, ?attribute_count: Integer?, ?type: Class?) -> Hash[Symbol, untyped]

Untyped other declarations

This PR introduces 3 untyped other declarations and 4 partially typed other declarations, and clears 3 untyped other declarations and 2 partially typed other declarations. It increases the percentage of typed other declarations from 82.62% to 82.91% (+0.29%).

Untyped other declarations (+3-3)Introduced:
sig/datadog/di/context.rbs:23
└── @return_value: untyped
sig/datadog/di/context.rbs:39
└── attr_reader target_self: untyped
sig/datadog/di/context.rbs:46
└── attr_reader return_value: untyped
Cleared:
sig/datadog/di/context.rbs:20
└── @return_value: untyped
sig/datadog/di/context.rbs:36
└── attr_reader target_self: untyped
sig/datadog/di/context.rbs:41
└── attr_reader return_value: untyped
Partially typed other declarations (+4-2)Introduced:
sig/datadog/di/context.rbs:20
└── @entry_capture_expressions: Hash[String, untyped]?
sig/datadog/di/context.rbs:37
└── attr_reader locals: Hash[Symbol, untyped]?
sig/datadog/di/context.rbs:43
└── attr_reader serialized_entry_args: Hash[Symbol, untyped]?
sig/datadog/di/context.rbs:44
└── attr_reader entry_capture_expressions: Hash[String, untyped]?
Cleared:
sig/datadog/di/context.rbs:34
└── attr_reader locals: Hash[Symbol, untyped]?
sig/datadog/di/context.rbs:40
└── attr_reader serialized_entry_args: Hash[Symbol, untyped]?

If you believe a method or an attribute is rightfully untyped or partially typed, you can add # untyped:accept on the line before the definition to remove it from the stats.

@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Pipelines  Tests

Fix all issues with BitsAI

⚠️ Warnings

🚦 15 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-rb | ruby-app.amd64.DOA9: [public.ecr.aws/lts/ubuntu:22.04, linux/amd64, 3.4.7]   View in Datadog   GitLab

Unit Tests | Ruby 2.6 / build &amp; test (standard) [0]   View in Datadog   GitHub Actions

System Tests | test / End-to-end #10 / rack 10   View in Datadog   GitHub Actions

View all 15 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 7cf4914 | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Jun 1, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-06-15 22:36:47

Comparing candidate commit 7cf4914 in PR branch di-capture-expressions with baseline commit e014f83 in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 45 metrics, 1 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

@p-datadog p-datadog requested a review from Copilot June 2, 2026 20:17
@p-datadog

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Dynamic Instrumentation “capture expressions” support for Ruby log probes, enabling selective value capture (via DSL expressions) into snapshot payloads under captureExpressions, with configurable per-fire serialization time budgeting.

Changes:

  • Parse captureExpressions (including per-expression capture limits) from remote config and attach them to Datadog::DI::Probe.
  • Evaluate capture expressions at probe fire time with a configurable time budget and merge per-expression evaluation errors into snapshot evaluationErrors.
  • Extend serializer plumbing to allow per-evaluation overrides for string length / collection size limits; add docs, RBS signatures, and specs.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
supported-configurations.json Adds env var metadata for max serialization time budget.
spec/datadog/di/probe_spec.rb Adds unit coverage for probe capture_expressions behavior and rate-limit defaults.
spec/datadog/di/probe_builder_spec.rb Adds remote-config parsing tests for captureExpressions and related behaviors.
spec/datadog/di/capture_expression_spec.rb Adds tests for capture expression / limits value objects and limit resolution behavior.
spec/datadog/di/capture_expression_evaluator_spec.rb Adds tests for evaluation success/failure, timeout behavior, and limit propagation to serializer.
sig/datadog/di/serializer.rbs Updates serialize_value signature to include length and collection_size.
sig/datadog/di/probe.rbs Adds capture-expression-related fields and predicate signature.
sig/datadog/di/probe_notification_builder.rbs Adds capture_expression_evaluator ivar/reader typing.
sig/datadog/di/probe_builder.rbs Adds capture-expression builder helper typings and name pattern constant.
sig/datadog/di/capture_expression.rbs Adds RBS for CaptureExpression and CaptureLimits.
sig/datadog/di/capture_expression_evaluator.rbs Adds RBS for evaluator class and return types.
lib/datadog/di/serializer.rb Threads per-evaluation length/collection_size through recursive serialization.
lib/datadog/di/probe.rb Stores capture-expression config on probes and adjusts default rate limiting.
lib/datadog/di/probe_notification_builder.rb Evaluates capture expressions for snapshots and merges evaluation errors.
lib/datadog/di/probe_builder.rb Parses captureExpressions from remote config and validates names/expr structure.
lib/datadog/di/configuration.rb Introduces max_time_to_serialize_ms DI setting backed by env var.
lib/datadog/di/capture_expression.rb Adds CaptureExpression and CaptureLimits classes plus limit-resolution logic.
lib/datadog/di/capture_expression_evaluator.rb Implements evaluator with time budget, serialization, and error reporting.
lib/datadog/di/boot.rb Ensures capture-expression components are loaded during DI boot.
lib/datadog/core/configuration/supported_configurations.rb Registers new env var in supported configuration list (generated).
docs/DynamicInstrumentation.md Documents capture expressions, behavior choices, and new env var.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/datadog/di/capture_expression.rb Outdated
Comment thread lib/datadog/di/probe_builder.rb

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5affe59f77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/datadog/di/capture_expression.rb Outdated
Comment thread lib/datadog/di/probe_notification_builder.rb Outdated
p-datadog pushed a commit that referenced this pull request Jun 2, 2026
…d length

The CaptureLimits.resolve doc comment promises a per-field fallback chain
of expr_limits -> probe -> settings, but for collection_size and length the
code went straight from the expression-level override to settings, skipping
the probe-level overrides parsed into probe.max_capture_collection_size /
probe.max_capture_string_length. That meant a probe-level capture.maxCollectionSize
or capture.maxLength from remote config was silently ignored unless repeated
on every expression.

Adds the probe-level step to both fallback chains so the resolver matches
its documented contract. The serializer already accepts length: and collection_size:
keyword arguments, so the fix flows end-to-end.

Addresses review comments on PR #5845 from @Copilot (3344166066) and
@chatgpt-codex-connector (3344167297).

Co-Authored-By: Claude <noreply@anthropic.com>
p-datadog pushed a commit that referenced this pull request Jun 2, 2026
build_capture_expressions called raw.empty? before verifying raw was an
Array. A non-Array value from remote config (e.g. an Integer) would raise
NoMethodError instead of the intended ArgumentError, and the outer
rescue KeyError in build_from_remote_config would not wrap it either —
producing a misleading error class even though the bare rescue in
Component#parse_probe_spec_and_notify still kept it from escaping.

Reorders the early-return: nil short-circuits, then the Array type check,
then the empty-array short-circuit. Adds a probe_builder_spec test for
the non-Array case.

Addresses review comment on PR #5845 from @Copilot (3344166098).

Co-Authored-By: Claude <noreply@anthropic.com>
p-datadog pushed a commit that referenced this pull request Jun 2, 2026
The return-time Context for method probes was built with locals: nil, so
capture expressions referencing arg1 or kwarg names resolved as
undefined/nil — the condition path at instrumenter.rb:135 already populates
locals via serializer.combine_args(args, kwargs, self) but the return-time
path didn't.

Passes combine_args(args, kwargs, self) as locals when the probe has
capture expressions; keeps nil otherwise to avoid an allocation on every
method invocation for probes that don't need it. At return time the args
reference is the same Ruby object as at entry, so values reflect any
in-place mutation by the method body — matching what conditions already
see and what cross-tracer convention reports for method exit scope.

Adds an integration test exercising a method probe with a capture
expression referencing arg1 against a positional argument.

Addresses review comment on PR #5845 from @chatgpt-codex-connector (3344167304).

Co-Authored-By: Claude <noreply@anthropic.com>
p-ddsign and others added 16 commits June 9, 2026 10:01
Adds capture-expression support to Ruby log probes.

* Parse the `captureExpressions` field in remote-config LOG_PROBE payloads.
* Evaluate each expression against the probe scope at fire time.
* Emit a `captureExpressions` block in the snapshot under the same
  `captures.{lines.<n>|entry|return}` key that full snapshots use.
* Per-fire time budget controlled by
  `DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS` (default 200).
  Remaining expressions emit a stub `{notCapturedReason: "timeout"}`.
* Snapshot wins at fire time when both `captureSnapshot` and
  `captureExpressions` are set on the same probe (matches Python/Java/Go).
* Per-expression evaluation errors are added to `evaluationErrors` with
  the failed expression's name; the failed key is omitted from the
  `captureExpressions` block.
* Capture-expression probes default to the snapshot rate-limit bucket
  (1/sec); existing `sampling.snapshotsPerSecond` overrides this.

Tests cover parsing (including the backend name pattern
`^[a-zA-Z0-9_?]+$`), the rate-limit fallback, per-expression depth
override, time-budget exhaustion, and the success/error/timeout output
shapes.

Known limitation: per-expression `maxLength` and `maxCollectionSize`
are not yet honored end-to-end; the snapshot serializer reads those
two limits from the DI settings directly. Per-expression
`maxReferenceDepth` and `maxFieldCount` work.
- Thread max_length and max_collection_size kwargs through Serializer#serialize_value
  and its recursive calls, so per-expression CaptureLimits overrides for all four
  fields work end-to-end (previously only depth and attribute_count were honored).
- Add Steep signatures for the new ProbeBuilder helpers, the
  capture_expression_evaluator accessor on ProbeNotificationBuilder, and the new
  length/collection_size kwargs on Serializer#serialize_value.
- Remove the 'known limitation' note from docs/DynamicInstrumentation.md.
…_SERIALIZE

The prior commit added DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS as
a new key, which would require central Configuration Registry
coordination and broke validate_supported_configurations_v2_local_file.

Switch to the existing DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE
env var that .NET already declares (and which is already centrally
registered). Its 200 ms default matches our chosen budget; its
semantics extend naturally to whole-snapshot serialization if Ruby
later applies the budget more broadly.

* Rename the setting from capture_expression_timeout_ms to
  max_time_to_serialize_ms.
* Drop the new DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS entry
  from supported-configurations.json and supported_configurations.rb;
  add the existing DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE.
* Update docs/DynamicInstrumentation.md to reference the .NET-aligned
  env var.
* Update the capture-expression evaluator and spec to use the new
  setting name.
…d length

The CaptureLimits.resolve doc comment promises a per-field fallback chain
of expr_limits -> probe -> settings, but for collection_size and length the
code went straight from the expression-level override to settings, skipping
the probe-level overrides parsed into probe.max_capture_collection_size /
probe.max_capture_string_length. That meant a probe-level capture.maxCollectionSize
or capture.maxLength from remote config was silently ignored unless repeated
on every expression.

Adds the probe-level step to both fallback chains so the resolver matches
its documented contract. The serializer already accepts length: and collection_size:
keyword arguments, so the fix flows end-to-end.

Addresses review comments on PR #5845 from @Copilot (3344166066) and
@chatgpt-codex-connector (3344167297).

Co-Authored-By: Claude <noreply@anthropic.com>
build_capture_expressions called raw.empty? before verifying raw was an
Array. A non-Array value from remote config (e.g. an Integer) would raise
NoMethodError instead of the intended ArgumentError, and the outer
rescue KeyError in build_from_remote_config would not wrap it either —
producing a misleading error class even though the bare rescue in
Component#parse_probe_spec_and_notify still kept it from escaping.

Reorders the early-return: nil short-circuits, then the Array type check,
then the empty-array short-circuit. Adds a probe_builder_spec test for
the non-Array case.

Addresses review comment on PR #5845 from @Copilot (3344166098).

Co-Authored-By: Claude <noreply@anthropic.com>
The return-time Context for method probes was built with locals: nil, so
capture expressions referencing arg1 or kwarg names resolved as
undefined/nil — the condition path at instrumenter.rb:135 already populates
locals via serializer.combine_args(args, kwargs, self) but the return-time
path didn't.

Passes combine_args(args, kwargs, self) as locals when the probe has
capture expressions; keeps nil otherwise to avoid an allocation on every
method invocation for probes that don't need it. At return time the args
reference is the same Ruby object as at entry, so values reflect any
in-place mutation by the method body — matching what conditions already
see and what cross-tracer convention reports for method exit scope.

Adds an integration test exercising a method probe with a capture
expression referencing arg1 against a positional argument.

Addresses review comment on PR #5845 from @chatgpt-codex-connector (3344167304).

Co-Authored-By: Claude <noreply@anthropic.com>
Probe-level overrides for max_capture_depth, max_capture_attribute_count,
max_capture_string_length, and max_capture_collection_size from remote
config were partially or fully ignored on every snapshot-path serializer
call site:

- probe_notification_builder.rb @return: depth and attribute_count flowed
  through, length and collection_size did not.
- probe_notification_builder.rb self (method probe and line probe): none
  of the four limits were passed; serializer fell back to settings for all
  of them, silently dropping every probe-level override.
- instrumenter.rb entry-args (serialize_args): depth and attribute_count
  flowed through, length and collection_size did not.
- context.rb serialized_locals (serialize_vars): same gap as serialize_args.

Adds Probe#snapshot_serializer_limits(settings) returning the four-limit
keyword-arg hash with per-field fallback to settings. All five snapshot-path
sites now splat it into the serializer call. Serializer#serialize_args and
serialize_vars gain matching length / collection_size kwargs that thread to
the underlying serialize_value.

Adds Probe#snapshot_serializer_limits unit tests covering the no-override,
all-override, and mixed-override cases, plus an integration test exercising
a max_capture_string_length override end-to-end through entry args, @return,
and self's instance variable.

Stale doc comments on Probe#max_capture_collection_size and
Probe#max_capture_string_length (which claimed the snapshot serializer read
settings directly) are removed.

Co-Authored-By: Claude <noreply@anthropic.com>
…el limit fallback

Address review feedback on the capture-expressions PR:

1. CaptureLimits.resolve now consults probe.max_capture_collection_size and
   probe.max_capture_string_length, matching the customer-facing documentation
   ("fall back to the probe-level capture and then to the tracer's default
   settings, independently per field"). Previously those two probe-level
   attributes were stored but never read.

2. ProbeNotificationBuilder now accepts and forwards telemetry to
   CaptureExpressionEvaluator. Component wires telemetry through at
   construction. Previously the evaluator's rescue block and timeout
   counter were no-ops because @telemetry was always nil.

3. CaptureExpressionEvaluator#evaluate uses the correct telemetry.inc
   signature: (namespace, metric_name, value). The previous 2-argument
   form would have raised ArgumentError once telemetry was actually
   wired through.

4. Added end-to-end ProbeNotificationBuilder#build_snapshot tests for:
   - line probe with capture_expressions emits captureExpressions block
   - method probe with capture_expressions emits entry/return blocks
   - method probe with capture_expressions and an exception emits throwable
   - captureSnapshot=true wins and drops captureExpressions
   - per-expression evaluation errors flow into top-level evaluationErrors

5. Added branch coverage for ProbeBuilder.build_capture_expressions /
   build_capture_limits type-check raises (non-array captureExpressions,
   non-hash entry, non-hash capture).

6. Documented DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE and the
   programmatic max_time_to_serialize_ms setting in GettingStarted.md.

7. capture_expression_spec.rb now uses instance_double(Datadog::DI::Probe)
   instead of plain double() so interface drift is caught.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Match the dd-trace-rb convention of one class per file. The DI codebase
otherwise has Probe in probe.rb, Serializer in serializer.rb, etc.
capture_expression.rb was the only DI file with two classes.

- lib/datadog/di/capture_limits.rb — new, CaptureLimits only
- lib/datadog/di/capture_expression.rb — CaptureExpression only,
  requires capture_limits for the limits attribute type
- sig split into capture_limits.rbs and capture_expression.rbs
- spec/datadog/di/capture_limits_spec.rb — new, CaptureLimits tests
- capture_expression_spec.rb — CaptureExpression tests only
- boot.rb requires capture_limits before capture_expression
- capture_expression_evaluator.rb and probe_builder.rb add explicit
  capture_limits requires (they were getting it transitively via
  capture_expression.rb)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…re telemetry on notification builder

Addresses review feedback on the capture-expressions PR:

1. Component pattern violation: CaptureExpressionEvaluator was calling
   Datadog.logger.debug on the rescue path despite already taking
   injected settings/serializer/telemetry. Logger is now a required
   constructor parameter; ProbeNotificationBuilder forwards it (and is
   itself given logger via Component#initialize, matching the existing
   Instrumenter signature).

2. Nil-defaulted keyword arguments: telemetry was `telemetry: nil` on
   both CaptureExpressionEvaluator and ProbeNotificationBuilder, but
   production always supplies a real telemetry component (Component
   passes it through). The default was test convenience, not a real
   optional dependency. Both telemetry: kwargs are now required.

3. Telemetry assertions: capture_expression_evaluator_spec now uses
   instance doubles for logger and telemetry and asserts the two
   telemetry calls (timeout -> telemetry.inc(NAMESPACE, "capture_expression_timeout", 1);
   rescue -> telemetry.report(exc, description: "DI capture-expression evaluation failed"))
   plus the corresponding logger.debug call on the rescue path.

The two failing-edge tests previously relied on telemetry's nil default,
so the rescue and timeout branches were never verified.

Co-Authored-By: Claude <noreply@anthropic.com>
Add YARD docs to every new public attribute, constructor parameter,
and module function added by the capture-expressions PR so a reader
can determine the type, shape, and nil-meaning of each value from
the source file alone, without opening other files.

- capture_expression.rb: @param tags on initialize; attr_reader
  comments giving type and nil semantics for name/expr/limits.
- capture_limits.rb: @param tags on initialize and self.resolve;
  attr_reader comments naming the integer-or-nil contract per field
  and what nil means at each level of the resolve chain.
- capture_expression_evaluator.rb: attr_reader comments naming the
  concrete types of settings/serializer/logger/telemetry and how
  each is consumed at evaluate-time.
- probe.rb: @param/@return on snapshot_serializer_limits, naming
  the resolved Hash{Symbol => Integer} shape (no nils in the output
  even though probe-level fields may be nil).
- probe_builder.rb: full YARD docs for build_capture_expressions and
  build_capture_limits, including @raise enumeration.
- probe_notification_builder.rb: @param tags on initialize; attr_reader
  comments for the new logger/telemetry/capture_expression_evaluator
  surfaces.
- serializer.rb: @param/@return added to serialize_args and
  serialize_vars, naming the new length:/collection_size: nil semantics
  added by this PR.

Co-Authored-By: Claude <noreply@anthropic.com>
Code comments in the capture-expressions PR referenced design docs in
an internal planning repo (paths like projects/capture-expressions/design/decisions.md)
and used bare decision identifiers (D1, D3, D4, D5) that only resolve
inside that planning repo. The references would 404 for any
DataDog/dd-trace-rb reader.

For each occurrence, either the surrounding text already conveyed the
intent (drop the "see <private-path>" sentence) or the cross-tracer
comparison was sourced inline (Python/Java/Go DI) so the reference was
redundant. No information loss for someone reading only the public
source.

Files updated:
- capture_expression_evaluator.rb class docstring (two paths + "see D4")
- probe.rb rate_limit comment ("planning repo" sentence)
- probe_builder.rb mutual-exclusion comment (one path + "(D1)")
- probe_notification_builder.rb mutual-exclusion comment (one path + "(D1)")
- probe_notification_builder.rb evaluation-errors merge comment (one path)

Co-Authored-By: Claude <noreply@anthropic.com>
…n_spec

Replace `untyped` placeholders in the capture-expressions RBS sigs with
the concrete types already established in the rest of DI:

- settings: Datadog::Core::Configuration::Settings
  (was untyped on CaptureExpressionEvaluator, CaptureLimits.resolve,
  Probe#snapshot_serializer_limits)
- logger: DI::Logger
  (was untyped on CaptureExpressionEvaluator, ProbeNotificationBuilder;
  Component#initialize wraps the Core::Logger into DI::Logger before
  passing it down, matching Instrumenter / ProbeManager sigs)
- telemetry: Core::Telemetry::Component
  (was untyped on CaptureExpressionEvaluator and ProbeNotificationBuilder;
  the earlier fix commit already made the constructors non-nil-required,
  so the sigs drop the trailing `?`)

Steep still passes on lib/datadog/di/ after the tightening.

Also remove a trailing blank line in spec/datadog/di/capture_expression_spec.rb
flagged by Layout/TrailingEmptyLines on CI.

Co-Authored-By: Claude <noreply@anthropic.com>
…nature

The earlier commit that made `logger` a positional arg and `telemetry:`
a required kwarg on ProbeNotificationBuilder updated the unit spec but
missed this integration spec, which uses `described_class.new` rather
than `ProbeNotificationBuilder.new` (so the earlier grep didn't catch it).

All 6 examples were failing with:
  ArgumentError: wrong number of arguments (given 2, expected 3;
    required keyword: telemetry)

Adds logger/telemetry doubles via the same as_null_object pattern used
in the unit spec.

Co-Authored-By: Claude <noreply@anthropic.com>
…tional telemetry

Adds five integration tests covering the customer-observable behaviors
the PR claims, which were previously only verified at unit level:

1. method probe mutual exclusion — both captureSnapshot=true and
   captureExpressions set on the probe; asserts the snapshot carries
   the `arguments` block and `captureExpressions` is absent from both
   entry and return blocks.
2. method probe timeout — sets max_time_to_serialize_ms=0; asserts
   captureExpressions = {"arg1" => {notCapturedReason: "timeout"}} on
   both entry and return.
3. method probe with raising expression — uses len(undefined) which
   raises ExpressionEvaluationError; asserts the key is omitted from
   captureExpressions and an entry appears in the snapshot's top-level
   evaluationErrors array.
4. line probe with capture expression — uses the existing line-probe
   integration test class; asserts captures.lines[40].captureExpressions
   contains the resolved local variable value.

Writing these tests surfaced a regression from the earlier
"make telemetry required" commit: Component.build(...) legitimately
accepts a nil telemetry kwarg and threads nil through to
ProbeNotificationBuilder, which then constructed CaptureExpressionEvaluator
with telemetry: nil. The earlier fix removed the &. guards on the two
telemetry call sites (timeout inc, rescue report), causing NoMethodError
in the integration tests where no telemetry component is configured.

Restores telemetry as a truly-optional dependency on both
CaptureExpressionEvaluator and ProbeNotificationBuilder:

- `telemetry: nil` default
- `telemetry&.inc(...)` / `telemetry&.report(...)` at the call sites
- RBS sigs `Core::Telemetry::Component?` (nullable)
- attr_reader doc comments name the nil case and how it is handled

The unit specs (which pass `instance_double(...).as_null_object`) are
unaffected — &. on a non-nil null-object still dispatches.

Co-Authored-By: Claude <noreply@anthropic.com>
@p-datadog p-datadog force-pushed the di-capture-expressions branch from 8b413eb to 1bf6c26 Compare June 9, 2026 14:01
p-ddsign added 9 commits June 9, 2026 10:05
The capture_expression_evaluator reads
settings.dynamic_instrumentation.max_time_to_serialize_ms but the RBS
interface for the dynamic_instrumentation namespace did not declare it,
so steep flagged it as Ruby::NoMethod on
lib/datadog/di/capture_expression_evaluator.rb:69.

The option is defined in lib/datadog/di/configuration.rb as type :int
with default 200, matching the Integer signature used for the other
max_* options in the interface.

Verified locally: bundle exec steep check
lib/datadog/di/capture_expression_evaluator.rb reports no errors.
Capture expressions are evaluated once, at method return — the only
point where args, kwargs, self, @return, and exception are all
observable. The previous code emitted the return-time captured_block
under BOTH the entry and return keys of the snapshot's captures map.

The snapshot schema's entry/return split exists for captureSnapshot,
which freezes args before super (entry) and reads self/@return after
(return). For captureExpressions there is no entry-time evaluation,
so putting the return-time hash under the entry key mislabeled
exit-time data as entry-time state. A customer expression like `arg1`
on a method that mutates its argument would appear under entry with
the post-mutation value — directly contradicting what entry means
under captureSnapshot.

Stop emitting the entry block for method-probe captureExpressions.
Line-probe captureExpressions are unchanged (single block, no
entry/return distinction). captureSnapshot is unchanged.

Tests updated to assert the entry key is absent for method probes
with capture_expressions only.

Verified locally:
  bundle exec rspec spec/datadog/di/probe_notification_builder_spec.rb
  bundle exec rspec spec/datadog/di/integration/instrumentation_spec.rb
  bundle exec steep check lib/datadog/di/probe_notification_builder.rb
…rd combine_args

Three follow-ups from the capture-expressions review:

1. Telemetry counter renamed from `capture_expression_timeout` to
   `capture_expressions_skipped_by_timeout`. The metric is per-expression
   (a single probe fire with N timed-out expressions increments by N).
   The new name reflects the unit. Comment added at the increment site.

2. New test in capture_expression_evaluator_spec exercising the
   realistic mid-loop case: one expression evaluates successfully, then
   the deadline check at the top of the next iteration fires. The
   existing "exhausted before evaluating any" context (budget = 0) only
   covered the trivial degenerate case. The new test stubs
   Process.clock_gettime with and_wrap_original so non-:nanosecond
   callers (Time.now via :float_second) still hit the real impl.

3. Skip serializer.combine_args in the method-probe return path when
   captureSnapshot is also set on the probe. Mutual exclusion means the
   combined hash is discarded at build time; the call was wasted work.
   The guard now mirrors the build-time precedence.

Verified locally:
  bundle exec rspec spec/datadog/di/capture_expression_evaluator_spec.rb
  bundle exec rspec spec/datadog/di/integration/instrumentation_spec.rb
  bundle exec rspec spec/datadog/di/probe_notification_builder_spec.rb
  bundle exec steep check lib/datadog/di/instrumenter.rb lib/datadog/di/capture_expression_evaluator.rb
All green.
Capture expressions on method probes now respect the probe's
evaluateAt field, matching Python, .NET, and PHP. Before this commit
the value was parsed off the RC payload but ignored: every
capture-expression block went under captures.return, irrespective of
evaluateAt.

Probe.evaluate_at — new attribute. Accepts :entry / :exit / nil; nil
coerces to :exit (matching the libdatadog ProbeCommon JSON-parse
default of EvaluateAt::Exit). Unknown symbols raise ArgumentError at
construction. Line probes ignore the value (single firing point at the
TracePoint callback).

ProbeBuilder.parse_evaluate_at — new helper. Maps the RC payload's
'ENTRY' / 'EXIT' strings to :entry / :exit. 'DEFAULT' (sent by the
Java backend tooling) maps to :exit. Absent or unrecognized values
coerce to :exit and emit a debug log; the runtime never raises on an
unknown evaluateAt because such payloads should still install as
conventional EXIT-timed probes.

Instrumenter#hook_method — for an evaluate_at: :entry method probe,
evaluate the capture expressions against the pre-super scope
(self / args / kwargs only, since @return / @duration / @exception
are nil at this point) and stash the result on the exit-time Context.
Mirrors the existing serialized_entry_args pattern.

Context — new entry_capture_expressions and
entry_capture_evaluation_errors fields, populated by the entry hook
and consumed by ProbeNotificationBuilder.

ProbeNotificationBuilder#build_snapshot — for method probes:
evaluate_at: :entry emits under captures.entry only, consuming the
pre-computed block from Context (does not re-evaluate against
exit-time scope); evaluate_at: :exit (default) emits under
captures.return only, evaluating against exit-time scope. Never both.
Line probes emit under captures.lines.<n>.captureExpressions as
before; evaluateAt is N/A there.

Specs: Probe — evaluate_at keyword acceptance, default coercion, and
ArgumentError on unknown values. ProbeBuilder — evaluateAt parsing for
ENTRY / EXIT / DEFAULT / absent / unknown. ProbeNotificationBuilder —
evaluate_at: :entry consumes the pre-computed block, merges entry-time
evaluationErrors, emits empty {} when nothing was stashed.
Integration — uses InstrumentationSpecTestClass#mutating_method to
exercise the entry-vs-exit distinction: an :entry probe captures the
pre-mutation arg value, an :exit probe captures the post-mutation
value, demonstrating the timing semantics end-to-end.

Steep clean. 860 / 860 DI specs pass (8 pre-existing pending).
Replace String.new('literal') with unary +'literal' in two test
sites added by the evaluateAt commit. The unary + form produces an
unfrozen string literal without an extra constructor call, which is
what StandardRB's Performance/UnfreezeString cop requires.

Both sites use the mutable string to test that mutating_method's
in-place sub! changes the entry vs return-time visible value, so the
mutability requirement is unchanged.

Verified locally: bundle exec rake standard reports no offenses.
Mirror the condition-evaluation rescue pattern used a few lines above
in the same method. The evaluator itself catches per-expression
StandardError; this outer rescue covers non-evaluator failures
(building the entry Context, combine_args) so a raise during entry-time
evaluation no longer escapes the instrumented method body.

propagate_all_exceptions still surfaces failures in the test suite.
Otherwise the failure is logged at debug + reported to telemetry, and
the method continues with no entry block stashed (the snapshot will
omit captures.entry.captureExpressions for this fire, matching the
'no entry block was stashed' fallback already covered by spec).
Two new integration tests cover the bug-fix paths the PR description
calls out but the existing maxLength test did not exercise:

  - Method probe, captureSnapshot=true, max_capture_collection_size=2:
    verifies array truncation in entry args, @return, and self ivar.
    Adds InstrumentationSpecTestClass#collection_method as the target.

  - Line probe at instrumentation_integration_test_class.rb:40,
    captureSnapshot=true, max_capture_collection_size=1: verifies the
    redacted hash local is truncated via the line-probe locals path
    (Context#serialized_locals → serialize_vars → probe-level limits).

The earlier maxLength test covered method probes only. These two close
the maxCollectionSize and line-probe-locals gaps the PR description
claims as fixed.
CaptureExpression's RBS signature declares expr as a non-nil
DI::EL::Expression. The capture-expression probe specs only exercise
storage and probe.capture_expressions? — they never evaluate the
expression — so a typed stub keeps the type contract honest without
changing what the tests verify.
@p-datadog p-datadog marked this pull request as ready for review June 15, 2026 22:13
@p-datadog p-datadog requested review from a team as code owners June 15, 2026 22:13
@p-datadog p-datadog requested a review from Copilot June 15, 2026 22:13
@p-datadog p-datadog marked this pull request as draft June 15, 2026 22:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 5 comments.

Comment on lines +17 to +18
# @param logger [Datadog::Core::Logger] logger forwarded to the internal
# CaptureExpressionEvaluator for per-expression evaluation failures.
attr_reader :serializer

# Logger; passed through to CaptureExpressionEvaluator at construction.
# @return [Datadog::Core::Logger]
Comment on lines +49 to +50
# Lazily-constructed sub-component that evaluates probe.capture_expressions
# during build_snapshot when probe.capture_expressions? is true.
Comment on lines +26 to +29
# @param settings [Datadog::Core::Configuration::Settings]
# @param serializer [Datadog::DI::Serializer]
# @param logger [Datadog::Core::Logger]
# @param telemetry [Datadog::Core::Telemetry::Component, nil] nil is
Comment on lines +50 to +53
# Logger used for debug-level reporting of per-expression evaluation
# failures.
# @return [Datadog::Core::Logger]
attr_reader :logger

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7cf491474a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +210 to +214
entry_context = Context.new(
probe: probe, settings: settings, serializer: serializer,
target_self: self,
locals: serializer.combine_args(args, kwargs, self),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make @duration nil-safe for entry capture expressions

For method probes with evaluateAt: ENTRY, any capture expression that references @duration (even isUndefined(@duration)) is evaluated against this entry context before a duration exists. The EL compiler expands @duration to context.duration * 1000, so the nil duration raises and the capture key is omitted with an evaluation error instead of behaving like the documented nil/undefined entry-time value. Please either make the @duration expansion nil-safe or avoid evaluating it in the entry context.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos core Involves Datadog core libraries debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants