From 5a2c70575353fe469c64965dc50d1f20fce5ad63 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 09:41:45 -0400 Subject: [PATCH 01/22] feat(django): Gate user identity behind data_collection config Apply the data_collection.user_info setting to Django user identity (id, email, username) collection, giving it precedence over the legacy send_default_pii boolean when explicitly configured. This mirrors the existing behavior for client IP/user info already shipped for Flask, WSGI, and ASGI. The user extraction logic in _after_get_response is pulled into a shared helper used by both the WSGI and ASGI event processors, and the parametrized send_default_pii/data_collection test matrix is extracted into tests/integrations/django/utils.py for reuse across the WSGI and ASGI test suites. Refs PY-2583 --- sentry_sdk/integrations/django/__init__.py | 78 +++++++++------ sentry_sdk/integrations/django/asgi.py | 8 +- tests/integrations/django/asgi/test_asgi.py | 33 +++++++ tests/integrations/django/myapp/urls.py | 5 + tests/integrations/django/myapp/views.py | 10 ++ .../django/test_data_scrubbing.py | 99 ++++++++++++------- tests/integrations/django/utils.py | 37 +++++++ 7 files changed, 202 insertions(+), 68 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 2c561164e6..0f8dfcd24a 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -30,6 +30,7 @@ capture_internal_exceptions, ensure_integration_enabled, event_from_exception, + has_data_collection_enabled, logger, transaction_from_function, walk_exception_chain, @@ -457,6 +458,39 @@ def _attempt_resolve_again( _set_transaction_name_and_source(scope, transaction_style, request) +def _get_user_from_request_and_set_on_scope(request: "WSGIRequest") -> None: + user = getattr(request, "user", None) + + # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. + # Exit early if the user has not been materialized yet. + is_lazy = isinstance(user, SimpleLazyObject) + if is_lazy and hasattr(request, "_cached_user"): + user = request._cached_user + elif is_lazy: + return + + if user is None or not is_authenticated(user): + return + + user_info = {} + try: + user_info["id"] = str(user.pk) + except Exception: + pass + + try: + user_info["email"] = user.email + except Exception: + pass + + try: + user_info["username"] = user.get_username() + except Exception: + pass + + sentry_sdk.set_user(user_info) + + def _after_get_response(request: "WSGIRequest") -> None: client = sentry_sdk.get_client() integration = client.get_integration(DjangoIntegration) @@ -468,37 +502,12 @@ def _after_get_response(request: "WSGIRequest") -> None: _attempt_resolve_again(request, scope, integration.transaction_style) span_streaming = has_span_streaming_enabled(client.options) - if span_streaming and should_send_default_pii(): - user = getattr(request, "user", None) - - # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. - # Exit early if the user has not been materialized yet. - is_lazy = isinstance(user, SimpleLazyObject) - if is_lazy and hasattr(request, "_cached_user"): - user = request._cached_user - elif is_lazy: - return - - if user is None or not is_authenticated(user): - return - - user_info = {} - try: - user_info["id"] = str(user.pk) - except Exception: - pass - - try: - user_info["email"] = user.email - except Exception: - pass - - try: - user_info["username"] = user.get_username() - except Exception: - pass - - sentry_sdk.set_user(user_info) + if span_streaming: + if has_data_collection_enabled(client.options): + if client.options["data_collection"]["user_info"]: + _get_user_from_request_and_set_on_scope(request) + elif should_send_default_pii(): + _get_user_from_request_and_set_on_scope(request) def _patch_get_response() -> None: @@ -544,7 +553,12 @@ def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Eve with capture_internal_exceptions(): DjangoRequestExtractor(request).extract_into_event(event) - if should_send_default_pii(): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + with capture_internal_exceptions(): + _set_user_info(request, event) + elif should_send_default_pii(): with capture_internal_exceptions(): _set_user_info(request, event) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index 785fa2af34..df287e6436 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -22,6 +22,7 @@ from sentry_sdk.utils import ( capture_internal_exceptions, ensure_integration_enabled, + has_data_collection_enabled, ) if TYPE_CHECKING: @@ -70,7 +71,12 @@ def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Eve with capture_internal_exceptions(): DjangoRequestExtractor(request).extract_into_event(event) - if should_send_default_pii(): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + with capture_internal_exceptions(): + _set_user_info(request, event) + elif should_send_default_pii(): with capture_internal_exceptions(): _set_user_info(request, event) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 700704ad4f..a8617cfb4a 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -16,6 +16,10 @@ from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django.asgi import _asgi_middleware_mixin_factory from tests.integrations.django.myapp.asgi import channels_application +from tests.integrations.django.utils import ( + USER_INFO_INIT_KWARGS, + pytest_mark_django_db_decorator, +) try: from django.urls import reverse @@ -1096,3 +1100,32 @@ async def test_async_middleware_process_exception_is_awaited( assert response["status"] == 200 assert response["body"] == b"handled by async process_exception" + + +@pytest.mark.parametrize("application", APPS) +@pytest.mark.asyncio +@pytest.mark.skipif( + django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0" +) +@pytest.mark.parametrize("init_kwargs, expect_user", USER_INFO_INIT_KWARGS) +@pytest_mark_django_db_decorator() +async def test_user_identity_error_event_data_collection( + sentry_init, capture_events, application, init_kwargs, expect_user +): + sentry_init(integrations=[DjangoIntegration()], **init_kwargs) + events = capture_events() + + comm = HttpCommunicator(application, "GET", "/mylogin-with-exception") + await comm.get_response() + await comm.wait() + + event = events[-1] + + if expect_user: + assert event["user"]["id"] == "1" + assert event["user"]["email"] == "lennon@thebeatles.com" + assert event["user"]["username"] == "john" + else: + assert "id" not in event.get("user", {}) + assert "email" not in event.get("user", {}) + assert "username" not in event.get("user", {}) diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py index 87e9750889..2c1cad4298 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -47,6 +47,11 @@ def path(path, *args, **kwargs): path("nomessage", views.nomessage, name="nomessage"), path("view-with-signal", views.view_with_signal, name="view_with_signal"), path("mylogin", views.mylogin, name="mylogin"), + path( + "mylogin-with-exception", + views.mylogin_with_exception, + name="mylogin_with_exception", + ), path("classbased", views.ClassBasedView.as_view(), name="classbased"), path("sentryclass", views.SentryClassBasedView(), name="sentryclass"), path( diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index 80587acaa3..ebaa3b37eb 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -135,6 +135,16 @@ def mylogin(request): return HttpResponse("ok") +@csrf_exempt +def mylogin_with_exception(request): + user, _ = User.objects.get_or_create( + username="john", defaults={"email": "lennon@thebeatles.com"} + ) + user.backend = "django.contrib.auth.backends.ModelBackend" + login(request, user) + 1 / 0 + + @csrf_exempt def handler500(request): return HttpResponseServerError("Sentry error.") diff --git a/tests/integrations/django/test_data_scrubbing.py b/tests/integrations/django/test_data_scrubbing.py index 5e0eb03508..4f20153a89 100644 --- a/tests/integrations/django/test_data_scrubbing.py +++ b/tests/integrations/django/test_data_scrubbing.py @@ -6,7 +6,10 @@ from sentry_sdk.integrations.django import DjangoIntegration from tests.conftest import unpack_werkzeug_response, werkzeug_set_cookie from tests.integrations.django.myapp.wsgi import application -from tests.integrations.django.utils import pytest_mark_django_db_decorator +from tests.integrations.django.utils import ( + USER_INFO_INIT_KWARGS, + pytest_mark_django_db_decorator, +) try: from django.urls import reverse @@ -392,40 +395,6 @@ def test_empty_query_string_is_dropped_with_data_collection( assert "query_string" not in event["request"] -USER_INFO_INIT_KWARGS = [ - pytest.param({"send_default_pii": True}, True, id="legacy_send_default_pii_true"), - pytest.param( - {"send_default_pii": False}, False, id="legacy_send_default_pii_false" - ), - pytest.param( - {"_experiments": {"data_collection": {"user_info": True}}}, - True, - id="data_collection_user_info_true", - ), - pytest.param( - {"_experiments": {"data_collection": {"user_info": False}}}, - False, - id="data_collection_user_info_false", - ), - pytest.param( - { - "send_default_pii": True, - "_experiments": {"data_collection": {"user_info": False}}, - }, - False, - id="data_collection_wins_over_send_default_pii_true", - ), - pytest.param( - { - "send_default_pii": False, - "_experiments": {"data_collection": {"user_info": True}}, - }, - True, - id="data_collection_wins_over_send_default_pii_false", - ), -] - - @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.parametrize("init_kwargs, expect_ip", USER_INFO_INIT_KWARGS) @@ -462,6 +431,42 @@ def test_user_info_span_attributes_data_collection( assert "client.address" not in root_span["attributes"] +@pytest.mark.forked +@pytest_mark_django_db_decorator() +@pytest.mark.parametrize("init_kwargs, expect_user", USER_INFO_INIT_KWARGS) +def test_user_identity_span_attributes_data_collection( + sentry_init, client, capture_items, init_kwargs, expect_user +): + init_kwargs = dict(init_kwargs) # shallow copy so we can mutate + experiments = init_kwargs.pop("_experiments", {}) + + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + _experiments=experiments, + **init_kwargs, + ) + + unpack_werkzeug_response(client.get(reverse("mylogin"))) + + items = capture_items("span") + unpack_werkzeug_response(client.get(reverse("template_test"))) + sentry_sdk.flush() + + spans = [item.payload for item in items] + (span,) = (s for s in spans if s["name"] == "/template-test") + + if expect_user: + assert span["attributes"][SPANDATA.USER_ID] == "1" + assert span["attributes"][SPANDATA.USER_EMAIL] == "lennon@thebeatles.com" + assert span["attributes"][SPANDATA.USER_NAME] == "john" + else: + assert SPANDATA.USER_ID not in span["attributes"] + assert SPANDATA.USER_EMAIL not in span["attributes"] + assert SPANDATA.USER_NAME not in span["attributes"] + + @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.parametrize("init_kwargs, expect_ip", USER_INFO_INIT_KWARGS) @@ -483,6 +488,30 @@ def test_user_info_error_event_data_collection( assert "REMOTE_ADDR" not in event["request"]["env"] +@pytest.mark.forked +@pytest_mark_django_db_decorator() +@pytest.mark.parametrize("init_kwargs, expect_user", USER_INFO_INIT_KWARGS) +def test_user_identity_error_event_data_collection( + sentry_init, client, capture_events, init_kwargs, expect_user +): + sentry_init(integrations=[DjangoIntegration()], **init_kwargs) + events = capture_events() + + client.get(reverse("mylogin")) + client.get(reverse("view_exc")) + + event = events[-1] + + if expect_user: + assert event["user"]["id"] == "1" + assert event["user"]["email"] == "lennon@thebeatles.com" + assert event["user"]["username"] == "john" + else: + assert "id" not in event.get("user", {}) + assert "email" not in event.get("user", {}) + assert "username" not in event.get("user", {}) + + @pytest.mark.forked @pytest_mark_django_db_decorator() def test_error_event_no_user_ip_address_without_remote_addr( diff --git a/tests/integrations/django/utils.py b/tests/integrations/django/utils.py index 8270b997ea..94495def4a 100644 --- a/tests/integrations/django/utils.py +++ b/tests/integrations/django/utils.py @@ -19,3 +19,40 @@ ) except AttributeError: pass + + +# Shared parametrization test matrix exercising the precedence between the legacy +# ``send_default_pii`` boolean and the ``data_collection.user_info`` setting. +# The second value indicates whether user info is expected to be collected. +USER_INFO_INIT_KWARGS = [ + pytest.param({"send_default_pii": True}, True, id="legacy_send_default_pii_true"), + pytest.param( + {"send_default_pii": False}, False, id="legacy_send_default_pii_false" + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": True}}}, + True, + id="data_collection_user_info_true", + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": False}}}, + False, + id="data_collection_user_info_false", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": {"data_collection": {"user_info": False}}, + }, + False, + id="data_collection_wins_over_send_default_pii_true", + ), + pytest.param( + { + "send_default_pii": False, + "_experiments": {"data_collection": {"user_info": True}}, + }, + True, + id="data_collection_wins_over_send_default_pii_false", + ), +] From 4ee42d032cfafb5c39d30a0f289f03de74f37cfc Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 10:18:17 -0400 Subject: [PATCH 02/22] test(django): Run user identity ASGI test in a forked process The new test was the only DB-touching test without @pytest.mark.forked. Running in the parent process leaked the created user into the shared in-memory sqlite DB (UNIQUE constraint failures in test_basic) and caused the parent to hold the session-scoped django_db_setup fixture, so forked children skipped postgres test DB creation but still destroyed it on teardown (database does not exist errors). --- tests/integrations/django/asgi/test_asgi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index a8617cfb4a..fb3212aff8 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -1102,6 +1102,7 @@ async def test_async_middleware_process_exception_is_awaited( assert response["body"] == b"handled by async process_exception" +@pytest.mark.forked @pytest.mark.parametrize("application", APPS) @pytest.mark.asyncio @pytest.mark.skipif( From 86566bdfae4ee9520c8cd530195ad1d5e8398c70 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 10:33:23 -0400 Subject: [PATCH 03/22] ref(tests): Share data_collection user info test matrix across suites Move the identical USER_INFO_INIT_KWARGS parametrization from the flask, wsgi, and django test suites into tests/integrations/utils.py as DATA_COLLECTION_USER_INFO_CASES, documenting that the second element of each case is whether user info is expected to be collected. --- tests/integrations/django/asgi/test_asgi.py | 8 ++-- .../django/test_data_scrubbing.py | 14 +++--- tests/integrations/django/utils.py | 37 --------------- tests/integrations/flask/test_flask.py | 47 ++----------------- tests/integrations/utils.py | 38 +++++++++++++++ tests/integrations/wsgi/test_wsgi.py | 43 ++--------------- 6 files changed, 55 insertions(+), 132 deletions(-) create mode 100644 tests/integrations/utils.py diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index fb3212aff8..752dba702a 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -16,10 +16,8 @@ from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django.asgi import _asgi_middleware_mixin_factory from tests.integrations.django.myapp.asgi import channels_application -from tests.integrations.django.utils import ( - USER_INFO_INIT_KWARGS, - pytest_mark_django_db_decorator, -) +from tests.integrations.django.utils import pytest_mark_django_db_decorator +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES try: from django.urls import reverse @@ -1108,7 +1106,7 @@ async def test_async_middleware_process_exception_is_awaited( @pytest.mark.skipif( django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0" ) -@pytest.mark.parametrize("init_kwargs, expect_user", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) @pytest_mark_django_db_decorator() async def test_user_identity_error_event_data_collection( sentry_init, capture_events, application, init_kwargs, expect_user diff --git a/tests/integrations/django/test_data_scrubbing.py b/tests/integrations/django/test_data_scrubbing.py index 4f20153a89..559b9b0466 100644 --- a/tests/integrations/django/test_data_scrubbing.py +++ b/tests/integrations/django/test_data_scrubbing.py @@ -6,10 +6,8 @@ from sentry_sdk.integrations.django import DjangoIntegration from tests.conftest import unpack_werkzeug_response, werkzeug_set_cookie from tests.integrations.django.myapp.wsgi import application -from tests.integrations.django.utils import ( - USER_INFO_INIT_KWARGS, - pytest_mark_django_db_decorator, -) +from tests.integrations.django.utils import pytest_mark_django_db_decorator +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES try: from django.urls import reverse @@ -397,7 +395,7 @@ def test_empty_query_string_is_dropped_with_data_collection( @pytest.mark.forked @pytest_mark_django_db_decorator() -@pytest.mark.parametrize("init_kwargs, expect_ip", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) def test_user_info_span_attributes_data_collection( sentry_init, client, capture_items, init_kwargs, expect_ip ): @@ -433,7 +431,7 @@ def test_user_info_span_attributes_data_collection( @pytest.mark.forked @pytest_mark_django_db_decorator() -@pytest.mark.parametrize("init_kwargs, expect_user", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) def test_user_identity_span_attributes_data_collection( sentry_init, client, capture_items, init_kwargs, expect_user ): @@ -469,7 +467,7 @@ def test_user_identity_span_attributes_data_collection( @pytest.mark.forked @pytest_mark_django_db_decorator() -@pytest.mark.parametrize("init_kwargs, expect_ip", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) def test_user_info_error_event_data_collection( sentry_init, client, capture_events, init_kwargs, expect_ip ): @@ -490,7 +488,7 @@ def test_user_info_error_event_data_collection( @pytest.mark.forked @pytest_mark_django_db_decorator() -@pytest.mark.parametrize("init_kwargs, expect_user", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) def test_user_identity_error_event_data_collection( sentry_init, client, capture_events, init_kwargs, expect_user ): diff --git a/tests/integrations/django/utils.py b/tests/integrations/django/utils.py index 94495def4a..8270b997ea 100644 --- a/tests/integrations/django/utils.py +++ b/tests/integrations/django/utils.py @@ -19,40 +19,3 @@ ) except AttributeError: pass - - -# Shared parametrization test matrix exercising the precedence between the legacy -# ``send_default_pii`` boolean and the ``data_collection.user_info`` setting. -# The second value indicates whether user info is expected to be collected. -USER_INFO_INIT_KWARGS = [ - pytest.param({"send_default_pii": True}, True, id="legacy_send_default_pii_true"), - pytest.param( - {"send_default_pii": False}, False, id="legacy_send_default_pii_false" - ), - pytest.param( - {"_experiments": {"data_collection": {"user_info": True}}}, - True, - id="data_collection_user_info_true", - ), - pytest.param( - {"_experiments": {"data_collection": {"user_info": False}}}, - False, - id="data_collection_user_info_false", - ), - pytest.param( - { - "send_default_pii": True, - "_experiments": {"data_collection": {"user_info": False}}, - }, - False, - id="data_collection_wins_over_send_default_pii_true", - ), - pytest.param( - { - "send_default_pii": False, - "_experiments": {"data_collection": {"user_info": True}}, - }, - True, - id="data_collection_wins_over_send_default_pii_false", - ), -] diff --git a/tests/integrations/flask/test_flask.py b/tests/integrations/flask/test_flask.py index c091e23f61..71c56d3fef 100644 --- a/tests/integrations/flask/test_flask.py +++ b/tests/integrations/flask/test_flask.py @@ -32,6 +32,7 @@ from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.serializer import MAX_DATABAG_BREADTH +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES # Query string used across the query-param filtering tests below. ``auth`` is a # built-in sensitive term, so it is redacted by the default denylist. @@ -1398,45 +1399,7 @@ def test_empty_query_string_is_dropped_with_data_collection( assert "query_string" not in event["request"] -# Parametrization shared by the user_info tests below. ``expect_ip`` is -# whether the client IP may be collected under the given init kwargs. -USER_INFO_INIT_KWARGS = [ - pytest.param({"send_default_pii": True}, True, id="legacy_send_default_pii_true"), - pytest.param( - {"send_default_pii": False}, False, id="legacy_send_default_pii_false" - ), - pytest.param( - {"_experiments": {"data_collection": {"user_info": True}}}, - True, - id="data_collection_user_info_true", - ), - pytest.param( - {"_experiments": {"data_collection": {"user_info": False}}}, - False, - id="data_collection_user_info_false", - ), - # ``data_collection`` is the single source of truth: it must win over - # ``send_default_pii`` when both are configured. - pytest.param( - { - "send_default_pii": True, - "_experiments": {"data_collection": {"user_info": False}}, - }, - False, - id="data_collection_wins_over_send_default_pii_true", - ), - pytest.param( - { - "send_default_pii": False, - "_experiments": {"data_collection": {"user_info": True}}, - }, - True, - id="data_collection_wins_over_send_default_pii_false", - ), -] - - -@pytest.mark.parametrize("init_kwargs, expect_ip", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) def test_user_info_span_attributes_data_collection( sentry_init, app, capture_items, monkeypatch, init_kwargs, expect_ip ): @@ -1472,7 +1435,7 @@ def test_user_info_span_attributes_data_collection( assert "client.address" not in segment["attributes"] -@pytest.mark.parametrize("init_kwargs, expect_ip", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) def test_user_info_error_event_data_collection( sentry_init, app, capture_events, monkeypatch, init_kwargs, expect_ip ): @@ -1523,7 +1486,7 @@ def crash(): assert "ip_address" not in event.get("user", {}) -@pytest.mark.parametrize("init_kwargs, expect_user", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) def test_flask_login_user_identity_error_event_data_collection( sentry_init, app, capture_events, init_kwargs, expect_user ): @@ -1571,7 +1534,7 @@ def crash(): assert "username" not in user -@pytest.mark.parametrize("init_kwargs, expect_user", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) def test_flask_login_user_identity_span_attributes_data_collection( sentry_init, app, capture_items, init_kwargs, expect_user ): diff --git a/tests/integrations/utils.py b/tests/integrations/utils.py new file mode 100644 index 0000000000..557ed3b62c --- /dev/null +++ b/tests/integrations/utils.py @@ -0,0 +1,38 @@ +import pytest + +# Shared parametrization test matrix exercising the precedence between the legacy +# ``send_default_pii`` boolean and the ``data_collection.user_info`` setting. +# Each case is ``(init_kwargs, expect_user_info)`` where the second element indicates +# whether user info (IP address, user identity, etc.) is expected to be collected. +DATA_COLLECTION_USER_INFO_CASES = [ + pytest.param({"send_default_pii": True}, True, id="legacy_send_default_pii_true"), + pytest.param( + {"send_default_pii": False}, False, id="legacy_send_default_pii_false" + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": True}}}, + True, + id="data_collection_user_info_true", + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": False}}}, + False, + id="data_collection_user_info_false", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": {"data_collection": {"user_info": False}}, + }, + False, + id="data_collection_wins_over_send_default_pii_true", + ), + pytest.param( + { + "send_default_pii": False, + "_experiments": {"data_collection": {"user_info": True}}, + }, + True, + id="data_collection_wins_over_send_default_pii_false", + ), +] diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 084abe0745..eb73be1499 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -11,6 +11,7 @@ _ScopedResponse, get_request_url, ) +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES @pytest.fixture @@ -1303,45 +1304,7 @@ def dogpark(environ, start_response): assert "user.ip_address" not in child_span["attributes"] -# Parametrization shared by the user_info tests below. ``expect_ip`` is -# whether the client IP may be collected under the given init kwargs. -USER_INFO_INIT_KWARGS = [ - pytest.param({"send_default_pii": True}, True, id="legacy_send_default_pii_true"), - pytest.param( - {"send_default_pii": False}, False, id="legacy_send_default_pii_false" - ), - pytest.param( - {"_experiments": {"data_collection": {"user_info": True}}}, - True, - id="data_collection_user_info_true", - ), - pytest.param( - {"_experiments": {"data_collection": {"user_info": False}}}, - False, - id="data_collection_user_info_false", - ), - # ``data_collection`` is the single source of truth: it must win over - # ``send_default_pii`` when both are configured. - pytest.param( - { - "send_default_pii": True, - "_experiments": {"data_collection": {"user_info": False}}, - }, - False, - id="data_collection_wins_over_send_default_pii_true", - ), - pytest.param( - { - "send_default_pii": False, - "_experiments": {"data_collection": {"user_info": True}}, - }, - True, - id="data_collection_wins_over_send_default_pii_false", - ), -] - - -@pytest.mark.parametrize("init_kwargs, expect_ip", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) def test_user_info_span_attributes_data_collection( sentry_init, capture_items, init_kwargs, expect_ip ): @@ -1381,7 +1344,7 @@ def dogpark(environ, start_response): assert "client.address" not in server_span["attributes"] -@pytest.mark.parametrize("init_kwargs, expect_ip", USER_INFO_INIT_KWARGS) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) def test_user_info_error_event_data_collection( sentry_init, crashing_app, capture_events, init_kwargs, expect_ip ): From 057caa49746219e77de37f1255661d1dbc61583d Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 10:36:20 -0400 Subject: [PATCH 04/22] feat(sanic): Gate user info behind data_collection config Apply the data_collection.user_info setting to the Sanic integration's user IP collection, giving it precedence over the legacy send_default_pii boolean when explicitly configured. This covers both the user.ip_address scope attribute applied to all spans and the client.address span attribute, mirroring the behavior already shipped for Flask, WSGI, ASGI, and Django. The user IP span test is reparametrized with the shared DATA_COLLECTION_USER_INFO_CASES matrix from tests/integrations/utils.py, and a new test covers the previously untested client.address span attribute. Refs PY-2583 --- sentry_sdk/integrations/sanic.py | 18 +++++++--- tests/integrations/sanic/test_sanic.py | 49 +++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 3f584b37ed..2d839d1c61 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -196,8 +196,12 @@ async def _context_enter(request: "Request") -> None: sentry_sdk.traces.continue_trace(dict(request.headers)) scope.set_custom_sampling_context({"sanic_request": request}) - if should_send_default_pii() and request.remote_addr: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) + if request.remote_addr: + if has_data_collection_enabled(client.options): + if client.options["data_collection"]["user_info"]: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) + elif should_send_default_pii(): + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) span = sentry_sdk.traces.start_span( # Unless the request results in a 404 error, the name and source @@ -401,6 +405,10 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]": query=filtered_query or "" ).geturl() + if request.remote_addr: + if client_options["data_collection"]["user_info"]: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr + elif should_send_default_pii(): attributes[SPANDATA.URL_FULL] = request.url attributes["url.path"] = urlparts.path @@ -408,12 +416,12 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]": if urlparts.query: attributes[SPANDATA.HTTP_QUERY] = urlparts.query + if request.remote_addr: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr + if urlparts.scheme: attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme - if should_send_default_pii() and request.remote_addr: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr - return attributes diff --git a/tests/integrations/sanic/test_sanic.py b/tests/integrations/sanic/test_sanic.py index 1f030c425f..0bc6133744 100644 --- a/tests/integrations/sanic/test_sanic.py +++ b/tests/integrations/sanic/test_sanic.py @@ -16,6 +16,7 @@ from sentry_sdk.integrations.sanic import SanicIntegration from sentry_sdk.tracing import TransactionSource from tests.conftest import get_free_port +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES try: from sanic_testing import TestManager @@ -559,9 +560,9 @@ def test_span_origin(sentry_init, app, capture_events, capture_items, span_strea @pytest.mark.skipif( not PERFORMANCE_SUPPORTED, reason="Performance not supported on this Sanic version" ) -@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) def test_user_ip_address_on_all_spans( - sentry_init, app, capture_items, send_default_pii + sentry_init, app, capture_items, init_kwargs, expect_ip ): app.config.FORWARDED_SECRET = "test" @@ -575,8 +576,8 @@ def child_span_handler(request): integrations=[SanicIntegration()], default_integrations=False, traces_sample_rate=1.0, - send_default_pii=send_default_pii, trace_lifecycle="stream", + **init_kwargs, ) items = capture_items("span") @@ -592,7 +593,7 @@ def child_span_handler(request): child_span, server_span = [item.payload for item in items] - if send_default_pii: + if expect_ip: assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" assert child_span["attributes"]["user.ip_address"] == "127.0.0.1" else: @@ -600,6 +601,46 @@ def child_span_handler(request): assert "user.ip_address" not in child_span["attributes"] +@pytest.mark.skipif( + not PERFORMANCE_SUPPORTED, reason="Performance not supported on this Sanic version" +) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) +def test_client_address_span_attribute_data_collection( + sentry_init, app, capture_items, init_kwargs, expect_ip +): + app.config.FORWARDED_SECRET = "test" + + sentry_init( + integrations=[SanicIntegration()], + default_integrations=False, + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + items = capture_items("span") + + c = get_client(app) + with c as client: + client.get( + "/message", + headers={"Forwarded": "for=127.0.0.1;secret=test"}, + ) + + sentry_sdk.flush() + + (server_span,) = [ + item.payload + for item in items + if item.payload["attributes"].get("sentry.origin") == "auto.http.sanic" + ] + + if expect_ip: + assert server_span["attributes"]["client.address"] == "127.0.0.1" + else: + assert "client.address" not in server_span["attributes"] + + _QUERY_PARAM_DATA_COLLECTION_CASES = [ pytest.param( {"send_default_pii": True}, From c7cb8f89c2cd426a4afe08a5fbb8fce3f22b629c Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 11:04:35 -0400 Subject: [PATCH 05/22] feat(tornado): Gate user info behind data_collection config Apply the data_collection.user_info setting to the Tornado integration's user info collection, giving it precedence over the legacy send_default_pii boolean when explicitly configured. This covers the user.ip_address scope attribute applied to all spans, the client.address span attribute, and the is_authenticated user flag on error events, mirroring the behavior already shipped for Flask, WSGI, ASGI, Django, and Sanic. The user IP span test is reparametrized with the shared DATA_COLLECTION_USER_INFO_CASES matrix from tests/integrations/utils.py, and new tests cover the previously untested client.address span attribute and the data_collection gating of the is_authenticated flag. Refs PY-2583 --- sentry_sdk/integrations/tornado.py | 29 ++++++++-- tests/integrations/tornado/test_tornado.py | 65 ++++++++++++++++++++-- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 836f3a4b25..2c8923d1bd 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -133,8 +133,12 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] sentry_sdk.traces.continue_trace(dict(headers)) scope.set_custom_sampling_context({"tornado_request": self.request}) - if should_send_default_pii() and self.request.remote_ip: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + if self.request.remote_ip: + if has_data_collection_enabled(client.options): + if client.options["data_collection"]["user_info"]: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + elif should_send_default_pii(): + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) span_ctx = sentry_sdk.traces.start_span( name=_DEFAULT_ROOT_SPAN_NAME, @@ -218,6 +222,10 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": f"{parsed_url.url}?{filtered_query}" if filtered_query else parsed_url.url ) + if request.remote_ip: + if client_options["data_collection"]["user_info"]: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip + elif should_send_default_pii(): attributes[SPANDATA.URL_FULL] = request.full_url() attributes["url.path"] = request.path @@ -225,12 +233,12 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": if request.query: attributes[SPANDATA.URL_QUERY] = request.query + if request.remote_ip: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip + if request.protocol: attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol - if should_send_default_pii() and request.remote_ip: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip - with capture_internal_exceptions(): raw_data = _get_tornado_request_data(request) body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data @@ -310,7 +318,16 @@ def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": request_info["env"] = {"REMOTE_ADDR": request.remote_ip} request_info["headers"] = _filter_headers(dict(request.headers)) - if should_send_default_pii(): + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + try: + current_user = handler.current_user + except Exception: + current_user = None + + if current_user: + event.setdefault("user", {}).setdefault("is_authenticated", True) + elif should_send_default_pii(): try: current_user = handler.current_user except Exception: diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index 23fbc316d6..2268a791ee 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -8,6 +8,7 @@ from sentry_sdk import capture_message, start_transaction from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE from sentry_sdk.integrations.tornado import TornadoIntegration +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES @pytest.fixture @@ -701,6 +702,32 @@ def get(self): assert "user" not in event +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) +def test_user_auth_data_collection( + tornado_testcase, sentry_init, capture_events, init_kwargs, expect_user +): + sentry_init(integrations=[TornadoIntegration()], **init_kwargs) + events = capture_events() + + class UserHandler(RequestHandler): + def get(self): + 1 / 0 + + def get_current_user(self): + return 42 + + client = tornado_testcase(Application([(r"/auth", UserHandler)])) + + response = client.fetch("/auth") + assert response.code == 500 + + (event,) = events + if expect_user: + assert event["user"] == {"is_authenticated": True} + else: + assert "is_authenticated" not in event.get("user", {}) + + def test_formdata(tornado_testcase, sentry_init, capture_events): sentry_init(integrations=[TornadoIntegration()], send_default_pii=True) events = capture_events() @@ -927,15 +954,15 @@ def test_span_origin( assert event["contexts"]["trace"]["origin"] == "auto.http.tornado" -@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) def test_user_ip_address_on_all_spans( - tornado_testcase, sentry_init, capture_items, send_default_pii + tornado_testcase, sentry_init, capture_items, init_kwargs, expect_ip ): sentry_init( integrations=[TornadoIntegration()], traces_sample_rate=1.0, - send_default_pii=send_default_pii, trace_lifecycle="stream", + **init_kwargs, ) items = capture_items("span") @@ -947,9 +974,39 @@ def test_user_ip_address_on_all_spans( child_span, server_span = [item.payload for item in items] - if send_default_pii: + if expect_ip: assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" assert child_span["attributes"]["user.ip_address"] == "127.0.0.1" else: assert "user.ip_address" not in server_span["attributes"] assert "user.ip_address" not in child_span["attributes"] + + +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) +def test_client_address_span_attribute_data_collection( + tornado_testcase, sentry_init, capture_items, init_kwargs, expect_ip +): + sentry_init( + integrations=[TornadoIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + items = capture_items("span") + + client = tornado_testcase(Application([(r"/hi", ChildSpanHandler)])) + client.fetch("/hi") + + sentry_sdk.flush() + + (server_span,) = [ + item.payload + for item in items + if item.payload["attributes"].get("sentry.origin") == "auto.http.tornado" + ] + + if expect_ip: + assert server_span["attributes"]["client.address"] == "127.0.0.1" + else: + assert "client.address" not in server_span["attributes"] From 69185971ddc4220948f53cad74eb35be32995459 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 11:27:33 -0400 Subject: [PATCH 06/22] fix unbound var --- sentry_sdk/integrations/tornado.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 2c8923d1bd..bfb4e4391e 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -290,6 +290,7 @@ def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": event["transaction"] = transaction_from_function(method) or "" event["transaction_info"] = {"source": TransactionSource.COMPONENT} + client_options = sentry_sdk.get_client().options with capture_internal_exceptions(): extractor = TornadoRequestExtractor(request) extractor.extract_into_event(event) @@ -302,7 +303,6 @@ def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": request.path, ) - client_options = sentry_sdk.get_client().options if has_data_collection_enabled(client_options): if request.query: filtered_query = _apply_data_collection_filtering_to_query_string( From 9038fe5b4aefd65c788a01a975db8a0230de4c73 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 11:28:25 -0400 Subject: [PATCH 07/22] lint --- sentry_sdk/integrations/tornado.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index bfb4e4391e..e71bf94d12 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -136,9 +136,13 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] if self.request.remote_ip: if has_data_collection_enabled(client.options): if client.options["data_collection"]["user_info"]: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, self.request.remote_ip + ) elif should_send_default_pii(): - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, self.request.remote_ip + ) span_ctx = sentry_sdk.traces.start_span( name=_DEFAULT_ROOT_SPAN_NAME, From 7377073fe2f9c175f6ed00805857a702ca8a7eb8 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 11:39:30 -0400 Subject: [PATCH 08/22] feat(pyramid): Gate user info behind data_collection config Apply the data_collection.user_info setting to the Pyramid integration's user id collection, giving it precedence over the legacy send_default_pii boolean when explicitly configured. This covers both the user.id segment attribute under span streaming and the user id attached to error events, mirroring the behavior already shipped for Flask, WSGI, ASGI, Django, Sanic, and Tornado. The user id segment test is reparametrized with the shared DATA_COLLECTION_USER_INFO_CASES matrix from tests/integrations/utils.py, and a new test adds the first coverage of the user id on error events. Refs PY-2583 --- sentry_sdk/integrations/pyramid.py | 23 ++++++++--- tests/integrations/pyramid/test_pyramid.py | 46 ++++++++++++++++++++-- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/pyramid.py b/sentry_sdk/integrations/pyramid.py index 6837d8345c..5ab6ef90e9 100644 --- a/sentry_sdk/integrations/pyramid.py +++ b/sentry_sdk/integrations/pyramid.py @@ -15,6 +15,7 @@ capture_internal_exceptions, ensure_integration_enabled, event_from_exception, + has_data_collection_enabled, reraise, ) @@ -86,10 +87,16 @@ def sentry_patched_call_view( scope = sentry_sdk.get_isolation_scope() - if should_send_default_pii() and has_span_streaming_enabled(client.options): - user_id = authenticated_userid(request) - if user_id: - scope.set_user({"id": user_id}) + if has_span_streaming_enabled(client.options): + if has_data_collection_enabled(client.options): + if client.options["data_collection"]["user_info"]: + user_id = authenticated_userid(request) + if user_id: + scope.set_user({"id": user_id}) + elif should_send_default_pii(): + user_id = authenticated_userid(request) + if user_id: + scope.set_user({"id": user_id}) scope.add_event_processor( _make_event_processor(weakref.ref(request), integration) @@ -229,7 +236,13 @@ def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": with capture_internal_exceptions(): PyramidRequestExtractor(request).extract_into_event(event) - if should_send_default_pii(): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + with capture_internal_exceptions(): + user_info = event.setdefault("user", {}) + user_info.setdefault("id", authenticated_userid(request)) + elif should_send_default_pii(): with capture_internal_exceptions(): user_info = event.setdefault("user", {}) user_info.setdefault("id", authenticated_userid(request)) diff --git a/tests/integrations/pyramid/test_pyramid.py b/tests/integrations/pyramid/test_pyramid.py index 06c38eccce..bdd5cb5049 100644 --- a/tests/integrations/pyramid/test_pyramid.py +++ b/tests/integrations/pyramid/test_pyramid.py @@ -15,6 +15,7 @@ from sentry_sdk.serializer import MAX_DATABAG_BREADTH from sentry_sdk.traces import SpanStatus from tests.conftest import unpack_werkzeug_response +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES try: from importlib.metadata import version @@ -559,19 +560,20 @@ def test_span_origin( assert event["contexts"]["trace"]["origin"] == "auto.http.pyramid" -@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) def test_span_sets_user_id_on_segment( sentry_init, pyramid_config, capture_items, get_client, - send_default_pii, + init_kwargs, + expect_user, ): sentry_init( integrations=[PyramidIntegration()], traces_sample_rate=1.0, - send_default_pii=send_default_pii, trace_lifecycle="stream", + **init_kwargs, ) class AuthenticationPolicy: @@ -592,7 +594,43 @@ def authenticated_userid(self, request): assert len(spans) == 1 (segment,) = spans - if send_default_pii: + if expect_user: assert segment["attributes"]["user.id"] == "123-abc" else: assert "user.id" not in segment["attributes"] + + +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) +def test_user_id_error_event_data_collection( + sentry_init, + pyramid_config, + capture_events, + route, + get_client, + init_kwargs, + expect_user, +): + sentry_init(integrations=[PyramidIntegration()], **init_kwargs) + events = capture_events() + + class AuthenticationPolicy: + def authenticated_userid(self, request): + return "123-abc" + + pyramid_config.set_authorization_policy(ACLAuthorizationPolicy()) + pyramid_config.set_authentication_policy(AuthenticationPolicy()) + + @route("/crash") + def crash(request): + 1 / 0 + + client = get_client() + with pytest.raises(ZeroDivisionError): + client.get("/crash") + + (event,) = events + + if expect_user: + assert event["user"]["id"] == "123-abc" + else: + assert "id" not in event.get("user", {}) From 1634927146572bd24658dd77c68992d39cb022a0 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 15:08:56 -0400 Subject: [PATCH 09/22] feat(aiohttp): Gate user info behind data_collection config Apply the data_collection.user_info setting to the aiohttp integration's user IP collection under span streaming, giving it precedence over the legacy send_default_pii boolean when explicitly configured. This covers both the client.address span attribute and the user.ip_address scope attribute, mirroring the behavior already shipped for Flask, WSGI, ASGI, Django, Sanic, Tornado, and Pyramid. Add test_user_address_with_data_collection_and_span_streaming, parametrized with the shared DATA_COLLECTION_USER_INFO_CASES matrix from tests/integrations/utils.py, and remove the now-redundant IP assertion block from test_sensitive_header_passthrough_with_pii_span_streaming that its TODO reserved for this change. Refs PY-2583 --- sentry_sdk/integrations/aiohttp.py | 24 ++++++++--- tests/integrations/aiohttp/test_aiohttp.py | 48 +++++++++++++++++----- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 9c21952119..858bf273f2 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -165,6 +165,8 @@ async def sentry_app_handle( ) url_attributes = {} + client_address_attributes = {} + if has_data_collection_enabled(client.options): url_attributes["url.full"] = "%s://%s%s" % ( request.scheme, @@ -188,6 +190,15 @@ async def sentry_app_handle( "?" + filtered_query_string ) + if request.remote: + if client.options["data_collection"]["user_info"]: + client_address_attributes["client.address"] = ( + request.remote + ) + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, request.remote + ) + elif should_send_default_pii(): url_full = "%s://%s%s" % ( request.scheme, @@ -201,12 +212,13 @@ async def sentry_app_handle( url_attributes["url.full"] = url_full url_attributes["url.path"] = request.path - client_address_attributes = {} - if should_send_default_pii() and request.remote: - client_address_attributes["client.address"] = request.remote - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, request.remote - ) + if request.remote: + client_address_attributes["client.address"] = ( + request.remote + ) + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, request.remote + ) span_ctx = sentry_sdk.traces.start_span( # If this name makes it to the UI, AIOHTTP's URL diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 34fcf6892c..438b009c07 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -26,6 +26,7 @@ ) from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE from tests.conftest import ApproxDict +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES @pytest.mark.asyncio @@ -1208,6 +1209,43 @@ async def hello(request): assert "http.request.header.host" in server_span["attributes"] +@pytest.mark.asyncio +@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES) +async def test_user_address_with_data_collection_and_span_streaming( + sentry_init, aiohttp_client, capture_items, init_kwargs, expect_ip +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + async def hello(request): + return web.Response(text="hello") + + app = web.Application() + app.router.add_get("/", hello) + + items = capture_items("span") + + client = await aiohttp_client(app) + resp = await client.get("/") + assert resp.status == 200 + + sentry_sdk.flush() + + (server_span,) = [item.payload for item in items] + assert server_span["attributes"]["sentry.origin"] == "auto.http.aiohttp" + + if expect_ip: + assert server_span["attributes"]["client.address"] == "127.0.0.1" + assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + else: + assert "client.address" not in server_span["attributes"] + assert "user.ip_address" not in server_span["attributes"] + + @pytest.mark.asyncio async def test_sensitive_header_scrubbing_span_streaming( sentry_init, aiohttp_client, capture_items @@ -1420,16 +1458,6 @@ async def hello(request): == expected["cookie"] ) - # client.address and user.ip_address is captured under send_default_pii=True. - # TODO: This block will eventually need to be removed from this test into a separate - # test once data collection gating is introduced on these values - if options["send_default_pii"]: - assert server_span["attributes"]["client.address"] == "127.0.0.1" - assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" - else: - assert "user.ip_address" not in server_span["attributes"] - assert "client.address" not in server_span["attributes"] - @pytest.mark.asyncio async def test_sensitive_header_passthrough_with_pii_span_streaming_without_data_collection( From 093a47e40aa9fe494f80da9feb30d413af5338c8 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 15:26:34 -0400 Subject: [PATCH 10/22] feat(starlite,litestar): Gate user info behind data_collection config Apply the data_collection.user_info setting to the Starlite and Litestar integrations' user info collection on exception events, giving it precedence over the legacy send_default_pii boolean when explicitly configured. This gates the retrieve_user_from_scope call in each integration's exception handler, mirroring the behavior already shipped for Flask, WSGI, ASGI, Django, Sanic, Tornado, Pyramid, and aiohttp. The scope user exception event tests in both suites are reparametrized with the shared DATA_COLLECTION_USER_INFO_CASES matrix from tests/integrations/utils.py. Refs PY-2583 --- sentry_sdk/integrations/litestar.py | 8 +++++++- sentry_sdk/integrations/starlite.py | 8 +++++++- tests/integrations/litestar/test_litestar.py | 20 ++++++------------- tests/integrations/starlite/test_starlite.py | 21 +++++--------------- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 7acfed478e..8a3f09ffa0 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -357,8 +357,14 @@ def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any] @ensure_integration_enabled(LitestarIntegration) def exception_handler(exc: Exception, scope: "LitestarScope") -> None: user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): + client_options = sentry_sdk.get_client().options + + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + user_info = retrieve_user_from_scope(scope) + elif should_send_default_pii(): user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): sentry_scope = sentry_sdk.get_isolation_scope() sentry_scope.set_user(user_info) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 6182d0f85c..8963fc9e53 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -312,8 +312,14 @@ def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any] @ensure_integration_enabled(StarliteIntegration) def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): + client_options = sentry_sdk.get_client().options + + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + user_info = retrieve_user_from_scope(scope) + elif should_send_default_pii(): user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): sentry_scope = sentry_sdk.get_isolation_scope() sentry_scope.set_user(user_info) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index 3567e5c740..6f3615dabc 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -19,6 +19,7 @@ from sentry_sdk.integrations.litestar import LitestarIntegration from tests.conftest import ApproxDict from tests.integrations.conftest import parametrize_test_configurable_status_codes +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES def litestar_app_factory(middleware=None, debug=True, exception_handlers=None): @@ -611,24 +612,15 @@ def test_span_origin( assert span["origin"] == "auto.http.litestar" -@pytest.mark.parametrize( - "is_send_default_pii", - [ - True, - False, - ], - ids=[ - "send_default_pii=True", - "send_default_pii=False", - ], -) +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) @pytest.mark.parametrize("span_streaming", [True, False]) def test_litestar_scope_user_on_exception_event( sentry_init, capture_exceptions, capture_events, capture_items, - is_send_default_pii, + init_kwargs, + expect_user, span_streaming, ): class TestUserMiddleware(AbstractMiddleware): @@ -642,8 +634,8 @@ async def __call__(self, scope, receive, send): sentry_init( integrations=[LitestarIntegration()], - send_default_pii=is_send_default_pii, trace_lifecycle="stream" if span_streaming else "static", + **init_kwargs, ) litestar_app = litestar_app_factory(middleware=[TestUserMiddleware]) @@ -673,7 +665,7 @@ async def __call__(self, scope, receive, send): assert len(events) == 1 (event,) = events - if is_send_default_pii: + if expect_user: assert "user" in event assert event["user"] == { "email": "lennon@thebeatles.com", diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index e41754b819..d596a5921f 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -13,6 +13,7 @@ from sentry_sdk import capture_message from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE from sentry_sdk.integrations.starlite import StarliteIntegration +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES def starlite_app_factory(middleware=None, debug=True, exception_handlers=None): @@ -525,19 +526,9 @@ def test_span_origin(sentry_init, capture_events, capture_items, span_streaming) assert span["origin"] == "auto.http.starlite" -@pytest.mark.parametrize( - "is_send_default_pii", - [ - True, - False, - ], - ids=[ - "send_default_pii=True", - "send_default_pii=False", - ], -) +@pytest.mark.parametrize("init_kwargs, expect_user", DATA_COLLECTION_USER_INFO_CASES) def test_starlite_scope_user_on_exception_event( - sentry_init, capture_exceptions, capture_events, is_send_default_pii + sentry_init, capture_exceptions, capture_events, init_kwargs, expect_user ): class TestUserMiddleware(AbstractMiddleware): async def __call__(self, scope, receive, send): @@ -548,9 +539,7 @@ async def __call__(self, scope, receive, send): } await self.app(scope, receive, send) - sentry_init( - integrations=[StarliteIntegration()], send_default_pii=is_send_default_pii - ) + sentry_init(integrations=[StarliteIntegration()], **init_kwargs) starlite_app = starlite_app_factory(middleware=[TestUserMiddleware]) exceptions = capture_exceptions() events = capture_events() @@ -566,7 +555,7 @@ async def __call__(self, scope, receive, send): assert len(events) == 1 (event,) = events - if is_send_default_pii: + if expect_user: assert "user" in event assert event["user"] == { "email": "lennon@thebeatles.com", From 6e3c9ca3e16b97e970d791e00d116234e54c9652 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 16:50:14 -0400 Subject: [PATCH 11/22] feat(asgi): Gate user info behind data_collection config --- sentry_sdk/integrations/asgi.py | 16 +++++++--- tests/integrations/asgi/test_asgi.py | 44 +++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index e1157dda77..d594c3504d 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -47,6 +47,7 @@ _get_installed_modules, capture_internal_exceptions, event_from_exception, + has_data_collection_enabled, logger, nullcontext, qualname_from_function, @@ -253,10 +254,17 @@ async def _run_app( "network.protocol.name": ty, } - if scope.get("client") and should_send_default_pii(): - sentry_scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, _get_ip(scope) - ) + if scope.get("client"): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + sentry_scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + ) + elif should_send_default_pii(): + sentry_scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + ) if ty in ("http", "websocket"): if ( diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 321ac91d75..4df9325547 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -1446,11 +1446,41 @@ async def test_custom_transaction_name( @pytest.mark.asyncio -@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize( + "init_kwargs, expect_ip", + [ + pytest.param({"send_default_pii": True}, True, id="legacy_pii_true"), + pytest.param({"send_default_pii": False}, False, id="legacy_pii_false"), + pytest.param( + {"_experiments": {"data_collection": {}}}, + True, + id="dc_default_user_info", + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": True}}}, + True, + id="dc_user_info_true", + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": False}}}, + False, + id="dc_user_info_false", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": {"data_collection": {"user_info": False}}, + }, + False, + id="dc_wins_over_pii", + ), + ], +) async def test_user_ip_address_on_all_spans( sentry_init, capture_items, - send_default_pii, + init_kwargs, + expect_ip, ): async def app(scope, receive, send): if scope["type"] == "lifespan": @@ -1474,11 +1504,9 @@ async def app(scope, receive, send): ) await send({"type": "http.response.body", "body": b"Hello, world!"}) - sentry_init( - send_default_pii=send_default_pii, - traces_sample_rate=1.0, - trace_lifecycle="stream", - ) + kwargs = {k: v for k, v in init_kwargs.items() if k != "_experiments"} + experiments = {"trace_lifecycle": "stream", **init_kwargs.get("_experiments", {})} + sentry_init(traces_sample_rate=1.0, _experiments=experiments, **kwargs) sentry_app = SentryAsgiMiddleware(app) async def wrapped_app(scope, receive, send): @@ -1493,7 +1521,7 @@ async def wrapped_app(scope, receive, send): child_span, server_span = [item.payload for item in items] - if send_default_pii: + if expect_ip: assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" assert child_span["attributes"]["user.ip_address"] == "127.0.0.1" else: From 64e29e9172266ca199040aaf14a0d75a83f977a8 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 16:50:14 -0400 Subject: [PATCH 12/22] feat(quart): Gate user info behind data_collection config --- sentry_sdk/integrations/quart.py | 35 ++++++- tests/integrations/quart/test_quart.py | 139 +++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/quart.py b/sentry_sdk/integrations/quart.py index 8c281f5874..444ef93fa5 100644 --- a/sentry_sdk/integrations/quart.py +++ b/sentry_sdk/integrations/quart.py @@ -235,9 +235,25 @@ async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: else parsed_url.url, ) - # TODO: Add the user properties that are seen in the branch below here once - # code is added to respect the `user_info` settings within the data collection - # configuration + if client_options["data_collection"]["user_info"]: + user_properties = {} + + if len(request_websocket.access_route) >= 1: + segment.set_attribute( + "client.address", request_websocket.access_route[0] + ) + user_properties["ip_address"] = request_websocket.access_route[ + 0 + ] + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_properties["id"] = current_user_id + + if user_properties: + existing_user_properties = scope._user or {} + scope.set_user({**existing_user_properties, **user_properties}) + elif should_send_default_pii(): segment.set_attribute("url.full", request_websocket.url) segment.set_attribute( @@ -284,7 +300,18 @@ def inner(event: "Event", hint: "dict[str, Any]") -> "Event": request_info["method"] = request.method request_info["headers"] = _filter_headers(dict(request.headers)) - if should_send_default_pii(): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + if len(request.access_route) >= 1: + request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_info = event.setdefault("user", {}) + user_info["id"] = current_user_id + + elif should_send_default_pii(): if len(request.access_route) >= 1: request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 286d6aeaff..73f8f42f57 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -1101,6 +1101,145 @@ async def login(): assert "user.id" not in segment.get("attributes", {}) +QUART_USER_INFO_CASES = [ + pytest.param( + {"_experiments": {"data_collection": {}}}, + True, + id="dc_default_user_info", + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": True}}}, + True, + id="dc_user_info_true", + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": False}}}, + False, + id="dc_user_info_false", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": {"data_collection": {"user_info": False}}, + }, + False, + id="dc_wins_over_pii", + ), +] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("init_kwargs, expect_user_info", QUART_USER_INFO_CASES) +async def test_quart_auth_user_info_data_collection( + sentry_init, + capture_events, + init_kwargs, + expect_user_info, +): + from quart_auth import AuthUser, login_user + + sentry_init(integrations=[quart_sentry.QuartIntegration()], **init_kwargs) + app = quart_app_factory() + + @app.route("/login") + async def login(): + login_user(AuthUser("42")) + return "ok" + + events = capture_events() + + client = app.test_client() + assert (await client.get("/login")).status_code == 200 + assert not events + + assert (await client.get("/message")).status_code == 200 + + (event,) = events + if expect_user_info: + assert event["user"]["id"] == "42" + assert "REMOTE_ADDR" in event["request"]["env"] + else: + assert event.get("user", {}).get("id") is None + assert "env" not in event["request"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("init_kwargs, expect_user_info", QUART_USER_INFO_CASES) +async def test_span_streaming_quart_auth_user_id_data_collection( + sentry_init, + capture_items, + init_kwargs, + expect_user_info, +): + from quart_auth import AuthUser, login_user + + kwargs = {k: v for k, v in init_kwargs.items() if k != "_experiments"} + sentry_init( + integrations=[quart_sentry.QuartIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + _experiments=init_kwargs.get("_experiments", {}), + **kwargs, + ) + items = capture_items("span") + + app = quart_app_factory() + + @app.route("/login") + async def login(): + login_user(AuthUser("42")) + return "ok" + + client = app.test_client() + assert (await client.get("/login")).status_code == 200 + assert (await client.get("/message")).status_code == 200 + + sentry_sdk.flush() + + spans = [item.payload for item in items] + assert len(spans) == 2 + + segment = spans[1] + if expect_user_info: + assert segment["attributes"]["user.id"] == "42" + else: + assert "user.id" not in segment.get("attributes", {}) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("init_kwargs, expect_user_info", QUART_USER_INFO_CASES) +async def test_span_streaming_request_attributes_data_collection( + sentry_init, capture_items, init_kwargs, expect_user_info +): + kwargs = {k: v for k, v in init_kwargs.items() if k != "_experiments"} + sentry_init( + integrations=[quart_sentry.QuartIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + _experiments=init_kwargs.get("_experiments", {}), + **kwargs, + ) + items = capture_items("span") + + app = quart_app_factory() + client = app.test_client() + response = await client.get("/message") + assert response.status_code == 200 + + sentry_sdk.flush() + + spans = [item.payload for item in items] + assert len(spans) == 1 + + segment = spans[0] + if expect_user_info: + assert "client.address" in segment["attributes"] + assert "user.ip_address" in segment["attributes"] + else: + assert "client.address" not in segment["attributes"] + assert "user.ip_address" not in segment["attributes"] + + @pytest.mark.asyncio async def test_span_streaming_sensitive_header_passthrough_with_pii_and_no_data_collection( sentry_init, capture_items From f409f024ec365fe55e34464e38fe780bfaf613d4 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 16:50:14 -0400 Subject: [PATCH 13/22] feat(starlette): Gate user info behind data_collection config --- sentry_sdk/integrations/starlette.py | 6 +- .../integrations/starlette/test_starlette.py | 97 ++++++++++--------- 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 21cdfeecc5..7579c70c11 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -375,7 +375,11 @@ def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: if "user" not in scope: return - if not should_send_default_pii(): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if not client_options["data_collection"]["user_info"]: + return + elif not should_send_default_pii(): return user_info: "Dict[str, Any]" = {} diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index bf0d7b0993..8c89c9b399 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -984,33 +984,40 @@ def test_catch_exceptions( assert event["exception"]["values"][0]["mechanism"]["type"] == "starlette" -def test_user_information_error(sentry_init, capture_events): - sentry_init( - send_default_pii=True, - integrations=[StarletteIntegration()], - ) - starlette_app = starlette_app_factory( - middleware=[Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())] - ) - events = capture_events() - - client = TestClient(starlette_app, raise_server_exceptions=False) - try: - client.get("/custom_error", auth=("Gabriela", "hello123")) - except Exception: - pass - - (event,) = events - user = event.get("user", None) - assert user - assert "username" in user - assert user["username"] == "Gabriela" +USER_AUTH_CASES = [ + pytest.param({"send_default_pii": True}, True, id="legacy_pii_true"), + pytest.param({"send_default_pii": False}, False, id="legacy_pii_false"), + pytest.param( + {"_experiments": {"data_collection": {}}}, + True, + id="dc_default_user_info", + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": True}}}, + True, + id="dc_user_info_true", + ), + pytest.param( + {"_experiments": {"data_collection": {"user_info": False}}}, + False, + id="dc_user_info_false", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": {"data_collection": {"user_info": False}}, + }, + False, + id="dc_wins_over_pii", + ), +] -def test_user_information_error_no_pii(sentry_init, capture_events): +@pytest.mark.parametrize("init_kwargs, expect_user", USER_AUTH_CASES) +def test_user_information_error(sentry_init, capture_events, init_kwargs, expect_user): sentry_init( - send_default_pii=False, integrations=[StarletteIntegration()], + **init_kwargs, ) starlette_app = starlette_app_factory( middleware=[Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())] @@ -1024,35 +1031,23 @@ def test_user_information_error_no_pii(sentry_init, capture_events): pass (event,) = events - assert "user" not in event - - -def test_user_information_transaction(sentry_init, capture_events): - sentry_init( - traces_sample_rate=1.0, - send_default_pii=True, - integrations=[StarletteIntegration()], - ) - starlette_app = starlette_app_factory( - middleware=[Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())] - ) - events = capture_events() - - client = TestClient(starlette_app, raise_server_exceptions=False) - client.get("/message", auth=("Gabriela", "hello123")) - - (_, transaction_event) = events - user = transaction_event.get("user", None) - assert user - assert "username" in user - assert user["username"] == "Gabriela" + if expect_user: + user = event.get("user", None) + assert user + assert "username" in user + assert user["username"] == "Gabriela" + else: + assert "user" not in event -def test_user_information_transaction_no_pii(sentry_init, capture_events): +@pytest.mark.parametrize("init_kwargs, expect_user", USER_AUTH_CASES) +def test_user_information_transaction( + sentry_init, capture_events, init_kwargs, expect_user +): sentry_init( traces_sample_rate=1.0, - send_default_pii=False, integrations=[StarletteIntegration()], + **init_kwargs, ) starlette_app = starlette_app_factory( middleware=[Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())] @@ -1063,7 +1058,13 @@ def test_user_information_transaction_no_pii(sentry_init, capture_events): client.get("/message", auth=("Gabriela", "hello123")) (_, transaction_event) = events - assert "user" not in transaction_event + if expect_user: + user = transaction_event.get("user", None) + assert user + assert "username" in user + assert user["username"] == "Gabriela" + else: + assert "user" not in transaction_event def test_user_information_does_not_clobber_app_set_user(sentry_init, capture_events): From 11fb505b28c594d5ec0c27cb3de75b35e5e93e85 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 16:50:14 -0400 Subject: [PATCH 14/22] feat(aws_lambda): Gate user info behind data_collection config --- sentry_sdk/integrations/aws_lambda.py | 18 +++++- .../.gitignore | 11 ++++ .../BasicOkDataCollectionUserInfoOff/index.py | 19 ++++++ .../.gitignore | 11 ++++ .../BasicOkDataCollectionUserInfoOn/index.py | 19 ++++++ .../aws_lambda/test_aws_lambda.py | 60 +++++++++++++++++++ 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOff/.gitignore create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOff/index.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOn/.gitignore create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOn/index.py diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 84047a7581..b0873df5b9 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -437,7 +437,23 @@ def event_processor( if "headers" in aws_event: request["headers"] = _filter_headers(aws_event["headers"]) - if should_send_default_pii(): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: + user_info = sentry_event.setdefault("user", {}) + + identity = aws_event.get("identity") + if identity is None: + identity = {} + + id = identity.get("userArn") + if id is not None: + user_info.setdefault("id", id) + + ip = identity.get("sourceIp") + if ip is not None: + user_info.setdefault("ip_address", ip) + elif should_send_default_pii(): user_info = sentry_event.setdefault("user", {}) identity = aws_event.get("identity") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOff/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOff/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOff/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOff/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOff/index.py new file mode 100644 index 0000000000..18c5450196 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOff/index.py @@ -0,0 +1,19 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + integrations=[AwsLambdaIntegration()], + _experiments={ + "data_collection": { + "user_info": False, + } + }, +) + + +def handler(event, context): + return {"event": event} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOn/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOn/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOn/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOn/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOn/index.py new file mode 100644 index 0000000000..07527dad4a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionUserInfoOn/index.py @@ -0,0 +1,19 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + integrations=[AwsLambdaIntegration()], + _experiments={ + "data_collection": { + "user_info": True, + } + }, +) + + +def handler(event, context): + return {"event": event} diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 3c03e1510b..cb56561abf 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -464,6 +464,66 @@ def test_request_data_with_send_default_pii_true(lambda_client, test_environment "data": None, } + # Legacy send_default_pii=True attaches the user identity. + assert transaction_event["user"] == { + "id": "42", + "ip_address": "213.47.147.207", + } + + +USER_INFO_PAYLOAD = b""" + { + "resource": "/asd", + "path": "/asd", + "httpMethod": "GET", + "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + "X-Forwarded-Proto": "https" + }, + "queryStringParameters": { + "bonkers": "true" + }, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" + } + }, + "body": null, + "isBase64Encoded": false + } +""" + + +def test_user_info_with_data_collection_user_info_on(lambda_client, test_environment): + lambda_client.invoke( + FunctionName="BasicOkDataCollectionUserInfoOn", + Payload=USER_INFO_PAYLOAD, + ) + envelopes = test_environment["server"].envelopes + + (transaction_event,) = envelopes + + assert transaction_event["user"] == { + "id": "42", + "ip_address": "213.47.147.207", + } + + +def test_user_info_with_data_collection_user_info_off(lambda_client, test_environment): + lambda_client.invoke( + FunctionName="BasicOkDataCollectionUserInfoOff", + Payload=USER_INFO_PAYLOAD, + ) + envelopes = test_environment["server"].envelopes + + (transaction_event,) = envelopes + + assert "user" not in transaction_event + def test_request_data_with_data_collection_allowlist(lambda_client, test_environment): payload = b""" From d28ee71a7be5e05b79a595dc9037c2d687118af1 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 23 Jul 2026 16:57:52 -0400 Subject: [PATCH 15/22] test(aws_lambda): Place identity at top level of test event payloads The request event processor reads aws_event["identity"] at the top level, but the payloads used by the user info tests nested it under requestContext, so no user was attached and the assertions failed in CI. --- tests/integrations/aws_lambda/test_aws_lambda.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index cb56561abf..aa4fd54032 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -427,11 +427,9 @@ def test_request_data_with_send_default_pii_true(lambda_client, test_environment }, "pathParameters": null, "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" }, "body": null, "isBase64Encoded": false @@ -486,11 +484,9 @@ def test_request_data_with_send_default_pii_true(lambda_client, test_environment }, "pathParameters": null, "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" }, "body": null, "isBase64Encoded": false From 2fbf67a0799f85c2d4062b487a74ae0510f234e0 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 24 Jul 2026 08:59:26 -0400 Subject: [PATCH 16/22] fix lambda tests --- tests/integrations/aws_lambda/test_aws_lambda.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index aa4fd54032..997fbb3c0b 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -427,9 +427,11 @@ def test_request_data_with_send_default_pii_true(lambda_client, test_environment }, "pathParameters": null, "stageVariables": null, - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" + }, }, "body": null, "isBase64Encoded": false @@ -484,9 +486,11 @@ def test_request_data_with_send_default_pii_true(lambda_client, test_environment }, "pathParameters": null, "stageVariables": null, - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42", + }, }, "body": null, "isBase64Encoded": false From c210fa3558b92314691e8c771baca0ded4fe5132 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 24 Jul 2026 09:12:16 -0400 Subject: [PATCH 17/22] . --- tests/integrations/asgi/test_asgi.py | 11 ++++++++--- tests/integrations/quart/test_quart.py | 23 ++++++++++++----------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 4df9325547..71feb2dc01 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -1504,9 +1504,14 @@ async def app(scope, receive, send): ) await send({"type": "http.response.body", "body": b"Hello, world!"}) - kwargs = {k: v for k, v in init_kwargs.items() if k != "_experiments"} - experiments = {"trace_lifecycle": "stream", **init_kwargs.get("_experiments", {})} - sentry_init(traces_sample_rate=1.0, _experiments=experiments, **kwargs) + kwargs = dict(init_kwargs) + experiments = init_kwargs.pop("_experiments") + sentry_init( + trace_lifecycle="stream", + traces_sample_rate=1.0, + _experiments=experiments, + **kwargs, + ) sentry_app = SentryAsgiMiddleware(app) async def wrapped_app(scope, receive, send): diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 73f8f42f57..896f0cc12f 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -1138,7 +1138,8 @@ async def test_quart_auth_user_info_data_collection( ): from quart_auth import AuthUser, login_user - sentry_init(integrations=[quart_sentry.QuartIntegration()], **init_kwargs) + kwargs = dict(init_kwargs) + sentry_init(integrations=[quart_sentry.QuartIntegration()], **kwargs) app = quart_app_factory() @app.route("/login") @@ -1173,12 +1174,12 @@ async def test_span_streaming_quart_auth_user_id_data_collection( ): from quart_auth import AuthUser, login_user - kwargs = {k: v for k, v in init_kwargs.items() if k != "_experiments"} + kwargs = dict(init_kwargs) sentry_init( integrations=[quart_sentry.QuartIntegration()], traces_sample_rate=1.0, trace_lifecycle="stream", - _experiments=init_kwargs.get("_experiments", {}), + _experiments=kwargs.pop("_experiments", {}), **kwargs, ) items = capture_items("span") @@ -1211,12 +1212,12 @@ async def login(): async def test_span_streaming_request_attributes_data_collection( sentry_init, capture_items, init_kwargs, expect_user_info ): - kwargs = {k: v for k, v in init_kwargs.items() if k != "_experiments"} + kwargs = dict(init_kwargs) sentry_init( integrations=[quart_sentry.QuartIntegration()], traces_sample_rate=1.0, trace_lifecycle="stream", - _experiments=init_kwargs.get("_experiments", {}), + _experiments=kwargs.pop("_experiments", {}), **kwargs, ) items = capture_items("span") @@ -1349,14 +1350,13 @@ async def test_span_streaming_sensitive_header_passthrough_with_pii_and_no_data_ async def test_span_streaming_url_query_data_collection( sentry_init, capture_items, init_kwargs, expected_query ): - init_kwargs = dict(init_kwargs) - experiments = {"trace_lifecycle": "stream"} - experiments.update(init_kwargs.pop("_experiments", {})) + kwargs = dict(init_kwargs) sentry_init( integrations=[quart_sentry.QuartIntegration()], traces_sample_rate=1.0, - _experiments=experiments, - **init_kwargs, + trace_lifecycle="stream", + _experiments=kwargs.pop("_experiments", {}), + **kwargs, ) items = capture_items("span") @@ -1399,7 +1399,8 @@ async def test_span_streaming_url_query_multi_and_blank_values( sentry_init( integrations=[quart_sentry.QuartIntegration()], traces_sample_rate=1.0, - _experiments={"trace_lifecycle": "stream", "data_collection": {}}, + trace_lifecycle="stream", + _experiments={"data_collection": {}}, ) items = capture_items("span") From 236efc7f996e8b00a416c4952d1ac072075e9605 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 24 Jul 2026 09:27:06 -0400 Subject: [PATCH 18/22] test(aws_lambda): Remove trailing commas from invoke payloads The JSON payloads in test_request_data_with_send_default_pii_true and USER_INFO_PAYLOAD contained trailing commas inside requestContext, which the Lambda runtime rejects with InvalidRequestContent. This caused three test failures in CI. --- tests/integrations/aws_lambda/test_aws_lambda.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 997fbb3c0b..01ca19bd54 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -431,7 +431,7 @@ def test_request_data_with_send_default_pii_true(lambda_client, test_environment "identity": { "sourceIp": "213.47.147.207", "userArn": "42" - }, + } }, "body": null, "isBase64Encoded": false @@ -489,8 +489,8 @@ def test_request_data_with_send_default_pii_true(lambda_client, test_environment "requestContext": { "identity": { "sourceIp": "213.47.147.207", - "userArn": "42", - }, + "userArn": "42" + } }, "body": null, "isBase64Encoded": false From c265efdcc5aa942c130408a89cd099d4c74b9e84 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 24 Jul 2026 09:30:00 -0400 Subject: [PATCH 19/22] . --- tests/integrations/asgi/test_asgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 71feb2dc01..cacc20a91a 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -1505,7 +1505,7 @@ async def app(scope, receive, send): await send({"type": "http.response.body", "body": b"Hello, world!"}) kwargs = dict(init_kwargs) - experiments = init_kwargs.pop("_experiments") + experiments = kwargs.pop("_experiments") sentry_init( trace_lifecycle="stream", traces_sample_rate=1.0, From 077e667b3969f39a24c440af0334d873004401ac Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 24 Jul 2026 09:34:11 -0400 Subject: [PATCH 20/22] . --- tests/integrations/quart/test_quart.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 896f0cc12f..e084d6ac1b 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -1351,11 +1351,12 @@ async def test_span_streaming_url_query_data_collection( sentry_init, capture_items, init_kwargs, expected_query ): kwargs = dict(init_kwargs) + experiments = kwargs.pop("_experiments", {}) sentry_init( integrations=[quart_sentry.QuartIntegration()], traces_sample_rate=1.0, trace_lifecycle="stream", - _experiments=kwargs.pop("_experiments", {}), + _experiments=experiments, **kwargs, ) items = capture_items("span") From 350f483c581e433c0040a839b2f3cb80f0ef7370 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 24 Jul 2026 09:36:33 -0400 Subject: [PATCH 21/22] . --- tests/integrations/asgi/test_asgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index cacc20a91a..76c730e5b6 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -1505,7 +1505,7 @@ async def app(scope, receive, send): await send({"type": "http.response.body", "body": b"Hello, world!"}) kwargs = dict(init_kwargs) - experiments = kwargs.pop("_experiments") + experiments = kwargs.pop("_experiments", {}) sentry_init( trace_lifecycle="stream", traces_sample_rate=1.0, From 2dd69af30ce0d3306d1e6ac80e39489f7619356b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 24 Jul 2026 09:59:55 -0400 Subject: [PATCH 22/22] fix bug found in aws lambda --- sentry_sdk/integrations/aws_lambda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index b0873df5b9..6dad75076c 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -442,7 +442,7 @@ def event_processor( if client_options["data_collection"]["user_info"]: user_info = sentry_event.setdefault("user", {}) - identity = aws_event.get("identity") + identity = aws_event.get("requestContext", {}).get("identity") if identity is None: identity = {} @@ -456,7 +456,7 @@ def event_processor( elif should_send_default_pii(): user_info = sentry_event.setdefault("user", {}) - identity = aws_event.get("identity") + identity = aws_event.get("requestContext", {}).get("identity") if identity is None: identity = {}