Skip to content

Commit 02e5442

Browse files
authored
feat: PoC for data_info field (#11)
* feat: PoC for data_info field * chore: refactor request extraction for wsgi * feat: implement django request extraction * chore: fix formatting * chore: fix flask tests * chore: fix django tests * chore: fix encoding issues * chore: fix formatting
1 parent 3ce6eb8 commit 02e5442

File tree

7 files changed

+360
-17
lines changed

7 files changed

+360
-17
lines changed

sentry_sdk/integrations/_wsgi.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,144 @@
1+
import json
2+
import base64
3+
4+
from sentry_sdk.stripping import AnnotatedValue
5+
6+
17
def get_environ(environ):
28
"""
39
Returns our whitelisted environment variables.
410
"""
511
for key in ("REMOTE_ADDR", "SERVER_NAME", "SERVER_PORT"):
612
if key in environ:
713
yield key, environ[key]
14+
15+
16+
# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
17+
#
18+
# We need this function because Django does not give us a "pure" http header
19+
# dict. So we might as well use it for all WSGI integrations.
20+
def get_headers(environ):
21+
"""
22+
Returns only proper HTTP headers.
23+
24+
"""
25+
for key, value in environ.items():
26+
key = str(key)
27+
if key.startswith("HTTP_") and key not in (
28+
"HTTP_CONTENT_TYPE",
29+
"HTTP_CONTENT_LENGTH",
30+
):
31+
yield key[5:].replace("_", "-").title(), value
32+
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
33+
yield key.replace("_", "-").title(), value
34+
35+
36+
class RequestExtractor(object):
37+
def __init__(self, request):
38+
self.request = request
39+
40+
def extract_into_scope(self, scope):
41+
# if the code below fails halfway through we at least have some data
42+
scope.request = request_info = {}
43+
44+
request_info["url"] = self.url
45+
request_info["query_string"] = self.query_string
46+
request_info["method"] = self.method
47+
request_info["headers"] = dict(self.headers)
48+
request_info["env"] = dict(get_environ(self.env))
49+
request_info["cookies"] = dict(self.cookies)
50+
51+
if self.form or self.files:
52+
data = dict(self.form.items())
53+
for k, v in self.files.items():
54+
data[k] = AnnotatedValue(
55+
"",
56+
{"len": self.size_of_file(v), "rem": [["!filecontent", "x", 0, 0]]},
57+
)
58+
59+
if self.files or self.form_is_multipart:
60+
ct = "multipart"
61+
else:
62+
ct = "urlencoded"
63+
repr = "structured"
64+
elif self.json is not None:
65+
data = self.json
66+
ct = "json"
67+
repr = "structured"
68+
elif self.raw_data:
69+
data = self.raw_data
70+
71+
try:
72+
if isinstance(data, bytes):
73+
data = data.decode("utf-8")
74+
ct = "plain"
75+
repr = "other"
76+
except UnicodeDecodeError:
77+
ct = "bytes"
78+
repr = "base64"
79+
data = base64.b64encode(data).decode("ascii")
80+
else:
81+
return
82+
83+
request_info["data"] = data
84+
request_info["data_info"] = {"ct": ct, "repr": repr}
85+
86+
@property
87+
def url(self):
88+
raise NotImplementedError()
89+
90+
@property
91+
def query_string(self):
92+
return self.env.get("QUERY_STRING")
93+
94+
@property
95+
def method(self):
96+
return self.env.get("REQUEST_METHOD")
97+
98+
@property
99+
def headers(self):
100+
return get_headers(self.env)
101+
102+
@property
103+
def env(self):
104+
raise NotImplementedError()
105+
106+
@property
107+
def cookies(self):
108+
raise NotImplementedError()
109+
110+
@property
111+
def raw_data(self):
112+
raise NotImplementedError()
113+
114+
@property
115+
def form(self):
116+
raise NotImplementedError()
117+
118+
@property
119+
def form_is_multipart(self):
120+
return self.env.get("CONTENT_TYPE").startswith("multipart/form-data")
121+
122+
@property
123+
def is_json(self):
124+
mt = (self.env.get("CONTENT_TYPE") or "").split(";", 1)[0]
125+
return (
126+
mt == "application/json"
127+
or (mt.startswith("application/"))
128+
and mt.endswith("+json")
129+
)
130+
131+
@property
132+
def json(self):
133+
try:
134+
if self.is_json:
135+
return json.loads(self.raw_data.decode("utf-8"))
136+
except ValueError:
137+
pass
138+
139+
@property
140+
def files(self):
141+
raise NotImplementedError()
142+
143+
def size_of_file(self, file):
144+
raise NotImplementedError()

sentry_sdk/integrations/django/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.core.urlresolvers import resolve
1212

1313
from sentry_sdk import get_current_hub, configure_scope, capture_exception
14+
from .._wsgi import RequestExtractor
1415

1516

1617
try:
@@ -35,10 +36,46 @@ def process_request(self, request):
3536

3637
with configure_scope() as scope:
3738
scope.transaction = _get_transaction_from_request(request)
39+
try:
40+
DjangoRequestExtractor(request).extract_into_scope(scope)
41+
except Exception:
42+
get_current_hub().capture_internal_exception()
43+
44+
# TODO: user info
45+
3846
except Exception:
3947
get_current_hub().capture_internal_exception()
4048

4149

50+
class DjangoRequestExtractor(RequestExtractor):
51+
@property
52+
def url(self):
53+
return self.request.build_absolute_uri(self.request.path)
54+
55+
@property
56+
def env(self):
57+
return self.request.META
58+
59+
@property
60+
def cookies(self):
61+
return self.request.COOKIES
62+
63+
@property
64+
def raw_data(self):
65+
return self.request.body
66+
67+
@property
68+
def form(self):
69+
return self.request.POST
70+
71+
@property
72+
def files(self):
73+
return self.request.FILES
74+
75+
def size_of_file(self, file):
76+
return file.size
77+
78+
4279
def _request_finished(*args, **kwargs):
4380
get_current_hub().pop_scope_unsafe()
4481

sentry_sdk/integrations/flask.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import absolute_import
22

33
from sentry_sdk import capture_exception, configure_scope, get_current_hub
4-
from ._wsgi import get_environ
4+
from ._wsgi import RequestExtractor
55

66
try:
77
from flask_login import current_user
@@ -55,30 +55,48 @@ def _before_request(*args, **kwargs):
5555
scope.transaction = request.url_rule.endpoint
5656

5757
try:
58-
scope.request = _get_request_info()
58+
FlaskRequestExtractor(request).extract_into_scope(scope)
5959
except Exception:
6060
get_current_hub().capture_internal_exception()
6161

6262
try:
63-
scope.user = _get_user_info()
63+
_set_user_info(scope)
6464
except Exception:
6565
get_current_hub().capture_internal_exception()
6666
except Exception:
6767
get_current_hub().capture_internal_exception()
6868

6969

70-
def _get_request_info():
71-
return {
72-
"url": "%s://%s%s" % (request.scheme, request.host, request.path),
73-
"query_string": request.query_string,
74-
"method": request.method,
75-
"data": request.get_data(cache=True, as_text=True, parse_form_data=True),
76-
"headers": dict(request.headers),
77-
"env": get_environ(request.environ),
78-
}
70+
class FlaskRequestExtractor(RequestExtractor):
71+
@property
72+
def url(self):
73+
return "%s://%s%s" % (self.request.scheme, self.request.host, self.request.path)
7974

75+
@property
76+
def env(self):
77+
return self.request.environ
8078

81-
def _get_user_info():
79+
@property
80+
def cookies(self):
81+
return self.request.cookies
82+
83+
@property
84+
def raw_data(self):
85+
return self.request.data
86+
87+
@property
88+
def form(self):
89+
return self.request.form
90+
91+
@property
92+
def files(self):
93+
return request.files
94+
95+
def size_of_file(self, file):
96+
return file.content_length
97+
98+
99+
def _set_user_info(scope):
82100
try:
83101
ip_address = request.access_route[0]
84102
except IndexError:
@@ -96,4 +114,4 @@ def _get_user_info():
96114
# - no user is logged in
97115
pass
98116

99-
return user_info
117+
scope.user = user_info

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("get-dsn", views.get_dsn, name="get_dsn"),
30+
path("message", views.message, name="message"),
3031
]

tests/integrations/django/myapp/views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ def get_dsn(request):
1919
return HttpResponse(
2020
template.render(Context()), content_type="application/xhtml+xml"
2121
)
22+
23+
24+
def message(request):
25+
sentry_sdk.capture_message("hi")
26+
return HttpResponse("ok")

tests/integrations/django/test_basic.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ def test_middleware_exceptions(client, capture_exceptions):
5353
assert capture_exceptions == [exc.value]
5454

5555

56-
def test_get_dsn(request, client):
56+
def test_get_dsn(client):
5757
response = client.get(reverse("get_dsn"))
5858
assert response.content == b"LOL!"
59+
60+
61+
def test_request_captured(client, capture_events):
62+
response = client.get(reverse("message"))
63+
assert response.content == b"ok"
64+
65+
event, = capture_events
66+
assert event["request"] == {
67+
"cookies": {},
68+
"env": {
69+
"REMOTE_ADDR": "127.0.0.1",
70+
"SERVER_NAME": "testserver",
71+
"SERVER_PORT": "80",
72+
},
73+
"headers": {"Cookie": ""},
74+
"method": "GET",
75+
"query_string": "",
76+
"url": "http://testserver/message",
77+
}

0 commit comments

Comments
 (0)