Skip to content

Commit d99edef

Browse files
ref(data-collection): Promote data_collection from _experiments o top-level (#7560)
Make `data_collection` available as a documented top-level `sentry_sdk.init()` option. Update `has_data_collection_enabled` to check `provided_by_user` on the resolved dict instead of key presence under `_experiments`, and update all integration tests to pass `data_collection` at the top level. Fixes PY-2796 Fixes #7557 --------- Co-authored-by: Ivana Kellyer <ivana.kellyer@sentry.io>
1 parent 045d2c1 commit d99edef

63 files changed

Lines changed: 731 additions & 1194 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎sentry_sdk/consts.py‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,7 @@ def __init__(
13161316
transport_queue_size: int = DEFAULT_QUEUE_SIZE,
13171317
sample_rate: float = 1.0,
13181318
send_default_pii: "Optional[bool]" = None,
1319+
data_collection: "Optional[DataCollectionUserOptions]" = None,
13191320
http_proxy: "Optional[str]" = None,
13201321
https_proxy: "Optional[str]" = None,
13211322
ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006
@@ -1472,6 +1473,23 @@ def __init__(
14721473
If you enable this option, be sure to manually remove what you don't want to send using our features for
14731474
managing `Sensitive Data <https://docs.sentry.io/data-management/sensitive-data/>`_.
14741475
1476+
:param data_collection: Structured configuration controlling what data integrations collect
1477+
automatically, superseding `send_default_pii`. Passing a dict opts into the feature; omitted
1478+
fields use their defaults (most categories are collected, with the sensitive denylist
1479+
scrubbing values). When it is not set, the SDK derives behaviour from `send_default_pii` so
1480+
that upgrading changes nothing. Restrict collection per category (user identity, cookies,
1481+
HTTP headers/bodies, query params, generative AI inputs/outputs, stack frame variables,
1482+
source context). If `send_default_pii` is also set, `data_collection` takes precedence.
1483+
1484+
Example::
1485+
1486+
sentry_sdk.init(
1487+
dsn="...",
1488+
data_collection={"user_info": False, "http_bodies": []},
1489+
)
1490+
1491+
See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details.
1492+
14751493
:param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and
14761494
passwords from a `denylist`.
14771495
@@ -1807,24 +1825,6 @@ def __init__(
18071825
`trace_lifecycle="stream"` is enabled.
18081826
18091827
:param _experiments: Dictionary of experimental, opt-in features that are not yet stable.
1810-
1811-
``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations
1812-
collect automatically, superseding `send_default_pii`. Passing a dict under
1813-
`_experiments={"data_collection": {...}}` opts into the feature; omitted fields use their
1814-
defaults (most categories are collected, with the sensitive denylist scrubbing values).
1815-
When it is not set, the SDK derives behaviour from `send_default_pii` so that upgrading
1816-
changes nothing. Restrict collection per category (user identity, cookies, HTTP
1817-
headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source
1818-
context). If `send_default_pii` is also set, `data_collection` takes precedence.
1819-
1820-
Example::
1821-
1822-
sentry_sdk.init(
1823-
dsn="...",
1824-
_experiments={"data_collection": {"user_info": False, "http_bodies": []}},
1825-
)
1826-
1827-
See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details.
18281828
"""
18291829
pass
18301830

‎sentry_sdk/data_collection.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,15 @@ def _resolve_data_collection(options: "Dict[str, Any]") -> "DataCollection":
293293
concrete values for every field.
294294
295295
``data_collection`` must be a plain ``dict``.
296+
297+
Must be called exactly once per options dict, before ``client._get_options``
298+
overwrites ``options["data_collection"]`` with the resolved result. Feeding an
299+
already-resolved dict back in would flip ``provided_by_user`` to ``True``.
296300
"""
297-
user_dc = options.get("_experiments", {}).get("data_collection")
301+
user_dc = options.get("data_collection")
302+
if user_dc is None:
303+
user_dc = options.get("_experiments", {}).get("data_collection")
304+
298305
send_default_pii = options.get("send_default_pii")
299306

300307
include_local_variables = (

‎sentry_sdk/utils.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464

6565
from sentry_sdk._types import (
6666
AttributeValue,
67+
DataCollection,
6768
Event,
6869
ExcInfo,
6970
Hint,
@@ -2109,7 +2110,13 @@ def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
21092110
if options is None:
21102111
return False
21112112

2112-
return "data_collection" in options.get("_experiments", {})
2113+
data_collection: "Optional[DataCollection]" = options.get("data_collection")
2114+
# Client options are resolved as part of client initialization, so `data_collection`
2115+
# being None could be that the user just didn't provide it.
2116+
# `provided_by_user` is what actually records whether the user actually configured it.
2117+
return data_collection is not None and data_collection.get(
2118+
"provided_by_user", False
2119+
)
21132120

21142121

21152122
def get_before_send_log(

‎tests/integrations/aiohttp/test_aiohttp.py‎

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ async def test_aiohttp_request_body_data_collection(
155155
):
156156
sentry_init(
157157
integrations=[AioHttpIntegration()],
158-
_experiments={"data_collection": data_collection},
158+
data_collection=data_collection,
159159
)
160160

161161
body = {"some": "value"}
@@ -207,7 +207,7 @@ async def test_aiohttp_oversized_request_body_data_collection(
207207
sentry_init(
208208
integrations=[AioHttpIntegration()],
209209
max_request_body_size="small",
210-
_experiments={"data_collection": data_collection},
210+
data_collection=data_collection,
211211
)
212212

213213
body = "a" * 2000
@@ -643,21 +643,17 @@ async def handler(request):
643643
({"send_default_pii": False}, False, False),
644644
(
645645
{
646-
"_experiments": {
647-
"data_collection": {
648-
"url_query_params": {"mode": "denylist", "terms": []}
649-
}
646+
"data_collection": {
647+
"url_query_params": {"mode": "denylist", "terms": []}
650648
}
651649
},
652650
True,
653651
True,
654652
),
655653
(
656654
{
657-
"_experiments": {
658-
"data_collection": {
659-
"url_query_params": {"mode": "allowlist", "terms": []}
660-
}
655+
"data_collection": {
656+
"url_query_params": {"mode": "allowlist", "terms": []}
661657
}
662658
},
663659
True,
@@ -794,21 +790,17 @@ async def handler(request):
794790
({"send_default_pii": False}, False, False),
795791
(
796792
{
797-
"_experiments": {
798-
"data_collection": {
799-
"url_query_params": {"mode": "denylist", "terms": []}
800-
}
793+
"data_collection": {
794+
"url_query_params": {"mode": "denylist", "terms": []}
801795
}
802796
},
803797
True,
804798
True,
805799
),
806800
(
807801
{
808-
"_experiments": {
809-
"data_collection": {
810-
"url_query_params": {"mode": "allowlist", "terms": []}
811-
}
802+
"data_collection": {
803+
"url_query_params": {"mode": "allowlist", "terms": []}
812804
}
813805
},
814806
True,
@@ -1678,7 +1670,7 @@ async def hello(request):
16781670
pytest.param(
16791671
{
16801672
"send_default_pii": True,
1681-
"data_collection": None,
1673+
"data_collection": {},
16821674
},
16831675
{
16841676
"authorization": "[Filtered]",
@@ -1690,7 +1682,7 @@ async def hello(request):
16901682
pytest.param(
16911683
{
16921684
"send_default_pii": False,
1693-
"data_collection": None,
1685+
"data_collection": {},
16941686
},
16951687
{
16961688
"authorization": "[Filtered]",
@@ -1794,9 +1786,7 @@ async def test_sensitive_header_passthrough_with_pii_span_streaming(
17941786
traces_sample_rate=1.0,
17951787
send_default_pii=options["send_default_pii"],
17961788
trace_lifecycle="stream",
1797-
_experiments={
1798-
"data_collection": options["data_collection"],
1799-
},
1789+
data_collection=options["data_collection"],
18001790
)
18011791

18021792
async def hello(request):
@@ -2256,52 +2246,46 @@ async def hello(request):
22562246
id="defaults",
22572247
),
22582248
pytest.param(
2259-
{"_experiments": {"data_collection": {}}},
2249+
{"data_collection": {}},
22602250
"toy=tennisball&color=red&auth=%5BFiltered%5D",
22612251
id="data_collection_denylist_default",
22622252
),
22632253
pytest.param(
22642254
{
2265-
"_experiments": {
2266-
"data_collection": {
2267-
"url_query_params": {"mode": "denylist", "terms": ["toy"]}
2268-
}
2255+
"data_collection": {
2256+
"url_query_params": {"mode": "denylist", "terms": ["toy"]}
22692257
}
22702258
},
22712259
"toy=%5BFiltered%5D&color=red&auth=%5BFiltered%5D",
22722260
id="data_collection_denylist_custom_terms",
22732261
),
22742262
pytest.param(
22752263
{
2276-
"_experiments": {
2277-
"data_collection": {
2278-
"url_query_params": {"mode": "allowlist", "terms": ["toy"]}
2279-
}
2264+
"data_collection": {
2265+
"url_query_params": {"mode": "allowlist", "terms": ["toy"]}
22802266
}
22812267
},
22822268
"toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D",
22832269
id="data_collection_allowlist",
22842270
),
22852271
pytest.param(
22862272
{
2287-
"_experiments": {
2288-
"data_collection": {
2289-
"url_query_params": {"mode": "allowlist", "terms": ["auth"]}
2290-
}
2273+
"data_collection": {
2274+
"url_query_params": {"mode": "allowlist", "terms": ["auth"]}
22912275
}
22922276
},
22932277
"toy=%5BFiltered%5D&color=%5BFiltered%5D&auth=%5BFiltered%5D",
22942278
id="data_collection_allowlist_sensitive_term",
22952279
),
22962280
pytest.param(
2297-
{"_experiments": {"data_collection": {"url_query_params": {"mode": "off"}}}},
2281+
{"data_collection": {"url_query_params": {"mode": "off"}}},
22982282
None,
22992283
id="data_collection_off",
23002284
),
23012285
pytest.param(
23022286
{
23032287
"send_default_pii": True,
2304-
"_experiments": {"data_collection": {"url_query_params": {"mode": "off"}}},
2288+
"data_collection": {"url_query_params": {"mode": "off"}},
23052289
},
23062290
None,
23072291
id="data_collection_wins_over_send_default_pii",
@@ -2422,7 +2406,7 @@ async def hello(request):
24222406
assert event["request"]["url"] == "http://{host}/".format(host=host)
24232407
assert event["request"]["method"] == "GET"
24242408

2425-
if "data_collection" not in init_kwargs.get("_experiments", {}):
2409+
if "data_collection" not in init_kwargs:
24262410
assert (
24272411
event["request"]["query_string"] == "toy=tennisball&color=red&auth=secret"
24282412
)

‎tests/integrations/aiomysql/test_aiomysql.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ async def test_execute_many_record_params_with_data_collection_enabled(
218218
) -> None:
219219
sentry_init(
220220
integrations=[AioMySQLIntegration()],
221-
_experiments={"data_collection": {"database_query_data": True}},
221+
data_collection={"database_query_data": True},
222222
)
223223
events = capture_events()
224224

@@ -266,7 +266,7 @@ async def test_execute_many_record_params_with_data_collection_disabled(
266266
) -> None:
267267
sentry_init(
268268
integrations=[AioMySQLIntegration(record_params=True)],
269-
_experiments={"data_collection": {"database_query_data": False}},
269+
data_collection={"database_query_data": False},
270270
)
271271
events = capture_events()
272272

@@ -307,7 +307,7 @@ async def test_execute_many_record_params_with_data_collection_default(
307307
) -> None:
308308
sentry_init(
309309
integrations=[AioMySQLIntegration()],
310-
_experiments={"data_collection": {}},
310+
data_collection={},
311311
)
312312
events = capture_events()
313313

@@ -444,7 +444,7 @@ async def test_execute_record_params_with_data_collection_enabled(
444444
) -> None:
445445
sentry_init(
446446
integrations=[AioMySQLIntegration()],
447-
_experiments={"data_collection": {"database_query_data": True}},
447+
data_collection={"database_query_data": True},
448448
)
449449
events = capture_events()
450450

@@ -485,7 +485,7 @@ async def test_execute_record_params_with_data_collection_disabled(
485485
) -> None:
486486
sentry_init(
487487
integrations=[AioMySQLIntegration(record_params=True)],
488-
_experiments={"data_collection": {"database_query_data": False}},
488+
data_collection={"database_query_data": False},
489489
)
490490
events = capture_events()
491491

@@ -523,7 +523,7 @@ async def test_execute_record_params_with_data_collection_default(
523523
) -> None:
524524
sentry_init(
525525
integrations=[AioMySQLIntegration()],
526-
_experiments={"data_collection": {}},
526+
data_collection={},
527527
)
528528
events = capture_events()
529529

0 commit comments

Comments
 (0)