Skip to content

Commit a7e4971

Browse files
committed
ref(data-collection): move data_collection into _experiments
Fold `data_collection` under `_experiments` instead of exposing it as a top-level `Client` constructor option, and gate it on presence rather than a separate `enable_data_collection` flag.
1 parent e4abd02 commit a7e4971

4 files changed

Lines changed: 88 additions & 57 deletions

File tree

sentry_sdk/consts.py

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class CompressionAlgo(Enum):
8787
Callable[[SpanJSON, Hint], Optional[SpanJSON]]
8888
],
8989
"suppress_asgi_chained_exceptions": Optional[bool],
90-
"enable_data_collection": Optional[bool],
90+
"data_collection": Optional[DataCollectionUserOptions],
9191
},
9292
total=False,
9393
)
@@ -1280,7 +1280,6 @@ def __init__(
12801280
transport_queue_size: int = DEFAULT_QUEUE_SIZE,
12811281
sample_rate: float = 1.0,
12821282
send_default_pii: "Optional[bool]" = None,
1283-
data_collection: "Optional[DataCollectionUserOptions]" = None,
12841283
http_proxy: "Optional[str]" = None,
12851284
https_proxy: "Optional[str]" = None,
12861285
ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006
@@ -1435,28 +1434,6 @@ def __init__(
14351434
If you enable this option, be sure to manually remove what you don't want to send using our features for
14361435
managing `Sensitive Data <https://docs.sentry.io/data-management/sensitive-data/>`_.
14371436
1438-
:param data_collection: (EXPERIMENTAL) Structured configuration controlling what data integrations collect automatically,
1439-
superseding `send_default_pii`. This option only takes effect when the
1440-
`enable_data_collection` experiment is turned on via
1441-
`_experiments={"enable_data_collection": True}`; otherwise the SDK
1442-
continues to derive behaviour from `send_default_pii`. Pass a dict to enable or
1443-
restrict collection per category (user identity, cookies, HTTP headers/bodies, query params, generative AI
1444-
inputs/outputs, stack frame variables, source context).
1445-
1446-
When `data_collection` is set, omitted fields use their defaults (most categories are collected, with the
1447-
sensitive denylist scrubbing values). When it is not set, the SDK derives behaviour from `send_default_pii`
1448-
so that upgrading without configuring `data_collection` changes nothing. If both are set, `data_collection`
1449-
takes precedence.
1450-
1451-
Example::
1452-
1453-
sentry_sdk.init(
1454-
dsn="...",
1455-
data_collection={"user_info": False, "http_bodies": []},
1456-
)
1457-
1458-
See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details.
1459-
14601437
:param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and
14611438
passwords from a `denylist`.
14621439
@@ -1780,7 +1757,25 @@ def __init__(
17801757
:param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to
17811758
reduce downstream data loss.
17821759
1783-
:param _experiments:
1760+
:param _experiments: Dictionary of experimental, opt-in features that are not yet stable.
1761+
1762+
``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations
1763+
collect automatically, superseding `send_default_pii`. Passing a dict under
1764+
`_experiments={"data_collection": {...}}` opts into the feature; omitted fields use their
1765+
defaults (most categories are collected, with the sensitive denylist scrubbing values).
1766+
When it is not set, the SDK derives behaviour from `send_default_pii` so that upgrading
1767+
changes nothing. Restrict collection per category (user identity, cookies, HTTP
1768+
headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source
1769+
context). If `send_default_pii` is also set, `data_collection` takes precedence.
1770+
1771+
Example::
1772+
1773+
sentry_sdk.init(
1774+
dsn="...",
1775+
_experiments={"data_collection": {"user_info": False, "http_bodies": []}},
1776+
)
1777+
1778+
See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details.
17841779
"""
17851780
pass
17861781

sentry_sdk/data_collection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def _resolve_data_collection(options: "Dict[str, Any]") -> "DataCollection":
220220
221221
``data_collection`` must be a plain ``dict``.
222222
"""
223-
user_dc = options.get("data_collection")
223+
user_dc = options.get("_experiments", {}).get("data_collection")
224224
send_default_pii = options.get("send_default_pii")
225225

226226
include_local_variables = (

sentry_sdk/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2084,7 +2084,7 @@ def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
20842084
if options is None:
20852085
return False
20862086

2087-
return bool(options["_experiments"].get("enable_data_collection", False))
2087+
return "data_collection" in options.get("_experiments", {})
20882088

20892089

20902090
def get_before_send_log(

tests/test_data_collection.py

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@
44

55
import sentry_sdk
66
from sentry_sdk.data_collection import _ALL_HTTP_BODY_TYPES
7+
from sentry_sdk.utils import has_data_collection_enabled
78

89

910
def test_kvcb_invalid_mode():
1011
with pytest.raises(ValueError):
11-
sentry_sdk.init(data_collection={"cookies": {"mode": "nope"}}) # type: ignore Purposely ignoring to test invalid option
12+
sentry_sdk.init(_experiments={"data_collection": {"cookies": {"mode": "nope"}}}) # type: ignore Purposely ignoring to test invalid option
1213

1314

1415
def test_kvcb_from_dict_defaults_mode():
15-
sentry_sdk.init(data_collection={"cookies": {"mode": "denylist", "terms": ["x"]}})
16+
sentry_sdk.init(
17+
_experiments={
18+
"data_collection": {"cookies": {"mode": "denylist", "terms": ["x"]}}
19+
}
20+
)
1621
client = sentry_sdk.get_client()
1722
assert client.options["data_collection"]["cookies"] == {
1823
"mode": "denylist",
@@ -23,13 +28,13 @@ def test_kvcb_from_dict_defaults_mode():
2328
def test_http_headers_collection_defaults():
2429
default_terms = ["forwarded", "-ip", "remote-", "via", "-user"]
2530

26-
sentry_sdk.init(data_collection={"http_headers": {}}) # type: ignore Purposely ignoring to test invalid option
31+
sentry_sdk.init(_experiments={"data_collection": {"http_headers": {}}}) # type: ignore Purposely ignoring to test invalid option
2732
client = sentry_sdk.get_client()
2833
assert client.options["data_collection"]["http_headers"]["request"] == {
2934
"mode": "denylist"
3035
}
3136

32-
sentry_sdk.init(data_collection={"http_headers": "off"}) # type: ignore Purposely ignoring to test invalid option
37+
sentry_sdk.init(_experiments={"data_collection": {"http_headers": "off"}}) # type: ignore Purposely ignoring to test invalid option
3338
client = sentry_sdk.get_client()
3439
assert client.options["data_collection"]["http_headers"]["request"] == {
3540
"mode": "denylist"
@@ -45,9 +50,11 @@ def test_http_headers_collection_defaults():
4550

4651
def test_http_headers_use_default_in_setting_with_missing_config():
4752
sentry_sdk.init(
48-
data_collection={
49-
"http_headers": {
50-
"request": {"mode": "allowlist", "terms": ["x-id"]},
53+
_experiments={
54+
"data_collection": {
55+
"http_headers": {
56+
"request": {"mode": "allowlist", "terms": ["x-id"]},
57+
}
5158
}
5259
}
5360
)
@@ -108,7 +115,7 @@ def _get(dc, path):
108115
id="send_default_pii_false_collects_no_pii",
109116
),
110117
pytest.param(
111-
{"data_collection": {}},
118+
{"_experiments": {"data_collection": {}}},
112119
{
113120
"user_info": True,
114121
"gen_ai.inputs": True,
@@ -121,7 +128,11 @@ def _get(dc, path):
121128
id="explicit_data_collection_uses_spec_defaults",
122129
),
123130
pytest.param(
124-
{"data_collection": {"user_info": False, "http_bodies": []}},
131+
{
132+
"_experiments": {
133+
"data_collection": {"user_info": False, "http_bodies": []}
134+
}
135+
},
125136
{
126137
"user_info": False,
127138
"http_bodies": [],
@@ -132,7 +143,7 @@ def _get(dc, path):
132143
),
133144
pytest.param(
134145
{
135-
"data_collection": {},
146+
"_experiments": {"data_collection": {}},
136147
"include_local_variables": False,
137148
"include_source_context": False,
138149
},
@@ -141,11 +152,13 @@ def _get(dc, path):
141152
),
142153
pytest.param(
143154
{
144-
"data_collection": {
145-
"cookies": {"mode": "off"},
146-
"query_params": {"mode": "allowlist", "terms": ["page"]},
147-
"http_headers": {"request": {"mode": "off"}},
148-
"gen_ai": {"inputs": False, "outputs": True},
155+
"_experiments": {
156+
"data_collection": {
157+
"cookies": {"mode": "off"},
158+
"query_params": {"mode": "allowlist", "terms": ["page"]},
159+
"http_headers": {"request": {"mode": "off"}},
160+
"gen_ai": {"inputs": False, "outputs": True},
161+
}
149162
}
150163
},
151164
{
@@ -160,12 +173,14 @@ def _get(dc, path):
160173
),
161174
pytest.param(
162175
{
163-
"data_collection": {
164-
"cookies": None,
165-
"http_headers": None,
166-
"query_params": None,
167-
"graphql": None,
168-
"gen_ai": None,
176+
"_experiments": {
177+
"data_collection": {
178+
"cookies": None,
179+
"http_headers": None,
180+
"query_params": None,
181+
"graphql": None,
182+
"gen_ai": None,
183+
}
169184
}
170185
},
171186
{
@@ -180,7 +195,7 @@ def _get(dc, path):
180195
id="none_values_fall_back_to_spec_defaults",
181196
),
182197
pytest.param(
183-
{"data_collection": {}},
198+
{"_experiments": {"data_collection": {}}},
184199
{
185200
"graphql.document": True,
186201
"graphql.variables": True,
@@ -209,27 +224,27 @@ def _get(dc, path):
209224
id="legacy_pii_on_collects_graphql_database_and_queues",
210225
),
211226
pytest.param(
212-
{"data_collection": {"graphql": {"variables": False}}},
227+
{"_experiments": {"data_collection": {"graphql": {"variables": False}}}},
213228
{"graphql.document": True, "graphql.variables": False},
214229
id="explicit_partial_graphql_fills_omitted",
215230
),
216231
pytest.param(
217-
{"data_collection": {"frame_context_lines": True}},
232+
{"_experiments": {"data_collection": {"frame_context_lines": True}}},
218233
{"frame_context_lines": 5},
219234
id="frame_context_lines_bool_fallback_true",
220235
),
221236
pytest.param(
222-
{"data_collection": {"frame_context_lines": False}},
237+
{"_experiments": {"data_collection": {"frame_context_lines": False}}},
223238
{"frame_context_lines": 0},
224239
id="frame_context_lines_bool_fallback_false",
225240
),
226241
pytest.param(
227-
{"data_collection": {"frame_context_lines": 3}},
242+
{"_experiments": {"data_collection": {"frame_context_lines": 3}}},
228243
{"frame_context_lines": 3},
229244
id="frame_context_lines_bool_fallback_3",
230245
),
231246
pytest.param(
232-
{"data_collection": {"frame_context_lines": 0}},
247+
{"_experiments": {"data_collection": {"frame_context_lines": 0}}},
233248
{"frame_context_lines": 0},
234249
id="frame_context_lines_bool_fallback_0",
235250
),
@@ -245,7 +260,8 @@ def test_initialize_client_data_collection_overrides_send_default_pii_and_warns(
245260
with warnings.catch_warnings(record=True) as caught:
246261
warnings.simplefilter("always")
247262
dc = _initialize_client_with_config(
248-
send_default_pii=True, data_collection={"user_info": False}
263+
send_default_pii=True,
264+
_experiments={"data_collection": {"user_info": False}},
249265
)
250266
assert dc["user_info"] is False # data_collection wins
251267
assert any(issubclass(w.category, DeprecationWarning) for w in caught)
@@ -269,7 +285,7 @@ def test_initialize_client_data_collection_overrides_send_default_pii_and_warns(
269285
id="send_default_pii_enables_user_info",
270286
),
271287
pytest.param(
272-
{"data_collection": {"user_info": False}},
288+
{"_experiments": {"data_collection": {"user_info": False}}},
273289
{"user_info": False, "provided_by_user": True},
274290
id="explicit_data_collection_overrides_user_info",
275291
),
@@ -279,7 +295,10 @@ def test_initialize_client_data_collection_overrides_send_default_pii_and_warns(
279295
id="dsnless_spotlight_rederives_data_collection",
280296
),
281297
pytest.param(
282-
{"spotlight": True, "data_collection": {"user_info": False}},
298+
{
299+
"spotlight": True,
300+
"_experiments": {"data_collection": {"user_info": False}},
301+
},
283302
{"provided_by_user": True, "user_info": False},
284303
id="dsnless_spotlight_respects_explicit_data_collection",
285304
),
@@ -293,3 +312,20 @@ def test_client_data_collection_settings(init_kwargs, expected):
293312
assert client.should_send_default_pii() is value
294313
else:
295314
assert client.options["data_collection"][key] is value
315+
316+
317+
def test_has_data_collection_enabled_gates_on_presence():
318+
assert has_data_collection_enabled(None) is False
319+
assert has_data_collection_enabled({"_experiments": {}}) is False
320+
assert (
321+
has_data_collection_enabled({"_experiments": {"data_collection": {}}}) is True
322+
)
323+
324+
325+
def test_no_experiments_falls_back_to_send_default_pii():
326+
sentry_sdk.init(send_default_pii=True)
327+
client = sentry_sdk.get_client()
328+
dc = client.options["data_collection"]
329+
assert dc["provided_by_user"] is False
330+
assert dc["user_info"] is True
331+
assert has_data_collection_enabled(client.options) is False

0 commit comments

Comments
 (0)