Skip to content

Commit 05b284e

Browse files
authored
feat(data_collection): Support KeyValueCollectionBehaviour for stack_frame_variables (#7265)
Allow `stack_frame_variables` to accept an allowlist/denylist dict (like other data_collection options) in addition to a plain bool. Explicit `data_collection` config now ignores the legacy `include_local_variables` option instead of falling back to it, and `frame_context_lines` validates its value and raises on invalid input. Refs PY-2578 Refs #6738
1 parent f807c5a commit 05b284e

3 files changed

Lines changed: 122 additions & 12 deletions

File tree

‎sentry_sdk/_types.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ class DataCollectionUserOptions(TypedDict, total=False):
188188
gen_ai: "GenAICollectionUserOptions"
189189
database_query_data: bool
190190
queues: bool
191-
stack_frame_variables: bool
191+
stack_frame_variables: "Union[bool, KeyValueCollectionBehaviour]"
192192
frame_context_lines: int
193193

194194
class DataCollection(TypedDict):
@@ -202,7 +202,7 @@ class DataCollection(TypedDict):
202202
gen_ai: "GenAICollectionBehaviour"
203203
database_query_data: bool
204204
queues: bool
205-
stack_frame_variables: bool
205+
stack_frame_variables: "Union[bool, KeyValueCollectionBehaviour]"
206206
frame_context_lines: int
207207

208208
# "critical" is an alias of "fatal" recognized by Relay

‎sentry_sdk/data_collection.py‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@
2323
"""
2424

2525
import warnings
26-
from typing import TYPE_CHECKING, List, Mapping, Optional, Union, cast
26+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Union, cast
2727
from urllib.parse import parse_qs, urlencode
2828

2929
from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE
3030

3131
if TYPE_CHECKING:
32-
from typing import Any, Dict
33-
3432
from sentry_sdk._types import (
3533
DataCollection,
3634
GenAICollectionBehaviour,
@@ -190,7 +188,6 @@ def _map_from_send_default_pii(
190188

191189
def _resolve_explicit(
192190
d: "dict[str, Any]",
193-
include_local_variables: bool,
194191
) -> "DataCollection":
195192
"""
196193
Build a fully-resolved ``DataCollection`` from a user-supplied
@@ -205,10 +202,19 @@ def _resolve_explicit(
205202
frame_context_lines = _DEFAULT_FRAME_CONTEXT_LINES
206203
elif isinstance(frame_context_lines, bool):
207204
frame_context_lines = _DEFAULT_FRAME_CONTEXT_LINES if frame_context_lines else 0
205+
else:
206+
if not isinstance(frame_context_lines, int) or frame_context_lines < 0:
207+
raise ValueError(
208+
"Invalid `frame_context_lines` value: Must be 0 or greater."
209+
)
210+
211+
raw_stack_frame_variables = d.get("stack_frame_variables", True)
212+
stack_frame_variables: "Union[bool, KeyValueCollectionBehaviour]"
208213

209-
stack_frame_variables = d.get("stack_frame_variables")
210-
if stack_frame_variables is None:
211-
stack_frame_variables = include_local_variables
214+
if isinstance(raw_stack_frame_variables, dict):
215+
stack_frame_variables = _kvcb_from_value(raw_stack_frame_variables)
216+
else:
217+
stack_frame_variables = bool(raw_stack_frame_variables)
212218

213219
# http_bodies: omitted means "all valid types"; [] is the explicit opt-out.
214220
http_bodies = d.get("http_bodies")
@@ -319,7 +325,6 @@ def _resolve_data_collection(options: "Dict[str, Any]") -> "DataCollection":
319325
)
320326
return _resolve_explicit(
321327
user_dc,
322-
include_local_variables,
323328
)
324329

325330
return _map_from_send_default_pii(

‎tests/test_data_collection.py‎

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,32 @@ def test_kvcb_invalid_mode():
1212
sentry_sdk.init(_experiments={"data_collection": {"cookies": {"mode": "nope"}}}) # type: ignore Purposely ignoring to test invalid option
1313

1414

15+
def test_stack_frame_variables_invalid_mode():
16+
with pytest.raises(ValueError):
17+
sentry_sdk.init(
18+
_experiments={
19+
"data_collection": {"stack_frame_variables": {"mode": "nope"}}
20+
}
21+
)
22+
23+
24+
@pytest.mark.parametrize(
25+
"value",
26+
["3", -1, [1], 2.5],
27+
ids=[
28+
"frame_context_lines_string",
29+
"frame_context_lines_negative",
30+
"frame_context_lines_list",
31+
"frame_context_lines_float",
32+
],
33+
)
34+
def test_frame_context_lines_invalid_value(value):
35+
with pytest.raises(ValueError):
36+
sentry_sdk.init(
37+
_experiments={"data_collection": {"frame_context_lines": value}}
38+
)
39+
40+
1541
def test_kvcb_from_dict_defaults_mode():
1642
sentry_sdk.init(
1743
_experiments={
@@ -147,8 +173,8 @@ def _get(dc, path):
147173
"include_local_variables": False,
148174
"include_source_context": False,
149175
},
150-
{"stack_frame_variables": False, "frame_context_lines": 5},
151-
id="explicit_stack_frame_variables_falls_back_to_legacy_option",
176+
{"stack_frame_variables": True, "frame_context_lines": 5},
177+
id="explicit_data_collection_ignores_legacy_include_local_variables",
152178
),
153179
pytest.param(
154180
{
@@ -248,6 +274,85 @@ def _get(dc, path):
248274
{"frame_context_lines": 0},
249275
id="frame_context_lines_bool_fallback_0",
250276
),
277+
pytest.param(
278+
{"_experiments": {"data_collection": {"stack_frame_variables": True}}},
279+
{"stack_frame_variables": True},
280+
id="stack_frame_variables_explicit_true",
281+
),
282+
pytest.param(
283+
{"_experiments": {"data_collection": {"stack_frame_variables": False}}},
284+
{"stack_frame_variables": False},
285+
id="stack_frame_variables_explicit_false",
286+
),
287+
pytest.param(
288+
{
289+
"_experiments": {
290+
"data_collection": {
291+
"stack_frame_variables": {
292+
"mode": "allowlist",
293+
"terms": ["order_id"],
294+
}
295+
}
296+
}
297+
},
298+
{
299+
"stack_frame_variables": {
300+
"mode": "allowlist",
301+
"terms": ["order_id"],
302+
}
303+
},
304+
id="stack_frame_variables_allowlist_dict",
305+
),
306+
pytest.param(
307+
{
308+
"_experiments": {
309+
"data_collection": {
310+
"stack_frame_variables": {"terms": ["order_id"]}
311+
}
312+
}
313+
},
314+
{
315+
"stack_frame_variables": {
316+
"mode": "denylist",
317+
"terms": ["order_id"],
318+
}
319+
},
320+
id="stack_frame_variables_dict_defaults_mode_to_denylist",
321+
),
322+
pytest.param(
323+
{
324+
"_experiments": {
325+
"data_collection": {"stack_frame_variables": {"mode": "off"}}
326+
}
327+
},
328+
{"stack_frame_variables": {"mode": "off"}},
329+
id="stack_frame_variables_off_dict_omits_terms",
330+
),
331+
pytest.param(
332+
{"_experiments": {"data_collection": {"stack_frame_variables": "yes"}}},
333+
{"stack_frame_variables": True},
334+
id="stack_frame_variables_non_bool_truthy_coerces_to_true",
335+
),
336+
pytest.param(
337+
{"_experiments": {"data_collection": {"stack_frame_variables": ""}}},
338+
{"stack_frame_variables": False},
339+
id="stack_frame_variables_non_bool_falsy_coerces_to_false",
340+
),
341+
pytest.param(
342+
{"_experiments": {"data_collection": {"frame_context_lines": None}}},
343+
{"frame_context_lines": 5},
344+
id="frame_context_lines_none_falls_back_to_spec_default",
345+
),
346+
pytest.param(
347+
{"include_local_variables": False, "include_source_context": False},
348+
{"stack_frame_variables": False, "frame_context_lines": 0},
349+
id="legacy_include_local_variables_off_disables_stack_frame_variables",
350+
),
351+
pytest.param(
352+
{"include_local_variables": True, "include_source_context": True},
353+
{"stack_frame_variables": True, "frame_context_lines": 5},
354+
id="legacy_include_local_variables_on_enables_stack_frame_variables",
355+
),
251356
],
252357
)
253358
def test_initialize_client_data_collection(options, expected):

0 commit comments

Comments
 (0)