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
26 changes: 4 additions & 22 deletions sentry_sdk/integrations/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ def _push_appctx(*args, **kwargs):
# have (not the case for CLI for example)
hub = Hub.current
hub.push_scope()
with hub.configure_scope() as scope:
scope.add_event_processor(event_processor)


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


def event_processor(event, hint):
request = getattr(_request_ctx_stack.top, "request", None)

if request:
if "transaction" not in event:
try:
event["transaction"] = request.url_rule.endpoint
except Exception:
pass

with capture_internal_exceptions():
FlaskRequestExtractor(request).extract_into_event(event)

if _should_send_default_pii():
with capture_internal_exceptions():
_add_user_to_event(event)

return event


class FlaskRequestExtractor(RequestExtractor):
def url(self):
return "%s://%s%s" % (self.request.scheme, self.request.host, self.request.path)
Expand Down Expand Up @@ -134,6 +112,10 @@ def inner(event, hint):
with capture_internal_exceptions():
FlaskRequestExtractor(request).extract_into_event(event)

if _should_send_default_pii():
with capture_internal_exceptions():
_add_user_to_event(event)

return event

return inner
Expand Down
151 changes: 151 additions & 0 deletions sentry_sdk/integrations/sanic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import sys
import weakref
from inspect import isawaitable

from sentry_sdk import Hub, push_scope, configure_scope
from sentry_sdk._compat import urlparse, reraise
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations._wsgi import RequestExtractor, _filter_headers

from sanic import Sanic
from sanic.router import Router
from sanic.handlers import ErrorHandler


class SanicIntegration(Integration):
identifier = "sanic"

def install(self):
if sys.version_info < (3, 7):
# Sanic is async. We better have contextvars or we're going to leak
# state between requests.
raise RuntimeError("The sanic integration for Sentry requires Python 3.7+")

old_handle_request = Sanic.handle_request

async def sentry_handle_request(self, request, *args, **kwargs):
weak_request = weakref.ref(request)

with push_scope() as scope:
scope.add_event_processor(_make_request_processor(weak_request))
response = old_handle_request(self, request, *args, **kwargs)
if isawaitable(response):
response = await response
return response

Sanic.handle_request = sentry_handle_request

old_router_get = Router.get

def sentry_router_get(self, request):
rv = old_router_get(self, request)
with capture_internal_exceptions():
with configure_scope() as scope:
scope.transaction = rv[0].__name__
return rv

Router.get = sentry_router_get

old_error_handler_lookup = ErrorHandler.lookup

def sentry_error_handler_lookup(self, exception):
_capture_exception(exception)
old_error_handler = old_error_handler_lookup(self, exception)

if old_error_handler is None:
return None

async def sentry_wrapped_error_handler(request, exception):
try:
response = old_error_handler(request, exception)
if isawaitable(response):
response = await response
return response
except Exception:
exc_info = sys.exc_info()
_capture_exception(exc_info)
reraise(*exc_info)

return sentry_wrapped_error_handler

ErrorHandler.lookup = sentry_error_handler_lookup


def _capture_exception(exception):
with capture_internal_exceptions():
hub = Hub.current
event, hint = event_from_exception(
exception,
with_locals=hub.client.options["with_locals"],
mechanism={"type": "sanic", "handled": False},
)

hub.capture_event(event, hint=hint)


def _make_request_processor(weak_request):
def sanic_processor(event, hint):
request = weak_request()
if request is None:
return event

with capture_internal_exceptions():
extractor = SanicRequestExtractor(request)
extractor.extract_into_event(event)

request_info = event["request"]
if "query_string" not in request_info:
request_info["query_string"] = extractor.urlparts.query

if "method" not in request_info:
request_info["method"] = request.method

if "env" not in request_info:
request_info["env"] = {"REMOTE_ADDR": request.remote_addr}

if "headers" not in request_info:
request_info["headers"] = _filter_headers(dict(request.headers))

return event

return sanic_processor


class SanicRequestExtractor(RequestExtractor):
def __init__(self, request):
RequestExtractor.__init__(self, request)
self.urlparts = urlparse.urlsplit(self.request.url)

def content_length(self):
if self.request.body is None:
return 0
return len(self.request.body)

def url(self):
return "%s://%s%s" % (
self.urlparts.scheme,
self.urlparts.netloc,
self.urlparts.path,
)

def cookies(self):
return dict(self.request.cookies)

def raw_data(self):
return self.request.body

def form(self):
return self.request.form

def is_json(self):
raise NotImplementedError()

def json(self):
return self.request.json

def files(self):
return self.request.files

def size_of_file(self, file):
return len(file.body or ())
30 changes: 30 additions & 0 deletions tests/integrations/flask/test_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,33 @@ def error_handler(err):

