Skip to content

Commit 2b574c0

Browse files
committed
ref: Share code from Flask/Django with WSGI middleware
Fixes #22
1 parent a8c1bbd commit 2b574c0

File tree

8 files changed

+169
-81
lines changed

8 files changed

+169
-81
lines changed

sentry_sdk/_compat.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ def implements_str(cls):
1616
cls.__str__ = lambda x: unicode(x).encode("utf-8") # noqa
1717
return cls
1818

19+
exec("def reraise(tp, value, tb=None):\n raise tp, value, tb")
20+
21+
def implements_iterator(cls):
22+
cls.next = cls.__next__
23+
del cls.__next__
24+
return cls
25+
1926

2027
else:
2128
import urllib.parse as urlparse # noqa
@@ -24,9 +31,19 @@ def implements_str(cls):
2431
text_type = str
2532
number_types = (int, float)
2633

34+
def _identity(x):
35+
return x
36+
37+
implements_iterator = _identity
38+
2739
def implements_str(x):
2840
return x
2941

42+
def reraise(tp, value, tb=None):
43+
if value.__traceback__ is not tb:
44+
raise value.with_traceback(tb)
45+
raise value
46+
3047

3148
def with_metaclass(meta, *bases):
3249
class metaclass(type):

sentry_sdk/integrations/_wsgi.py

Lines changed: 89 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import json
2+
import sys
23

3-
from sentry_sdk.hub import _should_send_default_pii
4+
import sentry_sdk
5+
from sentry_sdk.hub import _internal_exceptions, _should_send_default_pii
46
from sentry_sdk.event import AnnotatedValue
7+
from sentry_sdk._compat import reraise, implements_iterator
58

69

710
def get_environ(environ):
@@ -38,28 +41,11 @@ def __init__(self, request):
3841
self.request = request
3942

4043
def extract_into_event(self, event, client_options):
41-
if "request" in event:
42-
return
43-
44-
# if the code below fails halfway through we at least have some data
45-
event["request"] = request_info = {}
46-
44+
request_info = event.setdefault("request", {})
4745
request_info["url"] = self.url
48-
request_info["query_string"] = self.query_string
49-
request_info["method"] = self.method
50-
51-
request_info["env"] = dict(get_environ(self.env))
5246

5347
if _should_send_default_pii():
54-
request_info["headers"] = dict(self.headers)
5548
request_info["cookies"] = dict(self.cookies)
56-
else:
57-
request_info["headers"] = {
58-
k: v
59-
for k, v in dict(self.headers).items()
60-
if k.lower().replace("_", "-")
61-
not in ("set-cookie", "cookie", "authentication")
62-
}
6349

