Skip to content

Commit b4a3cb7

Browse files
committed
feat: user data in django
1 parent 673ebcf commit b4a3cb7

8 files changed

Lines changed: 90 additions & 4 deletions

File tree

sentry_sdk/hub.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def _internal_exceptions():
1515
try:
1616
yield
1717
except Exception:
18-
Hub.current.capture_exception()
18+
Hub.current.capture_internal_exception()
1919

2020

2121
class HubMeta(type):

sentry_sdk/integrations/_wsgi.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,17 @@ def files(self):
145145

146146
def size_of_file(self, file):
147147
raise NotImplementedError()
148+
149+
150+
def get_client_ip(environ):
151+
"""
152+
Naively yank the first IP address in an X-Forwarded-For header
153+
and assume this is correct.
154+
155+
Note: Don't use this in security sensitive situations since this
156+
value may be forged from a client.
157+
"""
158+
try:
159+
return environ['HTTP_X_FORWARDED_FOR'].split(',')[0].strip()
160+
except (KeyError, IndexError):
161+
return environ.get('REMOTE_ADDR')

sentry_sdk/integrations/django.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import absolute_import
22

3+
from django import VERSION as DJANGO_VERSION
34
from django.core import signals
45

56
try:
@@ -9,10 +10,18 @@
910

1011
from sentry_sdk import get_current_hub, configure_scope, capture_exception
1112
from sentry_sdk.hub import _internal_exceptions
12-
from ._wsgi import RequestExtractor
13+
from ._wsgi import RequestExtractor, get_client_ip
1314
from . import Integration
1415

1516

17+
if DJANGO_VERSION < (1, 10):
18+
def is_authenticated(request_user):
19+
return request_user.is_authenticated()
20+
else:
21+
def is_authenticated(request_user):
22+
return request_user.is_authenticated
23+
24+
1625
class DjangoIntegration(Integration):
1726
identifier = 'django'
1827

@@ -46,6 +55,10 @@ def processor(event):
4655
with _internal_exceptions():
4756
DjangoRequestExtractor(request).extract_into_event(event)
4857

58+
if 'user' not in event:
59+
with _internal_exceptions():
60+
_set_user_info(request, event)
61+
4962
# TODO: user info
5063

5164
return processor
@@ -83,3 +96,24 @@ def files(self):
8396

8497
def size_of_file(self, file):
8598
return file.size
99+
100+
101+
def _set_user_info(request, event):
102+
event['user'] = user_info = {
103+
'ip_address': get_client_ip(request.META),
104+
}
105+
106+
user = getattr(request, "user", None)
107+
108+
if user is None or not is_authenticated(user):
109+
return
110+
111+
try:
112+
user_info['email'] = user.email
113+
except Exception:
114+
pass
115+
116+
try:
117+
user_info['username'] = user.get_username()
118+
except Exception:
119+
pass

sentry_sdk/stripping.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ def __init__(self, value, metadata):
1212
def flatten_metadata(obj):
1313
def inner(obj):
1414
if isinstance(obj, Mapping):
15-
assert "" not in obj, "Merging metadata not supported"
1615
rv = {}
1716
meta = {}
1817
for k, v in obj.items():
18+
# if we actually have "" keys in our data, throw them away. It's
19+
# unclear how we would tell them apart from metadata
20+
if k == "":
21+
continue
22+
1923
rv[k], meta[k] = inner(v)
2024
if meta[k] is None:
2125
del meta[k]

tests/integrations/django/myapp/settings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ def process_response(self, request, response):
6666
return response
6767

6868

69-
MIDDLEWARE_CLASSES = ["tests.integrations.django.myapp.settings.TestMiddleware"]
69+
MIDDLEWARE_CLASSES = [
70+
'django.contrib.sessions.middleware.SessionMiddleware',
71+
'django.contrib.auth.middleware.AuthenticationMiddleware',
72+
"tests.integrations.django.myapp.settings.TestMiddleware"
73+
]
7074

7175
if MiddlewareMixin is not object:
7276
MIDDLEWARE = MIDDLEWARE_CLASSES

tests/integrations/django/myapp/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@
2727
path("view-exc", views.view_exc, name="view_exc"),
2828
path("middleware-exc", views.self_check, name="middleware_exc"),
2929
path("message", views.message, name="message"),
30+
path("mylogin", views.mylogin, name="mylogin"),
3031
]

tests/integrations/django/myapp/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from django.contrib.auth import login
2+
from django.contrib.auth.models import User
13
from django.http import HttpResponse
24

35
import sentry_sdk
@@ -16,3 +18,10 @@ def view_exc(request):
1618
def message(request):
1719
sentry_sdk.capture_message("hi")
1820
return HttpResponse("ok")
21+
22+
23+
def mylogin(request):
24+
user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')
25+
user.backend = 'django.contrib.auth.backends.ModelBackend'
26+
login(request, user)
27+
return HttpResponse("ok")

tests/integrations/django/test_basic.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,23 @@ def test_request_captured(client, capture_events):
5858
"query_string": "",
5959
"url": "http://testserver/message",
6060
}
61+
62+
63+
@pytest.mark.django_db
64+
def test_user_captured(client, capture_events):
65+
events = capture_events()
66+
response = client.get(reverse("mylogin"))
67+
assert response.content == b'ok'
68+
69+
assert not events
70+
71+
response = client.get(reverse("message"))
72+
assert response.content == b'ok'
73+
74+
event, = events
75+
76+
assert event['user'] == {
77+
'email': 'lennon@thebeatles.com',
78+
'ip_address': "127.0.0.1",
79+
"username": "john"
80+
}

0 commit comments

Comments
 (0)