Skip to content

DEBUG-4675 DI: fix off-by-one in serializer max capture depth#5753

Merged
p-datadog merged 4 commits into
masterfrom
fix-debug-4675-max-capture-depth-off-by-one
May 13, 2026
Merged

DEBUG-4675 DI: fix off-by-one in serializer max capture depth#5753
p-datadog merged 4 commits into
masterfrom
fix-debug-4675-max-capture-depth-off-by-one

Conversation

@p-datadog

@p-datadog p-datadog commented May 13, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Fixes an off-by-one bug in the Dynamic Instrumentation snapshot serializer: with max_capture_depth=N, the serializer was capturing N+1 levels of nested objects (the top-level value plus N nested levels) instead of N levels.

The truncation check is in three branches of Serializer#serialize_valueArray, Hash, and the catch-all object branch. Each used if depth < 0, which only fires after one extra recursion. Changed to if depth <= 0 so truncation fires when the next recursion would exceed the configured depth.

Motivation:

System test Test_Debugger_Line_Probe_Snaphots::test_default_max_reference_depth (added in DataDog/system-tests#5624) is filed against Ruby as bug (DEBUG-4675) with the note "Ruby has off-by-one bug: captures 4 levels instead of 3". The test sets max_capture_depth=3 (default) and asserts exactly 3 levels of nesting are captured before truncation.

The previous behavior was inconsistent with the default-depth documentation and with other Datadog tracer libraries.

Change log entry

Yes. Dynamic Instrumentation: Fix off-by-one in max_capture_depth so snapshots respect the configured nesting limit exactly (previously captured one extra level).

Additional Notes:

Three existing "depth exceeded" cases in spec/datadog/di/serializer_spec.rb tested the buggy behavior (inputs nested one level deeper than max_capture_depth=2). Inputs are reduced by one level so the truncation hits the typed value (Array/Hash/Object) the test name promises.

The serializer test setup in spec/datadog/di/serializer_helper.rb uses max_capture_depth=2. Other DI specs that mock max_capture_depth=2 (instrumenter_spec, probe_notification_builder_spec, integration/probe_notification_builder_spec) only exercise objects with at most one level of nesting and remain unaffected.

Companion system-tests PR (draft) removing the bug marker: DataDog/system-tests#6918.

System-tests validation result:

A force-run entry was added to .github/forced-tests-list.cfg to exercise test_default_max_reference_depth in this PR's End-to-end CI runs (commit 1392e5ecd8). Results on commit 161ab4a373:

  • rails72: 21/21 SUCCESS
  • rails80: 21/21 SUCCESS
  • uds-rails: 22/22 SUCCESS

The forced test passed on every End-to-end run against the manifest-supported weblogs (the three the test's parent declaration is scoped to). The force-list format has no weblog scoping, so the entry also ran the test against weblogs the manifest marks irrelevant (rack, sinatra14, sinatra22/32/41, rails42, rails52, rails61, uds-sinatra) — those failed at Failed to get /debugger/init: expected status code: 200, actual status code: 404 because those weblogs do not have the debugger controller endpoints. Those failures are not fix-related.

The force entry has been reverted in commit 36995dec85 now that the supported-weblog evidence is captured. Going forward, the test re-activates through the companion system-tests PR DataDog/system-tests#6918.

How to test the change?

  • bundle exec rake test:di — the three updated "depth exceeded" cases in serializer_spec.rb cover the new truncation point for Array, Hash, and Object.
  • System tests: ./run.sh DEBUGGER_PROBES_SNAPSHOT --library ruby — the supported-weblog runs in this PR's CI (links above) show the forced test passing on each.

The Serializer truncation check used `depth < 0`, causing one extra
level of nesting to be captured. With `max_capture_depth=N`, this
captured N+1 levels (top object plus N nested) instead of N levels.

Change the check to `depth <= 0` in the Array, Hash, and Object
branches so truncation fires when the next recursion would exceed
the configured depth. With the default `max_capture_depth=3`, snapshots
now contain 3 levels of nesting, matching the documented behavior and
the equivalent behavior in other tracer libraries.

System test `tests/debugger/test_debugger_probe_snapshot.py
::Test_Debugger_Line_Probe_Snaphots::test_default_max_reference_depth`
covers this case.

Existing `serializer_spec.rb` "depth exceeded" cases are updated to
reflect the corrected truncation point.
@p-datadog
p-datadog requested a review from a team as a code owner May 13, 2026 14:05
@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 May 13, 2026
@dd-octo-sts dd-octo-sts Bot added the debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database) label May 13, 2026

@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: 7843c90448

ℹ️ 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/serializer.rb
Forces the system-tests assertion covered by DEBUG-4675 to run in this
PR's CI, even though manifests/ruby.yml still marks it as `bug
(DEBUG-4675)`. Remove this entry before merging — the companion PR
DataDog/system-tests#6918 removes the bug marker once merged.
@datadog-official

datadog-official Bot commented May 13, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 97.15% (-0.01%)

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

Address review comment 3234907136:
the AR custom serializer's `depth: depth - 1` was effectively compensating
for the off-by-one this PR fixes in `lib/datadog/di/serializer.rb`. The
synthetic `{attributes:, new_record:}` hash is a transparent representation
of the AR model's fields, not a real additional nesting level — `serialize_value`
already decrements depth as it recurses into Array/Hash/object entries.
With the off-by-one fixed in serializer.rb, the AR decrement now over-
truncates: at the default `max_capture_depth=2`, `:attributes` hits the
new `<= 0` truncation and becomes `notCapturedReason` instead of being
captured down to leaf values.

- Pass `depth: depth` instead of `depth: depth - 1` in lib/datadog/di/contrib/active_record.rb:40
- Reworded the depth guidance in the surrounding doc comment so the
  custom-serializer contract no longer prescribes a `- 1` that depended on
  the buggy semantics.

The existing AR spec in spec/datadog/di/contrib/active_record/serializer_active_record_spec.rb
exercises this at the default max_capture_depth=2 and expects :attributes
captured down to leaf values; that assertion shape is preserved.
@pr-commenter

pr-commenter Bot commented May 13, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-05-13 15:32:32

Comparing candidate commit 161ab4a in PR branch fix-debug-4675-max-capture-depth-off-by-one with baseline commit a848ac1 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 merged commit 06846fb into master May 13, 2026
582 checks passed
@p-datadog
p-datadog deleted the fix-debug-4675-max-capture-depth-off-by-one branch May 13, 2026 16:42
@dd-octo-sts dd-octo-sts Bot added this to the 2.34.0 milestone May 13, 2026
p-datadog pushed a commit to DataDog/system-tests that referenced this pull request May 13, 2026
Replace the unconditional removal with a `component_version: <2.34.0`
gate, matching the pattern used for DEBUG-3747 / DEBUG-4343. The
off-by-one bug exists in all released dd-trace-rb versions through
2.33.0; the fix in DataDog/dd-trace-rb#5753 lands on master at 2.34.0.

For tracer versions <2.34.0 the bug marker still applies. For 2.34.0+
the test runs and passes (force-run validated this on rails72/rails80/
uds-rails End-to-end CI in dd-trace-rb#5753, commit 161ab4a373).
@Strech Strech mentioned this pull request May 27, 2026
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 debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants