Skip to content
Merged
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
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
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"]:
Comment thread
cursor[bot] marked this conversation as resolved.
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