Skip to content
18 changes: 18 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,12 +978,30 @@ class SPANDATA:
Example: "MainThread"
"""

USER_EMAIL = "user.email"
Comment thread
alexander-alderman-webb marked this conversation as resolved.
"""
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"
Comment thread
alexander-alderman-webb marked this conversation as resolved.
"""
The IP address of the user that triggered the request.
Example: "10.1.2.80"
"""

Comment thread
alexander-alderman-webb marked this conversation as resolved.
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.
Expand Down
44 changes: 40 additions & 4 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 43 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

@sentry/warden / warden: code-review

[38W-3LT] No test covering the early return for un-materialized `SimpleLazyObject` user (additional location)

The fix for issue #5274 (returning early when `is_lazy=True` and `_cached_user` is absent) has no test, so it could silently regress and re-introduce the `SynchronousOnlyOperation` crash in async views.

try:
from django.urls import resolve
Expand Down Expand Up @@ -457,12 +458,47 @@


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)

Check warning on line 476 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

@sentry/warden / warden: code-review

No test covering the early return for un-materialized `SimpleLazyObject` user

The fix for issue #5274 (returning early when `is_lazy=True` and `_cached_user` is absent) has no test, so it could silently regress and re-introduce the `SynchronousOnlyOperation` crash in async views.
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
Comment thread
cursor[bot] marked this conversation as resolved.

user_info = {}
try:
user_info["id"] = str(user.pk)
except Exception:
pass

try:
Comment thread
alexander-alderman-webb marked this conversation as resolved.
user_info["email"] = user.email
except Exception:
pass

try:
user_info["username"] = user.get_username()
except Exception:
pass

Check warning on line 499 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

@sentry/warden / warden: code-review

Span-streaming path uses `set_user()` which overwrites custom user fields, diverging from static-mode `setdefault` behavior

In `_after_get_response`, the span-streaming branch calls `sentry_sdk.set_user(user_info)`, which routes to `Scope.set_user` and does a full replacement (`self._user = value`). Because `_after_get_response` runs after `old_get_response()` (the view/middleware), any user fields the application set during the request (e.g. `segment`, custom attributes, or an `ip_address` set elsewhere) are discarded. This diverges from the static-mode event processor `_set_user_info()`, which uses `event.setdefault("user", {})` and `user_info.setdefault(...)` so application-provided fields win. Consider merging into the existing scope user (or matching the `setdefault` pattern) so span-streaming and static modes behave consistently.
Comment thread
alexander-alderman-webb marked this conversation as resolved.

sentry_sdk.set_user(user_info)


def _patch_get_response() -> None:
Expand Down
33 changes: 33 additions & 0 deletions tests/integrations/django/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,39 @@ def test_user_captured(
}


@pytest.mark.forked
Comment thread
alexander-alderman-webb marked this conversation as resolved.
@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])
Expand Down
Loading