Skip to content

Commit 4b35d66

Browse files
feat(django): Add user attributes in span streaming (#6541)
Adapt the user extraction logic from `_set_user_info()`. Set user information on the isolation scope after `BaseHandler.get_response()` has returned. Do not set user attributes if the user is an instance of `SimpleLazyObject` that has not cached its underlying object yet. Bailing out early prevents the exception reported in #5274.
1 parent 43bb0ca commit 4b35d66

3 files changed

Lines changed: 91 additions & 4 deletions

File tree

sentry_sdk/consts.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,12 +978,30 @@ class SPANDATA:
978978
Example: "MainThread"
979979
"""
980980

981+
USER_EMAIL = "user.email"
982+
"""
983+
User email address.
984+
Example: "test@example.com"
985+
"""
986+
987+
USER_ID = "user.id"
988+
"""
989+
Unique identifier of the user.
990+
Example: "S-1-5-21-202424912787-2692429404-2351956786-1000"
991+
"""
992+
981993
USER_IP_ADDRESS = "user.ip_address"
982994
"""
983995
The IP address of the user that triggered the request.
984996
Example: "10.1.2.80"
985997
"""
986998

999+
USER_NAME = "user.name"
1000+
"""
1001+
Short name or login/username of the user.
1002+
Example: "j.smith"
1003+
"""
1004+
9871005
URL_FULL = "url.full"
9881006
"""
9891007
The URL of the resource that was fetched.

sentry_sdk/integrations/django/__init__.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from django.conf import settings
4141
from django.conf import settings as django_settings
4242
from django.core import signals
43+
from django.utils.functional import SimpleLazyObject
4344

4445
try:
4546
from django.urls import resolve
@@ -457,12 +458,47 @@ def _attempt_resolve_again(
457458

458459

459460
def _after_get_response(request: "WSGIRequest") -> None:
460-
integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
461-
if integration is None or integration.transaction_style != "url":
461+
client = sentry_sdk.get_client()
462+
integration = client.get_integration(DjangoIntegration)
463+
if integration is None:
462464
return
463465

464-
scope = sentry_sdk.get_current_scope()
465-
_attempt_resolve_again(request, scope, integration.transaction_style)
466+
if integration.transaction_style == "url":
467+
scope = sentry_sdk.get_current_scope()
468+
_attempt_resolve_again(request, scope, integration.transaction_style)
469+
470+
span_streaming = has_span_streaming_enabled(client.options)
471+
if span_streaming and should_send_default_pii():
472+
user = getattr(request, "user", None)
473+
474+
# Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation.
475+
# Exit early if the user has not been materialized yet.
476+
is_lazy = isinstance(user, SimpleLazyObject)
477+
if is_lazy and hasattr(request, "_cached_user"):
478+
user = request._cached_user
479+
elif is_lazy:
480+
return
481+
482+
if user is None or not is_authenticated(user):
483+
return
484+
485+
user_info = {}
486+
try:
487+
user_info["id"] = str(user.pk)
488+
except Exception:
489+
pass
490+
491+
try:
492+
user_info["email"] = user.email
493+
except Exception:
494+
pass
495+
496+
try:
497+
user_info["username"] = user.get_username()
498+
except Exception:
499+
pass
500+
501+
sentry_sdk.set_user(user_info)
466502

467503

468504
def _patch_get_response() -> None:

tests/integrations/django/test_basic.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,39 @@ def test_user_captured(
530530
}
531531

532532

533+
@pytest.mark.forked
534+
@pytest_mark_django_db_decorator()
535+
def test_materialized_user_captured(
536+
sentry_init,
537+
client,
538+
capture_events,
539+
capture_items,
540+
):
541+
sentry_init(
542+
integrations=[DjangoIntegration()],
543+
send_default_pii=True,
544+
traces_sample_rate=1.0,
545+
_experiments={"trace_lifecycle": "stream"},
546+
)
547+
548+
content, status, headers = unpack_werkzeug_response(client.get(reverse("mylogin")))
549+
assert content == b"ok"
550+
551+
items = capture_items("span")
552+
553+
content, status, headers = unpack_werkzeug_response(
554+
client.get(reverse("template_test"))
555+
)
556+
557+
sentry_sdk.flush()
558+
spans = [item.payload for item in items]
559+
(span,) = (span for span in spans if span["name"] == "/template-test")
560+
561+
assert span["attributes"][SPANDATA.USER_ID] == "1"
562+
assert span["attributes"][SPANDATA.USER_EMAIL] == "lennon@thebeatles.com"
563+
assert span["attributes"][SPANDATA.USER_NAME] == "john"
564+
565+
533566
@pytest.mark.forked
534567
@pytest_mark_django_db_decorator()
535568
@pytest.mark.parametrize("span_streaming", [True, False])

0 commit comments

Comments
 (0)