Skip to content

Commit a23d6ba

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 8c4ad96 commit a23d6ba

File tree

8 files changed

+299
-3
lines changed

8 files changed

+299
-3
lines changed

tests/debugger/test_debugger_probe_snapshot.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ def _validate_spans(self):
8181
if not self.probe_spans[expected_trace]:
8282
raise ValueError(f"No spans found for trace {expected_trace}")
8383

84+
def _assert_keys_present(self, data: dict, required_keys: list):
85+
missing = [key for key in required_keys if key not in data]
86+
if missing:
87+
raise KeyError(f"Missing required keys: {', '.join(missing)}")
88+
8489

8590
@features.debugger_method_probe
8691
@scenarios.debugger_probes_snapshot
@@ -209,6 +214,12 @@ def _validate_snapshots(self):
209214
class Test_Debugger_Line_Probe_Snaphots(BaseDebuggerProbeSnaphotTest):
210215
"""Tests for line-level probe snapshots"""
211216

217+
# Default snapshot capture limits
218+
DEFAULT_MAX_REFERENCE_DEPTH = 3
219+
DEFAULT_MAX_COLLECTION_SIZE = 100
220+
DEFAULT_MAX_FIELD_COUNT = 20
221+
DEFAULT_MAX_LENGTH = 255
222+
212223
### log probe ###
213224
def setup_log_line_snapshot(self):
214225
self._setup("probe_snapshot_log_line", "/debugger/log", "log", lines=None)
@@ -218,6 +229,124 @@ def test_log_line_snapshot(self):
218229
self._assert()
219230
self._validate_snapshots()
220231

