Skip to content

Commit dec8c8f

Browse files
committed
Add test for debugger snapshot default capture limits
Add comprehensive test to verify that tracers correctly apply default capture limits when no capture property is specified in the probe configuration: - `maxReferenceDepth`: 3 - `maxCollectionSize`: 100 - `maxFieldCount`: 20 - `maxLength`: 255
1 parent b8f7990 commit dec8c8f

File tree

19 files changed

+650
-5
lines changed

19 files changed

+650
-5
lines changed

tests/debugger/test_debugger_probe_snapshot.py

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import tests.debugger.utils as debugger
77

88

9-
from utils import scenarios, features, missing_feature, context, irrelevant, bug
9+
from utils import scenarios, features, missing_feature, context, irrelevant, bug, logger
1010
from utils.interfaces._library.miscs import validate_process_tags
1111

1212

@@ -209,6 +209,12 @@ def _validate_snapshots(self):
209209
class Test_Debugger_Line_Probe_Snaphots(BaseDebuggerProbeSnaphotTest):
210210
"""Tests for line-level probe snapshots"""
211211

212+
# Default snapshot capture limits
213+
DEFAULT_MAX_REFERENCE_DEPTH = 3
214+
DEFAULT_MAX_COLLECTION_SIZE = 100
215+
DEFAULT_MAX_FIELD_COUNT = 20
216+
DEFAULT_MAX_LENGTH = 255
217+
212218
### log probe ###
213219
def setup_log_line_snapshot(self):
214220
self._setup("probe_snapshot_log_line", "/debugger/log", "log", lines=None)
@@ -218,6 +224,167 @@ def test_log_line_snapshot(self):
218224
self._assert()
219225
self._validate_snapshots()
220226

