Skip to content

Commit 32a92b1

Browse files
authored
feat(data-collection): Move data collection to _experiments (#6790)
Move `data_collection` from the top-level config into the `_experiments` property in order to prevent accidental use by agents/users while the feature is still being built. Fixes PY-2603 Fixes #6789
1 parent 2c389f8 commit 32a92b1

4 files changed

Lines changed: 97 additions & 52 deletions

File tree

sentry_sdk/consts.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class CompressionAlgo(Enum):
8787
Callable[[SpanJSON, Hint], Optional[SpanJSON]]
8888
],
8989
"suppress_asgi_chained_exceptions": Optional[bool],
90+
"data_collection": Optional[DataCollectionUserOptions],
9091
},
9192
total=False,
9293
)
@@ -1279,7 +1280,6 @@ def __init__(
12791280
transport_queue_size: int = DEFAULT_QUEUE_SIZE,
12801281
sample_rate: float = 1.0,
12811282
send_default_pii: "Optional[bool]" = None,
1282-
data_collection: "Optional[DataCollectionUserOptions]" = None,
12831283
http_proxy: "Optional[str]" = None,
12841284
https_proxy: "Optional[str]" = None,
12851285
ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006
@@ -1434,25 +1434,6 @@ def __init__(
14341434
If you enable this option, be sure to manually remove what you don't want to send using our features for
14351435
managing `Sensitive Data <https://docs.sentry.io/data-management/sensitive-data/>`_.
14361436
1437-
:param data_collection: (EXPERIMENTAL) Structured configuration controlling what data integrations collect automatically,
1438-
superseding `send_default_pii`. Pass a dict to enable or
1439-
restrict collection per category (user identity, cookies, HTTP headers/bodies, query params, generative AI
1440-
inputs/outputs, stack frame variables, source context).
1441-
1442-
When `data_collection` is set, omitted fields use their defaults (most categories are collected, with the
1443-
sensitive denylist scrubbing values). When it is not set, the SDK derives behaviour from `send_default_pii`
1444-
so that upgrading without configuring `data_collection` changes nothing. If both are set, `data_collection`
1445-
takes precedence.
1446-
1447-
Example::
1448-
1449-
sentry_sdk.init(
1450-
dsn="...",
1451-
data_collection={"user_info": False, "http_bodies": []},
1452-
)
1453-
1454-
See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details.
1455-
14561437
:param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and
14571438
passwords from a `denylist`.
14581439
@@ -1776,7 +1757,25 @@ def __init__(
17761757
:param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to
17771758
reduce downstream data loss.
17781759
1779-
: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.
17801779
"""
17811780
pass
17821781

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2080,6 +2080,13 @@ def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
20802080
)
20812081

20822082

2083+
def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
2084+
if options is None:
2085+
return False
2086+
2087+
return "data_collection" in options.get("_experiments", {})
2088+
2089+
20832090
def get_before_send_log(
20842091
options: "Optional[dict[str, Any]]",
20852092
) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":

tests/test_data_collection.py

Lines changed: 69 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,23 @@ 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+
assert (
324+
has_data_collection_enabled({"_experiments": {"data_collection": None}}) is True
325+
)
326+
327+
328+
def test_no_experiments_data_collection_values_fall_back_to_send_default_pii_configuration():
329+
sentry_sdk.init(send_default_pii=True)
330+
client = sentry_sdk.get_client()
331+
dc = client.options["data_collection"]
332+
assert dc["provided_by_user"] is False
333+
assert dc["user_info"] is True
334+
assert has_data_collection_enabled(client.options) is False

0 commit comments

Comments
 (0)