8000 feat: Sanic integration (#85) · etherscan-io/sentry-python@07a1e9c · GitHub
[go: up one dir, main page]

Skip to content

Commit 07a1e9c

Browse files
authored
feat: Sanic integration (getsentry#85)
* ref: Remove dead code in flask integration * feat: Sanic integration * fix: Make sanic tests valid python 2 s.t. pytest discover doesn't break
1 parent b17c7e0 commit 07a1e9c

File tree

5 files changed

+307
-25
lines changed

5 files changed

+307
-25
lines changed

sentry_sdk/integrations/flask.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ def _push_appctx(*args, **kwargs):
4646
# have (not the case for CLI for example)
4747
hub = Hub.current
4848
hub.push_scope()
49-
with hub.configure_scope() as scope:
50-
scope.add_event_processor(event_processor)
5149

5250

5351
def _pop_appctx(*args, **kwargs):
@@ -61,26 +59,6 @@ def _request_started(sender, **kwargs):
6159
scope.add_event_processor(_make_request_event_processor(app, weak_request))
6260

6361

64-
def event_processor(event, hint):
65-
request = getattr(_request_ctx_stack.top, "request", None)
66-
67-
if request:
68-
if "transaction" not in event:
69-
try:
70-
event["transaction"] = request.url_rule.endpoint
71-
except Exception:
72-
pass
73-
74-
with capture_internal_exceptions():
75-
FlaskRequestExtractor(request).extract_into_event(event)
76-
77-
if _should_send_default_pii():
78-
with capture_internal_exceptions():
79-
_add_user_to_event(event)
80-
81-
return event
82-
83-
8462
class FlaskRequestExtractor(RequestExtractor):
8563
def url(self):
8664
return "%s://%s%s" % (self.request.scheme, self.request.host, self.request.path)
@@ -134,6 +112,10 @@ def inner(event, hint):
134112
with capture_internal_exceptions():
135113
FlaskRequestExtractor(request).extract_into_event(event)
136114

115+
if _should_send_default_pii():
116+
with capture_internal_exceptions():
117+
_add_user_to_event(event)
118+
137119
return event
138120

139121
return inner

sentry_sdk/integrations/sanic.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import sys
2+
import weakref
3+
from inspect import isawaitable
4+
5+
from sentry_sdk import Hub, push_scope, configure_scope
6+
from sentry_sdk._compat import urlparse, reraise
7+
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
8+
from sentry_sdk.integrations import Integration
9+
from sentry_sdk.integrations._wsgi import RequestExtractor, _filter_headers
10+
11+
from sanic import Sanic
12+
from sanic.router import Router
13+
from sanic.handlers import ErrorHandler
14+
15+
16+
class SanicIntegration(Integration):
17+
identifier = "sanic"
18+
19+
def install(self):
20+
if sys.version_info < (3, 7):
21+
# Sanic is async. We better have contextvars or we're going to leak
22+
# state between requests.
23+
raise RuntimeError("The sanic integration for Sentry requires Python 3.7+")
24+
25+
old_handle_request = Sanic.handle_request
26+
27+
async def sentry_handle_request(self, request, *args, **kwargs):
28+
weak_request = weakref.ref(request)
29+
30+
with push_scope() as scope:
31+
scope.add_event_processor(_make_request_processor(weak_request))
32+
response = old_handle_request(self, request, *args, **kwargs)
33+
if isawaitable(response):
34+
response = await response
35+
return response
36+
37+
Sanic.handle_request = sentry_handle_request
38+
39+
old_router_get = Router.get
40+
41+
def sentry_router_get(self, request):
42+
rv = old_router_get(self, request)
43+
with capture_internal_exceptions():
44+
with configure_scope() as scope:
45+
scope.transaction = rv[0].__name__
46+
return rv
47+
48+
Router.get = sentry_router_get
49+
50+
old_error_handler_lookup = ErrorHandler.lookup
51+
52+
def sentry_error_handler_lookup(self, exception):
53+
_capture_exception(exception)
54+
old_error_handler = old_error_handler_lookup(self, exception)
55+
56+
if old_error_handler is None:
57+
return None
58+
59+
async def sentry_wrapped_error_handler(request, exception):
60+
try:
61+
response = old_error_handler(request, exception)
62+
if isawaitable(response):
63+
response = await response
64+
return response
65+
except Exception:
66+
exc_info = sys.exc_info()
67+
_capture_exception(exc_info)
68+
reraise(*exc_info)
69+
70+
return sentry_wrapped_error_handler
71+
72+
ErrorHandler.lookup = sentry_error_handler_lookup
73+
74+
75+
def _capture_exception(exception):
76+
with capture_internal_exceptions():
77+
hub = Hub.current
78+
event, hint = event_from_exception(
79+
exception,
80+
with_locals=hub.client.options["with_locals"],
81+
mechanism={"type": "sanic", "handled": False},
82+
)
83+
84+
hub.capture_event(event, hint=hint)
85+
86+
87+
def _make_request_processor(weak_request):
88+
def sanic_processor(event, hint):
89+
request = weak_request()
90+
if request is None:
91+
return event
92+
93+
with capture_internal_exceptions():
94+
extractor = SanicRequestExtractor(request)
95+
extractor.extract_into_event(event)
96+
97+
request_info = event["request"]
98+
if "query_string" not in request_info:
99+
request_info["query_string"] = extractor.urlparts.query
100+
101+
if "method" not in request_info:
102+
request_info["method"] = request.method
103+
104+
if "env" not in request_info:
105+
request_info["env"] = {"REMOTE_ADDR": request.remote_addr}
106+
107+
if "headers" not in request_info:
108+
request_info["headers"] = _filter_headers(dict(request.headers))
109+
110+
return event
111+
112+
return sanic_processor
113+
114+
115+
class SanicRequestExtractor(RequestExtractor):
116+
def __init__(self, request):
117+
RequestExtractor.__init__(self, request)
118+
self.urlparts = urlparse.urlsplit(self.request.url)
119+
120+
def content_length(self):
121+
if self.request.body is None:
122+
return 0
123+
return len(self.request.body)
124+
125+
def url(self):
126+
return "%s://%s%s" % (
127+
self.urlparts.scheme,
128+
self.urlparts.netloc,
129+
self.urlparts.path,
130+
)
131+
132+
def cookies(self):
133+
return dict(self.request.cookies)
134+
135+
def raw_data(self):
136+
return self.request.body
137+
138+
def form(self):
139+
return self.request.form
140+
141+
def is_json(self):
142+
raise NotImplementedError()
143+
144+
def json(self):
145+
return self.request.json
146+
147+
def files(self):
148+
return self.request.files
149+
150+
def size_of_file(self, file):
151+
return len(file.body or ())

tests/integrations/flask/test_flask.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,33 @@ def error_handler(err):
394394

395395
event, = events
396396
assert response.data.decode("utf-8") == "Sentry error: %s" % event["event_id"]
397+
398+
399+
def test_error_in_errorhandler(sentry_init, capture_events, app):
400+
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
401+
402+
app.debug = False
403+
app.testing = False
404+
405+
@app.route("/")
406+
def index():
407+
raise ValueError()
408+
409+
@app.errorhandler(500)
410+
def error_handler(err):
411+
1 / 0
412+
413+
events = capture_events()
414+
415+
client = app.test_client()
416+
417+
with pytest.raises(ZeroDivisionError):
418+
client.get("/")
419+
420+
event1, event2 = events
421+
422+
exception, = event1["exception"]["values"]
423+
assert exception["type"] == "ValueError"
424+
425+
exception, = event2["exception"]["values"]
426+
assert exception["type"] == "ZeroDivisionError"
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import pytest
2+
3+
sanic = pytest.importorskip("sanic")
4+
5+
from sentry_sdk import capture_message
6+
from sentry_sdk.integrations.sanic import SanicIntegration
7+
8+
from sanic import Sanic, response
9+
10+
11+
@pytest.fixture
12+
def app():
13+
app = Sanic(__name__)
14+
15+
@app.route("/message")
16+
def hi(request):
17+
capture_message("hi")
18+
return response.text("ok")
19+
20+
return app
21+
22+
23+
def test_request_data(sentry_init, app, capture_events):
24+
sentry_init(integrations=[SanicIntegration()])
25+
events = capture_events()
26+
27+
request, response = app.test_client.get("/message?foo=bar")
28+
assert response.status == 200
29+
30+
event, = events
31+
assert event["transaction"] == "hi"
32+
assert event["request"]["env"] == {"REMOTE_ADDR": ""}
33+
assert set(event["request"]["headers"]) == {
34+
"accept",
35+
"accept-encoding",
36+
"host",
37+
"user-agent",
38+
}
39+
assert event["request"]["query_string"] == "foo=bar"
40+
assert event["request"]["url"].endswith("/message")
41+
assert event["request"]["method"] == "GET"
42+
43+
# Assert that state is not leaked
44+
events.clear()
45+
capture_message("foo")
46+
event, = events
47+
48+
assert "request" not in event
49+
assert "transaction" not in event
50+
51+
52+
def test_errors(sentry_init, app, capture_events):
53+
sentry_init(integrations=[SanicIntegration()])
54+
events = capture_events()
55+
56+
@app.route("/error")
57+
def myerror(request):
58+
raise ValueError("oh no")
59+
60+
request, response = app.test_client.get("/error")
61+
assert response.status == 500
62+
63+
event, = events
64+
assert event["transaction"] == "myerror"
65+
exception, = event["exception"]["values"]
66+
67+
assert exception["type"] == "ValueError"
68+
assert exception["value"] == "oh no"
69+
assert any(
70+
frame["filename"] == "test_sanic.py"
71+
for frame in exception["stacktrace"]["frames"]
72+
)
73+
74+
75+
def test_error_in_errorhandler(sentry_init, app, capture_events):
76+
sentry_init(integrations=[SanicIntegration()])
77+
events = capture_events()
78+
79+
@app.route("/error")
80+
def myerror(request):
81+
raise ValueError("oh no")
82+
83+
@app.exception(ValueError)
84+
def myhandler(request, exception):
85+
1 / 0
86+
87+
request, response = app.test_client.get("/error")
88+
assert response.status == 500
89+
90+
event1, event2 = events
91+
92+
exception, = event1["exception"]["values"]
93+
assert exception["type"] == "ValueError"
94+
assert any(
95+
frame["filename"] == "test_sanic.py"
96+
for frame in exception["stacktrace"]["frames"]
97+
)
98+
99+
exception, = event2["exception"]["values"]
100+
assert exception["type"] == "ZeroDivisionError"
101+
assert any(
102+
frame["filename"] == "test_sanic.py"
103+
for frame in exception["stacktrace"]["frames"]
104+
)

tox.ini

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,21 @@ envlist =
1919

2020
{pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-flask-{1.0,0.11,0.12,dev}
2121

22-
{py2.7,py3.7}-requests
22+
{py3.7,py3.8}-sanic-0.8
23+
2324
{pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-celery-4
24-
py3.7-aws_lambda
2525
{pypy,py2.7}-celery-3
2626

27+
{py2.7,py3.7}-requests
28+
29+
py3.7-aws_lambda
30+
31+
2732

2833
[testenv]
2934
deps =
3035
-r test-requirements.txt
36+
3137
django-{1.6,1.7,1.8}: pytest-django<3.0
3238
django-{1.9,1.10,1.11,2.0,2.1,dev}: pytest-django>=3.0
3339
django-1.6: Django>=1.6,<1.7
@@ -39,15 +45,23 @@ deps =
3945
django-2.0: Django>=2.0,<2.1
4046
django-2.1: Django>=2.0,<2.1
4147
django-dev: git+https://github.com/django/django.git#egg=Django
48+
49+
flask: flask-login
4250
flask-0.11: Flask>=0.11,<0.12
4351
flask-0.12: Flask>=0.12,<0.13
4452
flask-1.0: Flask>=0.10,<0.11
4553
flask-dev: git+https://github.com/pallets/flask.git#egg=flask
54+
55+
sanic-0.8: sanic>=0.8,<0.9
56+
sanic: aiohttp
57+
4658
celery-3: Celery>=3.1,<4.0
4759
celery-4: Celery>=4.0,<5.0
60+
4861
requests: requests>=2.0
62+
4963
aws_lambda: boto3
50-
flask: flask-login
64+
5165
linters: black
5266
linters: flake8
5367
setenv =
@@ -58,6 +72,7 @@ setenv =
5872
celery: TESTPATH=tests/integrations/celery
5973
requests: TESTPATH=tests/integrations/requests
6074
aws_lambda: TESTPATH=tests/integrations/aws_lambda
75+
sanic: TESTPATH=tests/integrations/sanic
6176
passenv =
6277
AWS_ACCESS_KEY_ID
6378
AWS_SECRET_ACCESS_KEY

0 commit comments

Comments
 (0)
0