diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 3630dc8320..8eb37d37c4 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -978,12 +978,30 @@ class SPANDATA: Example: "MainThread" """ + USER_EMAIL = "user.email" + """ + User email address. + Example: "test@example.com" + """ + + USER_ID = "user.id" + """ + Unique identifier of the user. + Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" + """ + USER_IP_ADDRESS = "user.ip_address" """ The IP address of the user that triggered the request. Example: "10.1.2.80" """ + USER_NAME = "user.name" + """ + Short name or login/username of the user. + Example: "j.smith" + """ + URL_FULL = "url.full" """ The URL of the resource that was fetched. diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 4c444009e0..361b60079d 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -40,6 +40,7 @@ from django.conf import settings from django.conf import settings as django_settings from django.core import signals + from django.utils.functional import SimpleLazyObject try: from django.urls import resolve @@ -457,12 +458,47 @@ def _attempt_resolve_again( def _after_get_response(request: "WSGIRequest") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None or integration.transaction_style != "url": + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: return - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) + if integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _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) def _patch_get_response() -> None: diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index 4ffad98ad1..f7be9e5da6 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -530,6 +530,39 @@ def test_user_captured( } +@pytest.mark.forked +@pytest_mark_django_db_decorator() +def test_materialized_user_captured( + sentry_init, + client, + capture_events, + capture_items, +): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + + content, status, headers = unpack_werkzeug_response(client.get(reverse("mylogin"))) + assert content == b"ok" + + items = capture_items("span") + + content, status, headers = unpack_werkzeug_response( + client.get(reverse("template_test")) + ) + + sentry_sdk.flush() + spans = [item.payload for item in items] + (span,) = (span for span in spans if span["name"] == "/template-test") + + assert span["attributes"][SPANDATA.USER_ID] == "1" + assert span["attributes"][SPANDATA.USER_EMAIL] == "lennon@thebeatles.com" + assert span["attributes"][SPANDATA.USER_NAME] == "john" + + @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.parametrize("span_streaming", [True, False])