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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
40 changes: 38 additions & 2 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

try:
from django.urls import resolve
Expand Down Expand Up @@ -458,11 +459,46 @@

def _after_get_response(request: "WSGIRequest") -> None:
integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
if integration is None or integration.transaction_style != "url":
if integration is None:
return

scope = sentry_sdk.get_current_scope()
_attempt_resolve_again(request, scope, integration.transaction_style)

if integration.transaction_style == "url":
_attempt_resolve_again(request, scope, integration.transaction_style)

span_streaming = has_span_streaming_enabled(sentry_sdk.get_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
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

scope.set_user() in _after_get_response silently overwrites user data set during the view

Because `scope.set_user(user_info)` replaces `scope._user` entirely, any user fields set by application code or middleware earlier in the request (e.g. `sentry_sdk.set_user({"ip_address": ...})`) will be lost. Consider merging with existing scope user data or only calling `set_user` when `scope._user` is not already populated.
Comment thread
alexander-alderman-webb marked this conversation as resolved.

scope.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