event, = events
assert response.data.decode("utf-8") == "Sentry error: %s" % event["event_id"]


def test_error_in_errorhandler(sentry_init, capture_events, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])

app.debug = False
app.testing = False

@app.route("/")
def index():
raise ValueError()

@app.errorhandler(500)
def error_handler(err):
1 / 0

events = capture_events()

client = app.test_client()

with pytest.raises(ZeroDivisionError):
client.get("/")

event1, event2 = events

exception, = event1["exception"]["values"]
assert exception["type"] == "ValueError"

exception, = event2["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
104 changes: 104 additions & 0 deletions tests/integrations/sanic/test_sanic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import pytest

sanic = pytest.importorskip("sanic")

from sentry_sdk import capture_message
from sentry_sdk.integrations.sanic import SanicIntegration

from sanic import Sanic, response


@pytest.fixture
def app():
app = Sanic(__name__)

@app.route("/message")
def hi(request):
capture_message("hi")
return response.text("ok")

return app


def test_request_data(sentry_init, app, capture_events):
sentry_init(integrations=[SanicIntegration()])
events = capture_events()

request, response = app.test_client.get("/message?foo=bar")
assert response.status == 200

event, = events
assert event["transaction"] == "hi"
assert event["request"]["env"] == {"REMOTE_ADDR": ""}
assert set(event["request"]["headers"]) == {
"accept",
"accept-encoding",
"host",
"user-agent",
}
assert event["request"]["query_string"] == "foo=bar"
assert event["request"]["url"].endswith("/message")
assert event["request"]["method"] == "GET"

# Assert that state is not leaked
events.clear()
capture_message("foo")
event, = events

assert "request" not in event
assert "transaction" not in event


def test_errors(sentry_init, app, capture_events):
sentry_init(integrations=[SanicIntegration()])
events = capture_events()

@app.route("/error")
def myerror(request):
raise ValueError("oh no")

request, response = app.test_client.get("/error")
assert response.status == 500

event, = events
assert event["transaction"] == "myerror"
exception, = event["exception"]["values"]

assert exception["type"] == "ValueError"
assert exception["value"] == "oh no"
assert any(
frame["filename"] == "test_sanic.py"
for frame in exception["stacktrace"]["frames"]
)


def test_error_in_errorhandler(sentry_init, app, capture_events):
sentry_init(integrations=[SanicIntegration()])
events = capture_events()

@app.route("/error")
def myerror(request):
raise ValueError("oh no")

@app.exception(ValueError)
def myhandler(request, exception):
1 / 0

request, response = app.test_client.get("/error")
assert response.status == 500

event1, event2 = events

exception, = event1["exception"]["values"]
assert exception["type"] == "ValueError"
assert any(
frame["filename"] == "test_sanic.py"
for frame in exception["stacktrace"]["frames"]
)

exception, = event2["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert any(
frame["filename"] == "test_sanic.py"
for frame in exception["stacktrace"]["frames"]
)
21 changes: 18 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,21 @@ envlist =

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

{py2.7,py3.7}-requests
{py3.7,py3.8}-sanic-0.8

{pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-celery-4
py3.7-aws_lambda
{pypy,py2.7}-celery-3

{py2.7,py3.7}-requests

py3.7-aws_lambda



[testenv]
deps =
-r test-requirements.txt

django-{1.6,1.7,1.8}: pytest-django<3.0
django-{1.9,1.10,1.11,2.0,2.1,dev}: pytest-django>=3.0
django-1.6: Django>=1.6,<1.7
Expand All @@ -39,15 +45,23 @@ deps =
django-2.0: Django>=2.0,<2.1
django-2.1: Django>=2.0,<2.1
django-dev: git+https://github.com/django/django.git#egg=Django

flask: flask-login
flask-0.11: Flask>=0.11,<0.12
flask-0.12: Flask>=0.12,<0.13
flask-1.0: Flask>=0.10,<0.11
flask-dev: git+https://github.com/pallets/flask.git#egg=flask

sanic-0.8: sanic>=0.8,<0.9
sanic: aiohttp

celery-3: Celery>=3.1,<4.0
celery-4: Celery>=4.0,<5.0

requests: requests>=2.0

aws_lambda: boto3
flask: flask-login

linters: black
linters: flake8
setenv =
Expand All @@ -58,6 +72,7 @@ setenv =
celery: TESTPATH=tests/integrations/celery
requests: TESTPATH=tests/integrations/requests
aws_lambda: TESTPATH=tests/integrations/aws_lambda
sanic: TESTPATH=tests/integrations/sanic
passenv =
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
Expand Down