Skip to content
Merged
78 changes: 46 additions & 32 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
capture_internal_exceptions,
ensure_integration_enabled,
event_from_exception,
has_data_collection_enabled,
logger,
transaction_from_function,
walk_exception_chain,
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
8 changes: 7 additions & 1 deletion sentry_sdk/integrations/django/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
ensure_integration_enabled,
has_data_collection_enabled,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -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)

Expand Down
23 changes: 18 additions & 5 deletions sentry_sdk/integrations/pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
capture_internal_exceptions,
ensure_integration_enabled,
event_from_exception,
has_data_collection_enabled,
reraise,
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
18 changes: 13 additions & 5 deletions sentry_sdk/integrations/sanic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -401,19 +405,23 @@ 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

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


Expand Down
35 changes: 28 additions & 7 deletions sentry_sdk/integrations/tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,16 @@ 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,
Expand Down Expand Up @@ -218,19 +226,23 @@ 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

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
Expand Down Expand Up @@ -282,6 +294,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)
Expand All @@ -294,7 +307,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(
Expand All @@ -310,7 +322,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:
Expand Down
32 changes: 32 additions & 0 deletions tests/integrations/django/asgi/test_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +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 pytest_mark_django_db_decorator
from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES

try:
from django.urls import reverse
Expand Down Expand Up @@ -1096,3 +1098,33 @@ async def test_async_middleware_process_exception_is_awaited(

assert response["status"] == 200
assert response["body"] == b"handled by async process_exception"


@pytest.mark.forked
@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", 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
):
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", {})
5 changes: 5 additions & 0 deletions tests/integrations/django/myapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions tests/integrations/django/myapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
Loading
Loading