Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 20 additions & 21 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class CompressionAlgo(Enum):
Callable[[SpanJSON, Hint], Optional[SpanJSON]]
],
"suppress_asgi_chained_exceptions": Optional[bool],
"data_collection": Optional[DataCollectionUserOptions],
},
total=False,
)
Expand Down Expand Up @@ -1279,7 +1280,6 @@ def __init__(
transport_queue_size: int = DEFAULT_QUEUE_SIZE,
sample_rate: float = 1.0,
send_default_pii: "Optional[bool]" = None,
data_collection: "Optional[DataCollectionUserOptions]" = None,
http_proxy: "Optional[str]" = None,
https_proxy: "Optional[str]" = None,
ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006
Expand Down Expand Up @@ -1434,25 +1434,6 @@ def __init__(
If you enable this option, be sure to manually remove what you don't want to send using our features for
managing `Sensitive Data <https://docs.sentry.io/data-management/sensitive-data/>`_.

:param data_collection: (EXPERIMENTAL) Structured configuration controlling what data integrations collect automatically,
superseding `send_default_pii`. Pass a dict to enable or
restrict collection per category (user identity, cookies, HTTP headers/bodies, query params, generative AI
inputs/outputs, stack frame variables, source context).

When `data_collection` is set, omitted fields use their defaults (most categories are collected, with the
sensitive denylist scrubbing values). When it is not set, the SDK derives behaviour from `send_default_pii`
so that upgrading without configuring `data_collection` changes nothing. If both are set, `data_collection`
takes precedence.

Example::

sentry_sdk.init(
dsn="...",
data_collection={"user_info": False, "http_bodies": []},
)

See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details.

:param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and
passwords from a `denylist`.

Expand Down Expand Up @@ -1776,7 +1757,25 @@ def __init__(
:param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to
reduce downstream data loss.

:param _experiments:
:param _experiments: Dictionary of experimental, opt-in features that are not yet stable.

``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations
collect automatically, superseding `send_default_pii`. Passing a dict under
`_experiments={"data_collection": {...}}` opts into the feature; omitted fields use their
defaults (most categories are collected, with the sensitive denylist scrubbing values).
When it is not set, the SDK derives behaviour from `send_default_pii` so that upgrading
changes nothing. Restrict collection per category (user identity, cookies, HTTP
headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source
context). If `send_default_pii` is also set, `data_collection` takes precedence.

Example::

sentry_sdk.init(
dsn="...",
_experiments={"data_collection": {"user_info": False, "http_bodies": []}},
)

See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details.
"""
pass

Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/data_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def _resolve_data_collection(options: "Dict[str, Any]") -> "DataCollection":
``data_collection`` must be a plain ``dict``.
"""
user_dc = options.get("data_collection")
user_dc = options.get("_experiments", {}).get("data_collection")
send_default_pii = options.get("send_default_pii")

include_local_variables = (
Expand Down
7 changes: 7 additions & 0 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,13 @@ def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
)


def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
if options is None:
return False

return "data_collection" in options.get("_experiments", {})


def get_before_send_log(
options: "Optional[dict[str, Any]]",
) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
Expand Down
99 changes: 69 additions & 30 deletions tests/test_data_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@

import sentry_sdk
from sentry_sdk.data_collection import _ALL_HTTP_BODY_TYPES
from sentry_sdk.utils import has_data_collection_enabled


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


def test_kvcb_from_dict_defaults_mode():
sentry_sdk.init(data_collection={"cookies": {"mode": "denylist", "terms": ["x"]}})
sentry_sdk.init(
_experiments={
"data_collection": {"cookies": {"mode": "denylist", "terms": ["x"]}}
}
)
client = sentry_sdk.get_client()
assert client.options["data_collection"]["cookies"] == {
"mode": "denylist",
Expand All @@ -23,13 +28,13 @@ def test_kvcb_from_dict_defaults_mode():
def test_http_headers_collection_defaults():
default_terms = ["forwarded", "-ip", "remote-", "via", "-user"]

sentry_sdk.init(data_collection={"http_headers": {}}) # type: ignore Purposely ignoring to test invalid option
sentry_sdk.init(_experiments={"data_collection": {"http_headers": {}}}) # type: ignore Purposely ignoring to test invalid option
client = sentry_sdk.get_client()
assert client.options["data_collection"]["http_headers"]["request"] == {
"mode": "denylist"
}

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

def test_http_headers_use_default_in_setting_with_missing_config():
sentry_sdk.init(
data_collection={
"http_headers": {
"request": {"mode": "allowlist", "terms": ["x-id"]},
_experiments={
"data_collection": {
"http_headers": {
"request": {"mode": "allowlist", "terms": ["x-id"]},
}
}
}
)
Expand Down Expand Up @@ -108,7 +115,7 @@ def _get(dc, path):
id="send_default_pii_false_collects_no_pii",
),
pytest.param(
{"data_collection": {}},
{"_experiments": {"data_collection": {}}},
{
"user_info": True,
"gen_ai.inputs": True,
Expand All @@ -121,7 +128,11 @@ def _get(dc, path):
id="explicit_data_collection_uses_spec_defaults",
),
pytest.param(
{"data_collection": {"user_info": False, "http_bodies": []}},
{
"_experiments": {
"data_collection": {"user_info": False, "http_bodies": []}
}
},
{
"user_info": False,
"http_bodies": [],
Expand All @@ -132,7 +143,7 @@ def _get(dc, path):
),
pytest.param(
{
"data_collection": {},
"_experiments": {"data_collection": {}},
"include_local_variables": False,
"include_source_context": False,
},
Expand All @@ -141,11 +152,13 @@ def _get(dc, path):
),
pytest.param(
{
"data_collection": {
"cookies": {"mode": "off"},
"query_params": {"mode": "allowlist", "terms": ["page"]},
"http_headers": {"request": {"mode": "off"}},
"gen_ai": {"inputs": False, "outputs": True},
"_experiments": {
"data_collection": {
"cookies": {"mode": "off"},
"query_params": {"mode": "allowlist", "terms": ["page"]},
"http_headers": {"request": {"mode": "off"}},
"gen_ai": {"inputs": False, "outputs": True},
}
}
},
{
Expand All @@ -160,12 +173,14 @@ def _get(dc, path):
),
pytest.param(
{
"data_collection": {
"cookies": None,
"http_headers": None,
"query_params": None,
"graphql": None,
"gen_ai": None,
"_experiments": {
"data_collection": {
"cookies": None,
"http_headers": None,
"query_params": None,
"graphql": None,
"gen_ai": None,
}
}
},
{
Expand All @@ -180,7 +195,7 @@ def _get(dc, path):
id="none_values_fall_back_to_spec_defaults",
),
pytest.param(
{"data_collection": {}},
{"_experiments": {"data_collection": {}}},
{
"graphql.document": True,
"graphql.variables": True,
Expand Down Expand Up @@ -209,27 +224,27 @@ def _get(dc, path):
id="legacy_pii_on_collects_graphql_database_and_queues",
),
pytest.param(
{"data_collection": {"graphql": {"variables": False}}},
{"_experiments": {"data_collection": {"graphql": {"variables": False}}}},
{"graphql.document": True, "graphql.variables": False},
id="explicit_partial_graphql_fills_omitted",
),
pytest.param(
{"data_collection": {"frame_context_lines": True}},
{"_experiments": {"data_collection": {"frame_context_lines": True}}},
{"frame_context_lines": 5},
id="frame_context_lines_bool_fallback_true",
),
pytest.param(
{"data_collection": {"frame_context_lines": False}},
{"_experiments": {"data_collection": {"frame_context_lines": False}}},
{"frame_context_lines": 0},
id="frame_context_lines_bool_fallback_false",
),
pytest.param(
{"data_collection": {"frame_context_lines": 3}},
{"_experiments": {"data_collection": {"frame_context_lines": 3}}},
{"frame_context_lines": 3},
id="frame_context_lines_bool_fallback_3",
),
pytest.param(
{"data_collection": {"frame_context_lines": 0}},
{"_experiments": {"data_collection": {"frame_context_lines": 0}}},
{"frame_context_lines": 0},
id="frame_context_lines_bool_fallback_0",
),
Expand All @@ -245,7 +260,8 @@ def test_initialize_client_data_collection_overrides_send_default_pii_and_warns(
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
dc = _initialize_client_with_config(
send_default_pii=True, data_collection={"user_info": False}
send_default_pii=True,
_experiments={"data_collection": {"user_info": False}},
)
assert dc["user_info"] is False # data_collection wins
assert any(issubclass(w.category, DeprecationWarning) for w in caught)
Expand All @@ -269,7 +285,7 @@ def test_initialize_client_data_collection_overrides_send_default_pii_and_warns(
id="send_default_pii_enables_user_info",
),
pytest.param(
{"data_collection": {"user_info": False}},
{"_experiments": {"data_collection": {"user_info": False}}},
{"user_info": False, "provided_by_user": True},
id="explicit_data_collection_overrides_user_info",
),
Expand All @@ -279,7 +295,10 @@ def test_initialize_client_data_collection_overrides_send_default_pii_and_warns(
id="dsnless_spotlight_rederives_data_collection",
),
pytest.param(
{"spotlight": True, "data_collection": {"user_info": False}},
{
"spotlight": True,
"_experiments": {"data_collection": {"user_info": False}},
},
{"provided_by_user": True, "user_info": False},
id="dsnless_spotlight_respects_explicit_data_collection",
),
Expand All @@ -293,3 +312,23 @@ def test_client_data_collection_settings(init_kwargs, expected):
assert client.should_send_default_pii() is value
else:
assert client.options["data_collection"][key] is value


def test_has_data_collection_enabled_gates_on_presence():
assert has_data_collection_enabled(None) is False
assert has_data_collection_enabled({"_experiments": {}}) is False
assert (
has_data_collection_enabled({"_experiments": {"data_collection": {}}}) is True
)
assert (
has_data_collection_enabled({"_experiments": {"data_collection": None}}) is True
)


def test_no_experiments_data_collection_values_fall_back_to_send_default_pii_configuration():
sentry_sdk.init(send_default_pii=True)
client = sentry_sdk.get_client()
dc = client.options["data_collection"]
assert dc["provided_by_user"] is False
assert dc["user_info"] is True
assert has_data_collection_enabled(client.options) is False
Loading