6450
bodies = client_options.get("request_bodies")
6551
if (
@@ -108,22 +94,6 @@ def content_length(self):
10894
def url(self):
10995
raise NotImplementedError()
11096

111-
@property
112-
def query_string(self):
113-
return self.env.get("QUERY_STRING")
114-
115-
@property
116-
def method(self):
117-
return self.env.get("REQUEST_METHOD")
118-
119-
@property
120-
def headers(self):
121-
return get_headers(self.env)
122-
123-
@property
124-
def env(self):
125-
raise NotImplementedError()
126-
12797
@property
12898
def cookies(self):
12999
raise NotImplementedError()
@@ -136,10 +106,6 @@ def raw_data(self):
136106
def form(self):
137107
raise NotImplementedError()
138108

139-
@property
140-
def form_is_multipart(self):
141-
return self.env.get("CONTENT_TYPE").startswith("multipart/form-data")
142-
143109
@property
144110
def is_json(self):
145111
mt = (self.env.get("CONTENT_TYPE") or "").split(";", 1)[0]
@@ -177,3 +143,87 @@ def get_client_ip(environ):
177143
return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip()
178144
except (KeyError, IndexError):
179145
return environ.get("REMOTE_ADDR")
146+
147+
148+
def run_wsgi_app(app, environ, start_response):
149+
hub = sentry_sdk.get_current_hub()
150+
hub.push_scope()
151+
with _internal_exceptions():
152+
client_options = sentry_sdk.get_current_hub().client.options
153+
sentry_sdk.get_current_hub().add_event_processor(
154+
lambda: _make_wsgi_event_processor(environ, client_options)
155+
)
156+
157+
try:
158+
rv = app(environ, start_response)
159+
except Exception:
160+
einfo = sys.exc_info()
161+
sentry_sdk.capture_exception(einfo)
162+
hub.pop_scope_unsafe()
163+
reraise(*einfo)
164+
165+
return _ScopePoppingResponse(hub, rv)
166+
167+
168+
@implements_iterator
169+
class _ScopePoppingResponse(object):
170+
__slots__ = ("_response", "_hub")
171+
172+
def __init__(self, hub, response):
173+
self._hub = hub
174+
self._response = response
175+
176+
def __iter__(self):
177+
try:
178+
self._response = iter(self._response)
179+
except Exception:
180+
einfo = sys.exc_info()
181+
sentry_sdk.capture_exception(einfo)
182+
reraise(*einfo)
183+
return self
184+
185+
def __next__(self):
186+
try:
187+
return next(self._response)
188+
except Exception:
189+
einfo = sys.exc_info()
190+
sentry_sdk.capture_exception(einfo)
191+
reraise(*einfo)
192+
193+
def close(self):
194+
self._hub.pop_scope_unsafe()
195+
if hasattr(self._response, "close"):
196+
try:
197+
self._response.close()
198+
except Exception:
199+
einfo = sys.exc_info()
200+
sentry_sdk.capture_exception(einfo)
201+
reraise(*einfo)
202+
203+
204+
def _make_wsgi_event_processor(environ, client_options):
205+
def event_processor(event):
206+
with _internal_exceptions():
207+
# if the code below fails halfway through we at least have some data
208+
request_info = event.setdefault("request", {})
209+
210+
if "query_string" not in request_info:
211+
request_info["query_string"] = environ.get("QUERY_STRING")
212+
213+
if "method" not in request_info:
214+
request_info["method"] = environ.get("REQUEST_METHOD")
215+
216+
if "env" not in request_info:
217+
request_info["env"] = dict(get_environ(environ))
218+
219+
if "headers" not in request_info:
220+
request_info["headers"] = dict(get_headers(environ))
221+
if not _should_send_default_pii():
222+
request_info["headers"] = {
223+
k: v
224+
for k, v in request_info["headers"].items()
225+
if k.lower().replace("_", "-")
226+
not in ("set-cookie", "cookie", "authentication")
227+
}
228+
229+
return event_processor

sentry_sdk/integrations/django.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from sentry_sdk import get_current_hub, capture_exception
1212
from sentry_sdk.hub import _internal_exceptions, _should_send_default_pii
13-
from ._wsgi import RequestExtractor, get_client_ip
13+
from ._wsgi import RequestExtractor, get_client_ip, run_wsgi_app
1414
from . import Integration
1515

1616

@@ -33,19 +33,31 @@ def __init__(self):
3333
pass
3434

3535
def install(self, client):
36-
from django.core.handlers.base import BaseHandler
36+
# Patch in our custom middleware.
3737

38-
make_event_processor = self._make_event_processor
38+
from django.core.handlers.wsgi import WSGIHandler
39+
40+
old_app = WSGIHandler.__call__
41+
42+
def sentry_patched_wsgi_handler(self, environ, start_response):
43+
return run_wsgi_app(
44+
lambda *a, **kw: old_app(self, *a, **kw), environ, start_response
45+
)
46+
47+
WSGIHandler.__call__ = sentry_patched_wsgi_handler
48+
49+
# patch get_response, because at that point we have the Django request
50+
# object
51+
52+
from django.core.handlers.base import BaseHandler
3953

4054
old_get_response = BaseHandler.get_response
55+
make_event_processor = self._make_event_processor
4156

4257
def sentry_patched_get_response(self, request):
43-
with get_current_hub().push_scope():
44-
get_current_hub().add_event_processor(
45-
lambda: make_event_processor(request)
46-
)
58+
get_current_hub().add_event_processor(lambda: make_event_processor(request))
4759

48-
return old_get_response(self, request)
60+
return old_get_response(self, request)
4961

5062
BaseHandler.get_response = sentry_patched_get_response
5163

sentry_sdk/integrations/flask.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
from sentry_sdk import capture_exception, get_current_hub
44
from sentry_sdk.hub import _internal_exceptions, _should_send_default_pii
5-
from ._wsgi import RequestExtractor
5+
from ._wsgi import RequestExtractor, run_wsgi_app
66
from . import Integration
77

88
try:
99
from flask_login import current_user
1010
except ImportError:
1111
current_user = None
1212

13-
from flask import _request_ctx_stack, _app_ctx_stack
13+
from flask import Flask, _request_ctx_stack, _app_ctx_stack
1414
from flask.signals import (
1515
appcontext_pushed,
1616
appcontext_tearing_down,
@@ -29,8 +29,19 @@ def install(self, client):
2929
appcontext_tearing_down.connect(_pop_appctx)
3030
got_request_exception.connect(_capture_exception)
3131

32+
old_app = Flask.wsgi_app
33+
34+
def sentry_patched_wsgi_app(self, environ, start_response):
35+
return run_wsgi_app(
36+
lambda *a, **kw: old_app(self, *a, **kw), environ, start_response
37+
)
38+
39+
Flask.wsgi_app = sentry_patched_wsgi_app
40+
3241

3342
def _push_appctx(*args, **kwargs):
43+
# always want to push scope regardless of whether WSGI app might already
44+
# have (not the case for CLI for example)
3445
get_current_hub().push_scope()
3546
get_current_hub().add_event_processor(_make_event_processor)
3647

tests/integrations/conftest.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
@pytest.fixture
77
def capture_exceptions(monkeypatch):
88
def inner():
9-
errors = []
9+
errors = set()
1010
old_capture_exception = sentry_sdk.Hub.current.capture_exception
1111

1212
def capture_exception(error=None):
13-
errors.append(error or sys.exc_info()[1])
14-
return old_capture_exception(error)
13+
given_error = error
14+
error = error or sys.exc_info()[1]
15+
if isinstance(error, tuple):
16+
error = error[1]
17+
errors.add(error)
18+
return old_capture_exception(given_error)
1519

1620
monkeypatch.setattr(
1721
sentry_sdk.Hub.current, "capture_exception", capture_exception

tests/integrations/django/myapp/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
# SECURITY WARNING: don't run with debug turned on in production!
3838
DEBUG = True
3939

40-
ALLOWED_HOSTS = []
40+
ALLOWED_HOSTS = ["localhost"]
4141

4242

4343
# Application definition

0 commit comments

Comments
 (0)