232+
def setup_default_capture_values(self):
233+
"""Setup test with endpoint that generates data exceeding default capture limits"""
234+
test_depth = self.DEFAULT_MAX_REFERENCE_DEPTH + 7
235+
test_fields = self.DEFAULT_MAX_FIELD_COUNT + 30
236+
test_collection_size = self.DEFAULT_MAX_COLLECTION_SIZE + 100
237+
test_string_length = self.DEFAULT_MAX_LENGTH + 245
238+
239+
self._setup(
240+
"probe_snapshot_default_capture_limits",
241+
f"/debugger/snapshot/limits?"
242+
f"depth={test_depth}&"
243+
f"fields={test_fields}&"
244+
f"collectionSize={test_collection_size}&"
245+
f"stringLength={test_string_length}",
246+
"log",
247+
lines=None,
248+
)
249+
250+
@missing_feature(context.library != "nodejs", reason="Endpoint only implemented for Node.js", force_skip=True)
251+
def test_default_capture_values(self):
252+
"""Test that the tracer uses default capture values when capture property is omitted"""
253+
self._assert()
254+
self._validate_snapshots()
255+
256+
for probe_id in self.probe_ids:
257+
if probe_id not in self.probe_snapshots:
258+
raise ValueError(f"Snapshot {probe_id} was not received.")
259+
260+
snapshots = self.probe_snapshots[probe_id]
261+
if not snapshots:
262+
raise ValueError(f"No snapshots found for probe {probe_id}")
263+
264+
snapshot = snapshots[0]
265+
debugger_snapshot = snapshot.get("debugger", {}).get("snapshot") or snapshot.get("debugger.snapshot")
266+
267+
if not debugger_snapshot:
268+
raise ValueError(f"Snapshot data not found in expected format for probe {probe_id}")
269+
if "captures" not in debugger_snapshot:
270+
raise ValueError(f"No captures found in snapshot for probe {probe_id}")
271+
272+
captures = debugger_snapshot["captures"]
273+
if "lines" in captures:
274+
lines = captures["lines"]
275+
if isinstance(lines, dict) and len(lines) == 1:
276+
line_key = next(iter(lines))
277+
line_data = lines[line_key]
278+
else:
279+
raise ValueError(f"Expected 'lines' to be a dict with a single key, got: {len(lines)}")
280+
281+
if line_data and "locals" in line_data:
282+
locals_data = line_data["locals"]
283+
self._assert_keys_present(
284+
locals_data, ["manyFields", "largeCollection", "deepObject", "longString"]
285+
)
286+
287+
# Validate maxFieldCount
288+
many_fields = locals_data["manyFields"]
289+
assert many_fields.get("notCapturedReason") == "fieldCount", (
290+
f"manyFields should have notCapturedReason='fieldCount' to indicate "
291+
f"maxFieldCount limit was applied. Got: {many_fields.get('notCapturedReason')}"
292+
)
293+
294+
actual_size = many_fields.get("size")
295+
expected_field_count = self.DEFAULT_MAX_FIELD_COUNT + 30
296+
assert (
297+
actual_size == expected_field_count
298+
), f"manyFields should report size={expected_field_count}, got: {actual_size}"
299+
300+
captured_count = len(many_fields["fields"])
301+
assert captured_count == self.DEFAULT_MAX_FIELD_COUNT, (
302+
f"manyFields should have exactly {self.DEFAULT_MAX_FIELD_COUNT} fields captured "
303+
f"(maxFieldCount default), got: {captured_count}"
304+
)
305+
306+
# Validate maxCollectionSize
307+
large_collection = locals_data["largeCollection"]
308+
assert large_collection.get("notCapturedReason") == "collectionSize", (
309+
f"largeCollection should have notCapturedReason='collectionSize' to indicate "
310+
f"maxCollectionSize limit was applied. Got: {large_collection.get('notCapturedReason')}"
311+
)
312+
313+
actual_size = large_collection.get("size")
314+
expected_collection_size = self.DEFAULT_MAX_COLLECTION_SIZE + 100
315+
assert (
316+
actual_size == expected_collection_size
317+
), f"largeCollection should report size={expected_collection_size}, got: {actual_size}"
318+
319+
captured_count = len(large_collection["elements"])
320+
assert captured_count == self.DEFAULT_MAX_COLLECTION_SIZE, (
321+
f"largeCollection should have exactly {self.DEFAULT_MAX_COLLECTION_SIZE} elements "
322+
f"captured (maxCollectionSize default), got: {captured_count}"
323+
)
324+
325+
# Validate maxReferenceDepth
326+
self._assert_max_depth(locals_data["deepObject"], max_depth=self.DEFAULT_MAX_REFERENCE_DEPTH)
327+
328+
# Validate maxLength
329+
string_value = locals_data["longString"]["value"]
330+
assert len(string_value) <= self.DEFAULT_MAX_LENGTH, (
331+
f"longString has length {len(string_value)}, exceeds maxLength default "
332+
f"({self.DEFAULT_MAX_LENGTH}). String should be truncated."
333+
)
334+
335+
def _assert_max_depth(self, obj: dict, max_depth: int, current_depth: int = 1) -> None:
336+
"""Asserts that nested objects are truncated at maxReferenceDepth with notCapturedReason='depth'"""
337+
assert "fields" in obj, f"Expected 'fields' to be present in the object, got: {list(obj.keys())}"
338+
fields = obj["fields"]
339+
assert isinstance(fields, dict), f"Expected 'fields' to be a dict, got: {type(fields)}"
340+
assert "nested" in fields, f"Expected 'nested' to be present in the 'fields' object, got: {list(fields.keys())}"
341+
nested = fields["nested"]
342+
assert isinstance(nested, dict), f"Expected 'nested' to be a dict, got: {type(nested)}"
343+
if current_depth == max_depth:
344+
assert (
345+
nested.get("notCapturedReason") == "depth"
346+
), f"Expected 'notCapturedReason' to be 'depth', got: {nested.get('notCapturedReason')}"
347+
else:
348+
self._assert_max_depth(nested, max_depth, current_depth + 1)
349+
221350
def setup_log_line_snapshot_debug_track(self):
222351
self.use_debugger_endpoint = True
223352
self._setup("probe_snapshot_log_line", "/debugger/log", "log", lines=None)
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+
"136"
13+
]
14+
}
15+
}
16+
]
17+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict'
2+
3+
module.exports = ({ depth, fields, collectionSize, stringLength }) => {
4+
// Generate deeply nested object (tests maxReferenceDepth)
5+
const deepObject = depth > 0 ? createNestedObject(depth) : null
6+
7+
// Generate object with many fields (tests maxFieldCount)
8+
const manyFields = {}
9+
for (let i = 0; i < fields; i++) {
10+
manyFields[`field${i}`] = i
11+
}
12+
13+
// Generate large collection (tests maxCollectionSize)
14+
const largeCollection = []
15+
for (let i = 0; i < collectionSize; i++) {
16+
largeCollection.push(i)
17+
}
18+
19+
// Generate long string (tests maxLength)
20+
const longString = stringLength > 0 ? 'A'.repeat(stringLength) : ''
21+
22+
return {
23+
deepObject,
24+
manyFields,
25+
largeCollection,
26+
longString
27+
}
28+
}
29+
30+
function createNestedObject (maxLevel, level = 1) {
31+
if (level === maxLevel) return { level }
32+
return {
33+
level,
34+
nested: createNestedObject(maxLevel, level + 1)
35+
}
36+
}