227+
def setup_default_max_reference_depth(self):
228+
"""Setup test for default maxReferenceDepth"""
229+
self._setup_default_capture_limits()
230+
231+
def setup_default_max_field_count(self):
232+
"""Setup test for default maxFieldCount"""
233+
self._setup_default_capture_limits()
234+
235+
def setup_default_max_collection_size(self):
236+
"""Setup test for default maxCollectionSize"""
237+
self._setup_default_capture_limits()
238+
239+
def setup_default_max_length(self):
240+
"""Setup test for default maxLength"""
241+
self._setup_default_capture_limits()
242+
243+
def _setup_default_capture_limits(self):
244+
"""Shared setup method for default capture limit tests"""
245+
test_depth = self.DEFAULT_MAX_REFERENCE_DEPTH + 7
246+
test_fields = self.DEFAULT_MAX_FIELD_COUNT + 30
247+
test_collection_size = self.DEFAULT_MAX_COLLECTION_SIZE + 100
248+
test_string_length = self.DEFAULT_MAX_LENGTH + 1000
249+
250+
# Get the line number dynamically based on the language
251+
lines = self.method_and_language_to_line_number("SnapshotLimits", context.library.name)
252+
253+
self._setup(
254+
"probe_snapshot_default_capture_limits",
255+
f"/debugger/snapshot/limits?"
256+
f"depth={test_depth}&"
257+
f"fields={test_fields}&"
258+
f"collectionSize={test_collection_size}&"
259+
f"stringLength={test_string_length}",
260+
"log",
261+
lines=lines,
262+
)
263+
264+
def _get_snapshot_locals_variable(self, variable_name: str) -> dict:
265+
"""Helper method to extract a specific local variable from snapshot for default capture limit tests"""
266+
self._assert()
267+
self._validate_snapshots()
268+
269+
for probe_id in self.probe_ids:
270+
if probe_id not in self.probe_snapshots:
271+
raise ValueError(f"Snapshot {probe_id} was not received.")
272+
273+
snapshots = self.probe_snapshots[probe_id]
274+
if not snapshots:
275+
raise ValueError(f"No snapshots found for probe {probe_id}")
276+
277+
snapshot = snapshots[0]
278+
debugger_snapshot = snapshot.get("debugger", {}).get("snapshot") or snapshot.get("debugger.snapshot")
279+
280+
if not debugger_snapshot:
281+
raise ValueError(f"Snapshot data not found in expected format for probe {probe_id}")
282+
if "captures" not in debugger_snapshot:
283+
raise ValueError(f"No captures found in snapshot for probe {probe_id}")
284+
285+
captures = debugger_snapshot["captures"]
286+
if "lines" in captures:
287+
lines = captures["lines"]
288+
if isinstance(lines, dict) and len(lines) == 1:
289+
line_key = next(iter(lines))
290+
line_data = lines[line_key]
291+
else:
292+
raise ValueError(f"Expected 'lines' to be a dict with a single key, got: {len(lines)}")
293+
294+
if line_data and "locals" in line_data:
295+
locals_data = line_data["locals"]
296+
assert variable_name in locals_data, f"'{variable_name}' is missing from snapshot locals"
297+
return locals_data[variable_name]
298+
299+
raise ValueError("No locals data found in snapshot")
300+
301+
def _measure_captured_depth(self, obj: dict, current_depth: int = 1) -> int:
302+
"""Measure the actual depth captured before truncation (notCapturedReason='depth')"""
303+
fields = obj["fields"]
304+
assert isinstance(fields, dict), f"Expected 'fields' to be a dict, got: {type(fields)}"
305+
expected_nested_key = "@nested" if context.library.name == "ruby" else "nested"
306+
assert (
307+
expected_nested_key in fields
308+
), f"Expected '{expected_nested_key}' to be present in the 'fields' object, got: {list(fields.keys())}"
309+
nested = fields[expected_nested_key]
310+
assert isinstance(nested, dict), f"Expected 'nested' to be a dict, got: {type(nested)}"
311+
312+
if "notCapturedReason" in nested:
313+
assert (
314+
nested["notCapturedReason"] == "depth"
315+
), f"Expected notCapturedReason to be 'depth', got: {nested['notCapturedReason']}"
316+
return current_depth
317+
318+
return self._measure_captured_depth(nested, current_depth + 1)
319+
320+
@bug(
321+
context.library.name == "nodejs", reason="DEBUG-4611"
322+
) # Node.js does apply the correct default, but fails if no `capture` object is present
323+
@bug(
324+
context.library.name == "ruby", reason="DEBUG-0000"
325+
) # TODO: Add real JIRA ticket: Ruby has off-by-one bug: captures 4 levels instead of 3
326+
def test_default_max_reference_depth(self):
327+
"""Test that the tracer uses default maxReferenceDepth=3 when capture property is omitted"""
328+
deep_object = self._get_snapshot_locals_variable("deepObject")
329+
actual_depth = self._measure_captured_depth(deep_object)
330+
assert (
331+
actual_depth == self.DEFAULT_MAX_REFERENCE_DEPTH
332+
), f"deepObject should have been captured with {self.DEFAULT_MAX_REFERENCE_DEPTH} levels. Got: {actual_depth}"
333+
334+
@bug(
335+
context.library.name == "nodejs", reason="DEBUG-4611"
336+
) # Node.js does apply the correct default, but fails if no `capture` object is present
337+
def test_default_max_field_count(self):
338+
"""Test that the tracer uses default maxFieldCount=20 when capture property is omitted"""
339+
many_fields = self._get_snapshot_locals_variable("manyFields")
340+
assert (
341+
many_fields.get("notCapturedReason") == "fieldCount"
342+
), f"manyFields should have notCapturedReason='fieldCount'. Got: {many_fields.get('notCapturedReason')}"
343+
344+
captured_count = len(many_fields["fields"])
345+
assert (
346+
captured_count == self.DEFAULT_MAX_FIELD_COUNT
347+
), f"manyFields should have exactly {self.DEFAULT_MAX_FIELD_COUNT} fields captured. Got: {captured_count}"
348+
349+
@bug(
350+
context.library.name == "nodejs", reason="DEBUG-4611"
351+
) # Node.js does apply the correct default, but fails if no `capture` object is present
352+
def test_default_max_collection_size(self):
353+
"""Test that the tracer uses default maxCollectionSize=100 when capture property is omitted"""
354+
large_collection = self._get_snapshot_locals_variable("largeCollection")
355+
assert (
356+
large_collection.get("notCapturedReason") == "collectionSize"
357+
), f"largeCollection should have notCapturedReason='collectionSize'. Got: {large_collection.get('notCapturedReason')}"
358+
359+
actual_size = large_collection.get("size")
360+
if isinstance(actual_size, str) and context.library.name == "java":
361+
# TODO: Create JIRA ticket: Java size property is a string! Expected an int
362+
logger.warning("size property is a string! Expected an int")
363+
actual_size = int(actual_size)
364+
expected_collection_size = self.DEFAULT_MAX_COLLECTION_SIZE + 100
365+
assert (
366+
actual_size == expected_collection_size
367+
), f"largeCollection should report size={expected_collection_size}. Got: {actual_size}"
368+
369+
captured_count = len(large_collection["elements"])
370+
assert (
371+
captured_count == self.DEFAULT_MAX_COLLECTION_SIZE
372+
), f"largeCollection should have exactly {self.DEFAULT_MAX_COLLECTION_SIZE} elements. Got: {captured_count}"
373+
374+
@bug(
375+
context.library.name == "nodejs", reason="DEBUG-4611"
376+
) # Node.js does apply the correct default, but fails if no `capture` object is present
377+
@bug(
378+
context.library.name == "dotnet", reason="DEBUG-0000"
379+
) # TODO: Add real JIRA ticket: .NET uses a different default maxLength (1000)
380+
def test_default_max_length(self):
381+
"""Test that the tracer uses default maxLength=255 when capture property is omitted"""
382+
long_string = self._get_snapshot_locals_variable("longString")
383+
string_value = long_string["value"]
384+
assert (
385+
len(string_value) <= self.DEFAULT_MAX_LENGTH
386+
), f"longString should have length {self.DEFAULT_MAX_LENGTH}. Got: {len(string_value)}"
387+
221388
def setup_log_line_snapshot_debug_track(self):
222389
self.use_debugger_endpoint = True
223390
self._setup("probe_snapshot_log_line", "/debugger/log", "log", lines=None)