utils/build/docker/nodejs/express/debugger/index.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint-disable no-unused-vars, camelcase */
33

44
const Pii = require('./pii')
5+
const dataGenerator = require('./data_generator')
56

67
module.exports = {
78
initRoutes (app) {
@@ -14,7 +15,6 @@ module.exports = {
1415
// Padding
1516
// Padding
1617
// Padding
17-
// Padding
1818

1919
app.get('/debugger/log', (req, res) => {
2020
res.send('Log probe') // This needs to be line 20
@@ -125,5 +125,15 @@ module.exports = {
125125
const pii = boolValue ? new Pii() : null
126126
res.send('Expression probe') // This needs to be line 126
127127
})
128+
129+
app.get('/debugger/snapshot/limits', (req, res) => {
130+
const { deepObject, manyFields, largeCollection, longString } = dataGenerator({
131+
depth: parseInt(req.query.depth, 10) || 0,
132+
fields: parseInt(req.query.fields, 10) || 0,
133+
collectionSize: parseInt(req.query.collectionSize, 10) || 0,
134+
stringLength: parseInt(req.query.stringLength, 10) || 0,
135+
})
136+
res.send('Capture limits probe') // This needs to be line 136
137+
})
128138
}
129139
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
interface DataGeneratorOptions {
2+
depth: number
3+
fields: number
4+
collectionSize: number
5+
stringLength: number
6+
}
7+
8+
interface GeneratedData {
9+
deepObject: any
10+
manyFields: Record<string, number>
11+
largeCollection: number[]
12+
longString: string
13+
}
14+
15+
export function dataGenerator ({ depth, fields, collectionSize, stringLength }: DataGeneratorOptions): GeneratedData {
16+
// Generate deeply nested object (tests maxReferenceDepth)
17+
const deepObject = depth > 0 ? createNestedObject(depth) : null
18+
19+
// Generate object with many fields (tests maxFieldCount)
20+
const manyFields: Record<string, number> = {}
21+
for (let i = 0; i < fields; i++) {
22+
manyFields[`field${i}`] = i
23+
}
24+
25+
// Generate large collection (tests maxCollectionSize)
26+
const largeCollection: number[] = []
27+
for (let i = 0; i < collectionSize; i++) {
28+
largeCollection.push(i)
29+
}
30+
31+
// Generate long string (tests maxLength)
32+
const longString = stringLength > 0 ? 'A'.repeat(stringLength) : ''
33+
34+
return {
35+
deepObject,
36+
manyFields,
37+
largeCollection,
38+
longString
39+
}
40+
}
41+
42+
function createNestedObject (maxLevel: number, level = 1): any {
43+
if (level === maxLevel) return { level }
44+
return {
45+
level,
46+
nested: createNestedObject(maxLevel, level + 1)
47+
}
48+
}

utils/build/docker/nodejs/express4-typescript/debugger/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type { Express, Request, Response } from 'express'
44
import { Pii } from './pii'
5+
import { dataGenerator } from './data_generator'
56

67
export function initRoutes (app: Express) {
78
// Padding
@@ -14,7 +15,6 @@ export function initRoutes (app: Express) {
1415
// Padding
1516
// Padding
1617
// Padding
17-
// Padding
1818

1919
app.get('/debugger/log', (req: Request, res: Response) => {
2020
res.send('Log probe') // This needs to be line 20
@@ -125,4 +125,14 @@ export function initRoutes (app: Express) {
125125
const pii = boolValue ? new Pii() : null
126126
res.send('Expression probe') // This needs to be line 126
127127
})
128+
129+
app.get('/debugger/snapshot/limits', (req: Request, res: Response) => {
130+
const { deepObject, manyFields, largeCollection, longString } = dataGenerator({
131+
depth: parseInt(req.query.depth as string, 10) || 0,
132+
fields: parseInt(req.query.fields as string, 10) || 0,
133+
collectionSize: parseInt(req.query.collectionSize as string, 10) || 0,
134+
stringLength: parseInt(req.query.stringLength as string, 10) || 0
135+
})
136+
res.send('Capture limits probe') // This needs to be line 136
137+
})
128138
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict'
2+
3+
module.exports = ({ depth, fields, collectionSize, stringLength }) => {
4+
// Generate deeply nested object (tests maxReferenceDepth)
5+
const deepObject = depth > 0 ? createNestedObject(depth) : null
6+
7+
// Generate object with many fields (tests maxFieldCount)
8+
const manyFields = {}
9+
for (let i = 0; i < fields; i++) {
10+
manyFields[`field${i}`] = i
11+
}
12+
13+
// Generate large collection (tests maxCollectionSize)
14+
const largeCollection = []
15+
for (let i = 0; i < collectionSize; i++) {
16+
largeCollection.push(i)
17+
}
18+
19+
// Generate long string (tests maxLength)
20+
const longString = stringLength > 0 ? 'A'.repeat(stringLength) : ''
21+
22+
return {
23+
deepObject,
24+
manyFields,
25+
largeCollection,
26+
longString
27+
}
28+
}
29+
30+
function createNestedObject (maxLevel, level = 1) {
31+
if (level === maxLevel) return { level }
32+
return {
33+
level,
34+
nested: createNestedObject(maxLevel, level + 1)
35+
}
36+
}

utils/build/docker/nodejs/fastify/debugger/index.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint-disable no-unused-vars, camelcase */
33

44
const Pii = require('./pii')
5+
const dataGenerator = require('./data_generator')
56

67
module.exports = {
78
initRoutes (fastify) {
@@ -14,7 +15,6 @@ module.exports = {
1415
// Padding
1516
// Padding
1617
// Padding
17-
// Padding
1818

1919
fastify.get('/debugger/log', async (request, reply) => {
2020
return 'Log probe' // This needs to be line 20
@@ -125,5 +125,15 @@ module.exports = {
125125
const pii = boolValue ? new Pii() : null
126126
return 'Expression probe' // This needs to be line 126
127127
})
128+
129+
fastify.get('/debugger/snapshot/limits', async (request, reply) => {
130+
const { deepObject, manyFields, largeCollection, longString } = dataGenerator({
131+
depth: parseInt(request.query.depth, 10) || 0,
132+
fields: parseInt(request.query.fields, 10) || 0,
133+
collectionSize: parseInt(request.query.collectionSize, 10) || 0,
134+
stringLength: parseInt(request.query.stringLength, 10) || 0,
135+
})
136+
return 'Capture limits probe' // This needs to be line 136
137+
})
128138
}
129139
}

0 commit comments

Comments
 (0)