tests/debugger/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def method_and_language_to_line_number(self, method: str, language: str) -> list
139139
"StringOperations": {"java": [87], "dotnet": [97], "python": [96], "nodejs": [96]},
140140
"CollectionOperations": {"java": [114], "dotnet": [114], "python": [123], "nodejs": [120]},
141141
"Nulls": {"java": [130], "dotnet": [127], "python": [136], "nodejs": [126]},
142+
"SnapshotLimits": {"java": [153], "python": [172], "nodejs": [136], "ruby": [78], "dotnet": [150]},
142143
}
143144

144145
return definitions.get(method, {}).get(language, [])
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"language": "",
4+
"type": "",
5+
"id": "",
6+
"version": 0,
7+
"captureSnapshot": true,
8+
"where": {
9+
"typeName": null,
10+
"sourceFile": "ACTUAL_SOURCE_FILE",
11+
"lines": [
12+
"0"
13+
]
14+
}
15+
}
16+
]
17+

utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,17 @@ public IActionResult Budgets(int loops)
137137
}
138138
return Content("Budgets");
139139
}
140+
141+
[HttpGet("snapshot/limits")]
142+
[Consumes("application/json", "application/xml")]
143+
public IActionResult SnapshotLimits(int depth = 0, int collectionSize = 0, int stringLength = 0)
144+
{
145+
var data = DataGenerator.GenerateTestData(depth, collectionSize, stringLength);
146+
var deepObject = data["deepObject"];
147+
var manyFields = data["manyFields"];
148+
var largeCollection = data["largeCollection"];
149+
var longString = data["longString"];
150+
return Content("Capture limits probe"); // must be line 150
151+
}
140152
}
141153
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System.Collections.Generic;
2+
3+
namespace weblog.Models.Debugger
4+
{
5+
public class NestedObject
6+
{
7+
public int level { get; set; }
8+
public NestedObject nested { get; set; }
9+
}
10+
11+
public class ManyFieldsObject
12+
{
13+
// Define 50 public properties to test maxFieldCount
14+
public int field0 { get; set; } = 0;
15+
public int field1 { get; set; } = 1;
16+
public int field2 { get; set; } = 2;
17+
public int field3 { get; set; } = 3;
18+
public int field4 { get; set; } = 4;
19+
public int field5 { get; set; } = 5;
20+
public int field6 { get; set; } = 6;
21+
public int field7 { get; set; } = 7;
22+
public int field8 { get; set; } = 8;
23+
public int field9 { get; set; } = 9;
24+
public int field10 { get; set; } = 10;
25+
public int field11 { get; set; } = 11;
26+
public int field12 { get; set; } = 12;
27+
public int field13 { get; set; } = 13;
28+
public int field14 { get; set; } = 14;
29+
public int field15 { get; set; } = 15;
30+
public int field16 { get; set; } = 16;
31+
public int field17 { get; set; } = 17;
32+
public int field18 { get; set; } = 18;
33+
public int field19 { get; set; } = 19;
34+
public int field20 { get; set; } = 20;
35+
public int field21 { get; set; } = 21;
36+
public int field22 { get; set; } = 22;
37+
public int field23 { get; set; } = 23;
38+
public int field24 { get; set; } = 24;
39+
public int field25 { get; set; } = 25;
40+
public int field26 { get; set; } = 26;
41+
public int field27 { get; set; } = 27;
42+
public int field28 { get; set; } = 28;
43+
public int field29 { get; set; } = 29;
44+
public int field30 { get; set; } = 30;
45+
public int field31 { get; set; } = 31;
46+
public int field32 { get; set; } = 32;
47+
public int field33 { get; set; } = 33;
48+
public int field34 { get; set; } = 34;
49+
public int field35 { get; set; } = 35;
50+
public int field36 { get; set; } = 36;
51+
public int field37 { get; set; } = 37;
52+
public int field38 { get; set; } = 38;
53+
public int field39 { get; set; } = 39;
54+
public int field40 { get; set; } = 40;
55+
public int field41 { get; set; } = 41;
56+
public int field42 { get; set; } = 42;
57+
public int field43 { get; set; } = 43;
58+
public int field44 { get; set; } = 44;
59+
public int field45 { get; set; } = 45;
60+
public int field46 { get; set; } = 46;
61+
public int field47 { get; set; } = 47;
62+
public int field48 { get; set; } = 48;
63+
public int field49 { get; set; } = 49;
64+
}
65+
66+
public static class DataGenerator
67+
{
68+
public static Dictionary<string, object> GenerateTestData(int depth, int collectionSize, int stringLength)
69+
{
70+
var result = new Dictionary<string, object>();
71+
72+
// Generate deeply nested object (tests maxReferenceDepth)
73+
result["deepObject"] = depth > 0 ? CreateNestedObject(depth) : null;
74+
75+
// Generate object with many fields (tests maxFieldCount)
76+
result["manyFields"] = new ManyFieldsObject();
77+
78+
// Generate large collection (tests maxCollectionSize)
79+
var largeCollection = new List<int>();
80+
for (int i = 0; i < collectionSize; i++)
81+
{
82+
largeCollection.Add(i);
83+
}
84+
result["largeCollection"] = largeCollection;
85+
86+
// Generate long string (tests maxLength)
87+
result["longString"] = stringLength > 0 ? new string('A', stringLength) : "";
88+
89+
return result;
90+
}
91+
92+
private static NestedObject CreateNestedObject(int maxLevel, int level = 1)
93+
{
94+
var obj = new NestedObject
95+
{
96+
level = level,
97+
nested = level < maxLevel ? CreateNestedObject(maxLevel, level + 1) : null
98+
};
99+
100+
return obj;
101+
}
102+
}
103+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.datadoghq.system_tests.springboot;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
class NestedObject {
9+
public int level;
10+
public NestedObject nested;
11+
12+
public NestedObject(int level, NestedObject nested) {
13+
this.level = level;
14+
this.nested = nested;
15+
}
16+
}
17+
18+
class ManyFieldsObject {
19+
// Class with 50 public fields to test maxFieldCount, we can't test generate these dynamically so we just always have 50 fields
20+
public Integer field0 = 0, field1 = 1, field2 = 2, field3 = 3, field4 = 4,
21+
field5 = 5, field6 = 6, field7 = 7, field8 = 8, field9 = 9,
22+
field10 = 10, field11 = 11, field12 = 12, field13 = 13, field14 = 14,
23+
field15 = 15, field16 = 16, field17 = 17, field18 = 18, field19 = 19,
24+
field20 = 20, field21 = 21, field22 = 22, field23 = 23, field24 = 24,
25+
field25 = 25, field26 = 26, field27 = 27, field28 = 28, field29 = 29,
26+
field30 = 30, field31 = 31, field32 = 32, field33 = 33, field34 = 34,
27+
field35 = 35, field36 = 36, field37 = 37, field38 = 38, field39 = 39,
28+
field40 = 40, field41 = 41, field42 = 42, field43 = 43, field44 = 44,
29+
field45 = 45, field46 = 46, field47 = 47, field48 = 48, field49 = 49;
30+
}
31+
32+
public class DataGenerator {
33+
public static Map<String, Object> generateTestData(int depth, int collectionSize, int stringLength) {
34+
Map<String, Object> result = new HashMap<>();
35+
36+
// Generate deeply nested object (tests maxReferenceDepth)
37+
result.put("deepObject", depth > 0 ? createNestedObject(depth, 1) : null);
38+
39+
// Generate object with many fields (tests maxFieldCount)
40+
result.put("manyFields", new ManyFieldsObject());
41+
42+
// Generate large collection (tests maxCollectionSize)
43+
List<Integer> largeCollection = new ArrayList<>();
44+
for (int i = 0; i < collectionSize; i++) {
45+
largeCollection.add(i);
46+
}
47+
result.put("largeCollection", largeCollection);
48+
49+
// Generate long string (tests maxLength)
50+
result.put("longString", stringLength > 0 ? "A".repeat(stringLength) : "");
51+
52+
return result;
53+
}
54+
55+
private static NestedObject createNestedObject(int maxLevel, int level) {
56+
NestedObject nested = level < maxLevel ? createNestedObject(maxLevel, level + 1) : null;
57+
return new NestedObject(level, nested);
58+
}
59+
}
60+

0 commit comments

Comments
 (0)